dsh-wkgeo-ops 0.2.2 → 0.2.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.
- package/README.md +21 -10
- package/assets/icon.png +0 -0
- package/assets/icon.svg +3 -0
- package/client/client.js +355 -46
- package/lib/index.js +305 -2
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +8 -2
package/client/client.js
CHANGED
|
@@ -131,6 +131,25 @@ window.__ModuleLoader__.load({
|
|
|
131
131
|
initTitle: '第一步:先选站点根目录',
|
|
132
132
|
initHint: '所有站点的本地文件夹都放在这个根目录下。可以直接粘路径,也可以点「选择根目录」一层层点进来。',
|
|
133
133
|
initNote: '根目录只存在本机(DSH profile 里),不上传;只有根目录里面的文件夹能绑定给站点。',
|
|
134
|
+
// ---- v5:首次进入三步(① 选页根目录 → ② 授权 → ③ 建站,老板 2026-10-30 定) ----
|
|
135
|
+
stepRoot: '选页根目录',
|
|
136
|
+
stepAuth: '授权',
|
|
137
|
+
stepCreate: '建站',
|
|
138
|
+
stepRootDone: '页根目录:{dir}',
|
|
139
|
+
stepHint: '首次进入按顺序走:第 1 步选页根目录 → 第 2 步授权 → 第 3 步建站。',
|
|
140
|
+
authTitle: '第二步:授权 wkgeo 账号',
|
|
141
|
+
authHint: '建站要走 wkgeo CLI 的写接口,必须先授权。在终端里执行下面这条命令,浏览器会打开授权页;完成后回到这里点「我已授权,重新检查」。',
|
|
142
|
+
authCommand: 'wkgeo login',
|
|
143
|
+
authCheck: '我已授权,重新检查',
|
|
144
|
+
authChecking: '正在检查授权…',
|
|
145
|
+
authOk: '已授权:能读到站点列表',
|
|
146
|
+
authNote: '授权凭据由 wkgeo CLI 自己保存,本插件不读取、不显示、不上传账号密码或 token。',
|
|
147
|
+
needRoot: '先去第 1 步选页根目录,选定后才能建站。',
|
|
148
|
+
needAuth: '先去第 2 步完成授权(wkgeo login),之后才能建站。',
|
|
149
|
+
blockedTitle: '还差一步才能建站',
|
|
150
|
+
crashTitle: '悟空GEO-网站运营 · 面板异常',
|
|
151
|
+
crashRefresh: '刷新界面',
|
|
152
|
+
regWarn: '面板自检:',
|
|
134
153
|
rootLabel: '站点根目录',
|
|
135
154
|
rootUnset: '未设置根目录',
|
|
136
155
|
rootPick: '选择根目录',
|
|
@@ -248,6 +267,25 @@ window.__ModuleLoader__.load({
|
|
|
248
267
|
initTitle: 'Step 1: choose a site root folder',
|
|
249
268
|
initHint: 'Every site keeps its local folder under this root. Paste a path, or click "Choose root" and browse into it.',
|
|
250
269
|
initNote: 'The root is stored locally (in your DSH profile), never uploaded. Only folders inside the root can be bound to a site.',
|
|
270
|
+
// ---- v5: first run = root folder -> authorization -> create ----
|
|
271
|
+
stepRoot: 'root folder',
|
|
272
|
+
stepAuth: 'authorize',
|
|
273
|
+
stepCreate: 'create',
|
|
274
|
+
stepRootDone: 'Root folder: {dir}',
|
|
275
|
+
stepHint: 'First run goes in order: step 1 pick a root folder, step 2 authorize, step 3 create.',
|
|
276
|
+
authTitle: 'Step 2: authorize the wkgeo account',
|
|
277
|
+
authHint: 'Creating a site uses the wkgeo CLI write API, so authorization is required. Run the command below in a terminal; a browser page opens for authorization, then come back and click "I have authorized, check again".',
|
|
278
|
+
authCommand: 'wkgeo login',
|
|
279
|
+
authCheck: 'I have authorized, check again',
|
|
280
|
+
authChecking: 'Checking authorization…',
|
|
281
|
+
authOk: 'Authorized: the site list is readable',
|
|
282
|
+
authNote: 'Credentials are kept by the wkgeo CLI itself; this plugin never reads, shows or uploads your password or token.',
|
|
283
|
+
needRoot: 'Pick a root folder in step 1 first, then you can create a site.',
|
|
284
|
+
needAuth: 'Finish authorization in step 2 (wkgeo login) before creating a site.',
|
|
285
|
+
blockedTitle: 'One more step before creating a site',
|
|
286
|
+
crashTitle: 'wkgeo site ops · panel error',
|
|
287
|
+
crashRefresh: 'Reload UI',
|
|
288
|
+
regWarn: 'Panel self-check: ',
|
|
251
289
|
rootLabel: 'Site root folder',
|
|
252
290
|
rootUnset: 'no root folder yet',
|
|
253
291
|
rootPick: 'Choose root',
|
|
@@ -331,7 +369,9 @@ window.__ModuleLoader__.load({
|
|
|
331
369
|
height: '100%',
|
|
332
370
|
width: '100%',
|
|
333
371
|
minWidth: 0,
|
|
334
|
-
|
|
372
|
+
// 老板 2026-10-30 白屏复盘:主区容器是 flex 项,历史上有过「高度塌成 0、看起来全白」的情况,
|
|
373
|
+
// 这里给一个最小高度兜底;真渲染出来了,内容就一定会占可见高度。
|
|
374
|
+
minHeight: '320px',
|
|
335
375
|
flex: '1 1 auto',
|
|
336
376
|
gap: '10px',
|
|
337
377
|
padding: '12px',
|
|
@@ -956,12 +996,8 @@ window.__ModuleLoader__.load({
|
|
|
956
996
|
}
|
|
957
997
|
render() {
|
|
958
998
|
if (this.state.error) {
|
|
959
|
-
|
|
960
|
-
return h(
|
|
961
|
-
'div',
|
|
962
|
-
{ style: { ...STYLE.err, margin: '12px', fontSize: '12px', whiteSpace: 'pre-wrap' } },
|
|
963
|
-
'面板渲染出错:' + msg.slice(0, 600),
|
|
964
|
-
)
|
|
999
|
+
// 老板 2026-10-30:渲染异常不许变成一片空白 —— 消息 + 堆栈摘要直接打在面板上。
|
|
1000
|
+
return h(PanelCrash, { error: this.state.error, where: '面板渲染(PanelBoundary 捕获)' })
|
|
965
1001
|
}
|
|
966
1002
|
return this.props.children
|
|
967
1003
|
}
|
|
@@ -1512,6 +1548,107 @@ window.__ModuleLoader__.load({
|
|
|
1512
1548
|
)
|
|
1513
1549
|
}
|
|
1514
1550
|
|
|
1551
|
+
/**
|
|
1552
|
+
* 面板自检记录(老板 2026-10-30 白屏复盘):
|
|
1553
|
+
* 旧代码里注册失败被 `try{...}catch{return undefined}` 静默吞掉 —— 左栏有图标、点进去一片空白,
|
|
1554
|
+
* 界面上一个字都没有,只能靠猜。现在所有注册失败都记进这里,面板会把它打在首屏/错误页上。
|
|
1555
|
+
*/
|
|
1556
|
+
const REGISTRATION = { ok: {}, errors: [] }
|
|
1557
|
+
|
|
1558
|
+
function noteRegistration(label, err) {
|
|
1559
|
+
if (!err) {
|
|
1560
|
+
REGISTRATION.ok[label] = true
|
|
1561
|
+
return
|
|
1562
|
+
}
|
|
1563
|
+
const msg = String((err && err.message) || err || 'unknown')
|
|
1564
|
+
REGISTRATION.ok[label] = false
|
|
1565
|
+
const entry = label + ' → ' + msg
|
|
1566
|
+
if (REGISTRATION.errors[REGISTRATION.errors.length - 1] !== entry) {
|
|
1567
|
+
REGISTRATION.errors.push(entry)
|
|
1568
|
+
if (REGISTRATION.errors.length > 6) REGISTRATION.errors.shift()
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
console.error('[wkgeo-ops] slot register failed · ' + label, err)
|
|
1572
|
+
} catch (_consoleErr) {
|
|
1573
|
+
/* 忽略 */
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/** 错误兜底页:异常消息 + 堆栈摘要 + 注册自检,保证「绝不静默空白」。 */
|
|
1578
|
+
function PanelCrash(props) {
|
|
1579
|
+
const err = props ? props.error : null
|
|
1580
|
+
const where = (props && props.where) || '面板'
|
|
1581
|
+
const msg = String((err && err.message) || err || 'unknown')
|
|
1582
|
+
const stack = err && err.stack ? String(err.stack) : ''
|
|
1583
|
+
const trace = stack ? stack.split('\n').slice(0, 8).join('\n') : ''
|
|
1584
|
+
return h(
|
|
1585
|
+
'div',
|
|
1586
|
+
{ style: STYLE.wrap },
|
|
1587
|
+
h('div', { style: STYLE.bar }, h('span', { style: STYLE.barTitle }, MESSAGES.zh.crashTitle)),
|
|
1588
|
+
h(
|
|
1589
|
+
'div',
|
|
1590
|
+
{
|
|
1591
|
+
style: {
|
|
1592
|
+
...STYLE.err,
|
|
1593
|
+
whiteSpace: 'pre-wrap',
|
|
1594
|
+
wordBreak: 'break-word',
|
|
1595
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Consolas, monospace',
|
|
1596
|
+
fontSize: '12px',
|
|
1597
|
+
lineHeight: '1.5',
|
|
1598
|
+
padding: '10px',
|
|
1599
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1600
|
+
borderRadius: '8px',
|
|
1601
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1602
|
+
},
|
|
1603
|
+
},
|
|
1604
|
+
'位置:' + where + '\n' + msg + (trace ? '\n\n堆栈摘要:\n' + trace : ''),
|
|
1605
|
+
),
|
|
1606
|
+
REGISTRATION.errors.length
|
|
1607
|
+
? h(
|
|
1608
|
+
'div',
|
|
1609
|
+
{ style: { ...STYLE.warn, whiteSpace: 'pre-wrap', fontSize: '11px' } },
|
|
1610
|
+
MESSAGES.zh.regWarn + '\n' + REGISTRATION.errors.slice(-4).join('\n'),
|
|
1611
|
+
)
|
|
1612
|
+
: null,
|
|
1613
|
+
h(
|
|
1614
|
+
'div',
|
|
1615
|
+
{ style: STYLE.actions },
|
|
1616
|
+
h(
|
|
1617
|
+
'button',
|
|
1618
|
+
{
|
|
1619
|
+
type: 'button',
|
|
1620
|
+
style: STYLE.btnPrimary,
|
|
1621
|
+
onClick: () => {
|
|
1622
|
+
try {
|
|
1623
|
+
if (typeof window !== 'undefined' && window.location) window.location.reload()
|
|
1624
|
+
} catch (_reloadErr) {
|
|
1625
|
+
/* 忽略 */
|
|
1626
|
+
}
|
|
1627
|
+
},
|
|
1628
|
+
},
|
|
1629
|
+
MESSAGES.zh.crashRefresh,
|
|
1630
|
+
),
|
|
1631
|
+
),
|
|
1632
|
+
)
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/** 首次进入的三步指示条:① 选页根目录 → ② 授权 → ③ 建站 */
|
|
1636
|
+
function stepChip(t, label, done, active) {
|
|
1637
|
+
const mark = done ? '✓ ' : active ? '● ' : '○ '
|
|
1638
|
+
return h(
|
|
1639
|
+
'span',
|
|
1640
|
+
{
|
|
1641
|
+
style: {
|
|
1642
|
+
...STYLE.pill,
|
|
1643
|
+
color: done ? '#15803d' : active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary)',
|
|
1644
|
+
borderColor: done ? '#15803d' : undefined,
|
|
1645
|
+
opacity: done || active ? 1 : 0.75,
|
|
1646
|
+
},
|
|
1647
|
+
},
|
|
1648
|
+
mark + label,
|
|
1649
|
+
)
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1515
1652
|
/** 面板主体 */
|
|
1516
1653
|
function PanelBody(ctx) {
|
|
1517
1654
|
const t = makeT(ctx)
|
|
@@ -1570,6 +1707,18 @@ window.__ModuleLoader__.load({
|
|
|
1570
1707
|
}, [])
|
|
1571
1708
|
|
|
1572
1709
|
const openWizard = () => {
|
|
1710
|
+
// 三步顺序(老板 2026-10-30):① 页根目录 → ② 授权 → ③ 建站。前两步没完成不许进向导。
|
|
1711
|
+
if (!rootReady) {
|
|
1712
|
+
setView('init')
|
|
1713
|
+
setInitError(t('needRoot'))
|
|
1714
|
+
return
|
|
1715
|
+
}
|
|
1716
|
+
if (!authed) {
|
|
1717
|
+
setInitError('')
|
|
1718
|
+
setView('list')
|
|
1719
|
+
load(true)
|
|
1720
|
+
return
|
|
1721
|
+
}
|
|
1573
1722
|
setWizard(defaultWizard())
|
|
1574
1723
|
setWizardError('')
|
|
1575
1724
|
setRun({ busy: false, ok: false, siteId: '', log: [], unfilled: [], gaps: [] })
|
|
@@ -1824,12 +1973,25 @@ window.__ModuleLoader__.load({
|
|
|
1824
1973
|
const rootReady = Boolean(cfg.rootDir)
|
|
1825
1974
|
const boundCount = Object.keys(folders || {}).length
|
|
1826
1975
|
const cfgDown = cfg.phase === 'error'
|
|
1976
|
+
// 第 2 步「授权」的判定:站点接口能拿到 ok 数据即视为已授权;明确返回 NOT_LOGGED_IN 即未授权。
|
|
1977
|
+
const authed = Boolean(data) && data.ok !== false && !isLoginIssue
|
|
1978
|
+
const needAuth = Boolean(data) && isLoginIssue
|
|
1979
|
+
const canCreate = rootReady && authed
|
|
1980
|
+
const blockedHint = !rootReady ? t('needRoot') : !authed ? t('needAuth') : ''
|
|
1981
|
+
|
|
1982
|
+
const steps = h(
|
|
1983
|
+
'div',
|
|
1984
|
+
{ style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '6px' } },
|
|
1985
|
+
stepChip(t, '1 · ' + t('stepRoot'), rootReady, !rootReady),
|
|
1986
|
+
stepChip(t, '2 · ' + t('stepAuth'), authed, rootReady && !authed),
|
|
1987
|
+
stepChip(t, '3 · ' + t('stepCreate'), false, canCreate),
|
|
1988
|
+
)
|
|
1827
1989
|
|
|
1828
1990
|
const head = h(
|
|
1829
1991
|
'div',
|
|
1830
1992
|
{ style: STYLE.bar },
|
|
1831
1993
|
h('span', { style: STYLE.barTitle }, t('title')),
|
|
1832
|
-
h('span', { style: { ...STYLE.barMeta, fontSize: '10px', opacity: 0.65 }, title: 'client build' }, '
|
|
1994
|
+
h('span', { style: { ...STYLE.barMeta, fontSize: '10px', opacity: 0.65 }, title: 'client build' }, 'v6'),
|
|
1833
1995
|
h(
|
|
1834
1996
|
'span',
|
|
1835
1997
|
{ style: { ...STYLE.badge, cursor: 'pointer' }, title: cfg.rootDir || t('rootUnset'), onClick: openBrowserRoot },
|
|
@@ -1837,7 +1999,17 @@ window.__ModuleLoader__.load({
|
|
|
1837
1999
|
),
|
|
1838
2000
|
h('button', { type: 'button', style: STYLE.btn, disabled: busy, onClick: () => load(true), title: t('refresh') }, busy ? t('refreshing') : t('refresh')),
|
|
1839
2001
|
sites.length > 0 && (view === 'list' || view === 'detail')
|
|
1840
|
-
? h(
|
|
2002
|
+
? h(
|
|
2003
|
+
'button',
|
|
2004
|
+
{
|
|
2005
|
+
type: 'button',
|
|
2006
|
+
style: canCreate ? STYLE.btn : { ...STYLE.btn, opacity: 0.55, cursor: 'not-allowed' },
|
|
2007
|
+
disabled: !canCreate,
|
|
2008
|
+
title: blockedHint || t('create'),
|
|
2009
|
+
onClick: openWizard,
|
|
2010
|
+
},
|
|
2011
|
+
t('create'),
|
|
2012
|
+
)
|
|
1841
2013
|
: null,
|
|
1842
2014
|
h(
|
|
1843
2015
|
'span',
|
|
@@ -1847,6 +2019,34 @@ window.__ModuleLoader__.load({
|
|
|
1847
2019
|
),
|
|
1848
2020
|
)
|
|
1849
2021
|
|
|
2022
|
+
// 第 2 步:授权卡片(放在建站之前,未授权时首屏就是它,建站按钮全部禁用并提示去哪一步)
|
|
2023
|
+
const authPanel = h(
|
|
2024
|
+
'div',
|
|
2025
|
+
{ style: STYLE.initCard },
|
|
2026
|
+
h('div', { style: STYLE.bar }, h('span', { style: STYLE.barTitle }, t('authTitle'))),
|
|
2027
|
+
h('div', { style: STYLE.hint }, t('authHint')),
|
|
2028
|
+
h('div', { style: STYLE.growRow }, h('code', { style: STYLE.code }, t('authCommand'))),
|
|
2029
|
+
h(
|
|
2030
|
+
'div',
|
|
2031
|
+
{ style: STYLE.actions },
|
|
2032
|
+
h(
|
|
2033
|
+
'button',
|
|
2034
|
+
{ type: 'button', style: STYLE.btnPrimary, disabled: busy, onClick: () => load(true) },
|
|
2035
|
+
busy ? t('authChecking') : t('authCheck'),
|
|
2036
|
+
),
|
|
2037
|
+
h('button', { type: 'button', style: STYLE.btn, onClick: () => setView('init') }, t('rootChange')),
|
|
2038
|
+
// 第 3 步的入口先亮出来但禁用:未授权时点不动,title 说明缺的是哪一步
|
|
2039
|
+
h(
|
|
2040
|
+
'button',
|
|
2041
|
+
{ type: 'button', style: STYLE.btn, disabled: true, title: blockedHint || t('needAuth') },
|
|
2042
|
+
t('create'),
|
|
2043
|
+
),
|
|
2044
|
+
),
|
|
2045
|
+
needAuth && !busy ? h('div', { style: STYLE.err }, t('loginHint')) : null,
|
|
2046
|
+
state.error ? h('div', { style: STYLE.err }, String(state.error).slice(0, 300)) : null,
|
|
2047
|
+
h('div', { style: STYLE.note }, t('authNote')),
|
|
2048
|
+
)
|
|
2049
|
+
|
|
1850
2050
|
const initPanel = h(
|
|
1851
2051
|
'div',
|
|
1852
2052
|
{ style: STYLE.initCard },
|
|
@@ -1945,6 +2145,9 @@ window.__ModuleLoader__.load({
|
|
|
1945
2145
|
})
|
|
1946
2146
|
} else if (!rootReady && !cfgDown) {
|
|
1947
2147
|
body = initPanel
|
|
2148
|
+
} else if (needAuth) {
|
|
2149
|
+
// 第 2 步:目录已选、但还没授权 → 先授权,建站按钮此时是禁用的
|
|
2150
|
+
body = authPanel
|
|
1948
2151
|
} else if (state.phase === 'loading' && !data) {
|
|
1949
2152
|
body = h('div', { style: STYLE.state }, h('div', { style: STYLE.stateTitle }, t('refreshing')))
|
|
1950
2153
|
} else if (state.error) {
|
|
@@ -1953,9 +2156,19 @@ window.__ModuleLoader__.load({
|
|
|
1953
2156
|
{ style: STYLE.state },
|
|
1954
2157
|
h('div', { style: STYLE.stateTitle }, isLoginIssue ? t('loginHint') : t('error')),
|
|
1955
2158
|
h('div', null, String(state.error).slice(0, 300)),
|
|
1956
|
-
|
|
2159
|
+
!canCreate ? h('div', { style: STYLE.hint }, blockedHint) : null,
|
|
1957
2160
|
h('button', { type: 'button', style: STYLE.btn, onClick: () => load(true) }, t('retry')),
|
|
1958
|
-
h(
|
|
2161
|
+
h(
|
|
2162
|
+
'button',
|
|
2163
|
+
{
|
|
2164
|
+
type: 'button',
|
|
2165
|
+
style: canCreate ? STYLE.btnPrimary : { ...STYLE.btnPrimary, opacity: 0.55, cursor: 'not-allowed' },
|
|
2166
|
+
disabled: !canCreate,
|
|
2167
|
+
title: blockedHint || t('create'),
|
|
2168
|
+
onClick: openWizard,
|
|
2169
|
+
},
|
|
2170
|
+
t('create'),
|
|
2171
|
+
),
|
|
1959
2172
|
)
|
|
1960
2173
|
} else if (sites.length === 0) {
|
|
1961
2174
|
body = h(
|
|
@@ -1963,7 +2176,18 @@ window.__ModuleLoader__.load({
|
|
|
1963
2176
|
{ style: STYLE.state },
|
|
1964
2177
|
h('div', { style: STYLE.stateTitle }, t('empty')),
|
|
1965
2178
|
h('div', null, t('emptyHint')),
|
|
1966
|
-
h('
|
|
2179
|
+
!canCreate ? h('div', { style: STYLE.hint }, blockedHint) : null,
|
|
2180
|
+
h(
|
|
2181
|
+
'button',
|
|
2182
|
+
{
|
|
2183
|
+
type: 'button',
|
|
2184
|
+
style: canCreate ? { ...STYLE.btnPrimary, ...STYLE.btnBlock } : { ...STYLE.btn, ...STYLE.btnBlock, opacity: 0.55, cursor: 'not-allowed' },
|
|
2185
|
+
disabled: !canCreate,
|
|
2186
|
+
title: blockedHint || t('createFirst'),
|
|
2187
|
+
onClick: openWizard,
|
|
2188
|
+
},
|
|
2189
|
+
t('createFirst'),
|
|
2190
|
+
),
|
|
1967
2191
|
h('div', { style: STYLE.hint }, '没登录会取不到站点:', h('code', { style: STYLE.code }, 'wkgeo login')),
|
|
1968
2192
|
)
|
|
1969
2193
|
} else {
|
|
@@ -1980,7 +2204,16 @@ window.__ModuleLoader__.load({
|
|
|
1980
2204
|
onPickFolder: openBrowserSite,
|
|
1981
2205
|
}),
|
|
1982
2206
|
),
|
|
1983
|
-
h(
|
|
2207
|
+
h(
|
|
2208
|
+
'div',
|
|
2209
|
+
{
|
|
2210
|
+
key: '__new__',
|
|
2211
|
+
style: canCreate ? STYLE.newCard : { ...STYLE.newCard, opacity: 0.55, cursor: 'not-allowed' },
|
|
2212
|
+
title: blockedHint || t('newCard'),
|
|
2213
|
+
onClick: canCreate ? openWizard : undefined,
|
|
2214
|
+
},
|
|
2215
|
+
canCreate ? t('newCard') : t('blockedTitle') + ':' + blockedHint,
|
|
2216
|
+
),
|
|
1984
2217
|
)
|
|
1985
2218
|
}
|
|
1986
2219
|
|
|
@@ -1988,6 +2221,8 @@ window.__ModuleLoader__.load({
|
|
|
1988
2221
|
'div',
|
|
1989
2222
|
{ style: STYLE.wrap },
|
|
1990
2223
|
head,
|
|
2224
|
+
steps,
|
|
2225
|
+
rootReady ? h('div', { style: { ...STYLE.hint, fontSize: '11px' }, title: cfg.rootDir }, t('stepRootDone', { dir: cfg.rootDir })) : null,
|
|
1991
2226
|
cfgDown ? h('div', { style: STYLE.warn }, t('cfgUnavailable')) : null,
|
|
1992
2227
|
rootReady && cfg.exists === false ? h('div', { style: STYLE.warn }, t('rootMissingNow')) : null,
|
|
1993
2228
|
folderError ? h('div', { style: STYLE.err }, String(folderError).slice(0, 300)) : null,
|
|
@@ -1998,25 +2233,98 @@ window.__ModuleLoader__.load({
|
|
|
1998
2233
|
data && data.ageMs != null ? t('updated', { t: new Date().toLocaleTimeString() }) : '',
|
|
1999
2234
|
boundCount > 0 ? ' · 📁 ' + boundCount : '',
|
|
2000
2235
|
),
|
|
2236
|
+
REGISTRATION.errors.length
|
|
2237
|
+
? h(
|
|
2238
|
+
'div',
|
|
2239
|
+
{ style: { ...STYLE.warn, whiteSpace: 'pre-wrap', fontSize: '11px' } },
|
|
2240
|
+
t('regWarn') + REGISTRATION.errors.slice(-3).join(' | '),
|
|
2241
|
+
)
|
|
2242
|
+
: null,
|
|
2001
2243
|
)
|
|
2002
2244
|
}
|
|
2003
2245
|
|
|
2004
2246
|
function apply(ctx) {
|
|
2005
2247
|
// 面板入口:错误边界包住面板本身 —— 渲染异常变成可复制的报错文本,绝不整块空白。
|
|
2006
|
-
|
|
2248
|
+
// 面板入口:① 函数级 try/catch 兜住「构造元素树就抛错」;② PanelBoundary 兜住渲染期异常。
|
|
2249
|
+
// 两条都不许再变成空白 —— 出错就打错误页(异常消息 + 堆栈摘要 + 注册自检)。
|
|
2250
|
+
const OpsPanel = () => {
|
|
2251
|
+
try {
|
|
2252
|
+
return h(PanelBoundary, null, h(PanelBody, ctx))
|
|
2253
|
+
} catch (err) {
|
|
2254
|
+
return h(PanelCrash, { error: err, where: 'OpsPanel 构造元素树' })
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2007
2257
|
|
|
2008
|
-
|
|
2258
|
+
/** 注册期兜底:失败必须记账(旧代码在这里静默 return undefined,正是白屏查不出来的原因)。 */
|
|
2259
|
+
const guard = (label, fn) => {
|
|
2009
2260
|
try {
|
|
2010
2261
|
if (typeof ctx.effect === 'function') return ctx.effect(fn, label)
|
|
2011
2262
|
return fn()
|
|
2012
|
-
} catch (
|
|
2263
|
+
} catch (err) {
|
|
2264
|
+
noteRegistration(label, err)
|
|
2013
2265
|
return undefined
|
|
2014
2266
|
}
|
|
2015
2267
|
}
|
|
2016
2268
|
|
|
2017
|
-
|
|
2269
|
+
const SLOT_RETRY_MAX = 20
|
|
2270
|
+
const SLOT_RETRY_MS = 300
|
|
2271
|
+
|
|
2272
|
+
/**
|
|
2273
|
+
* 槽位注册(白屏根因修复,老板 2026-10-30):
|
|
2274
|
+
* DSH 的 SlotCore.register 在「槽位还没被父 entry 的 children 表声明」时会**同步抛错**
|
|
2275
|
+
* (`slot "main" is not declared (a parent entry's children table must declare it)`)。
|
|
2276
|
+
* 旧代码是「直接 register + catch 静默吞掉」:只要那次 register 抛了,`main` 就永远不存在,
|
|
2277
|
+
* 结果正是「左栏图标在、点进去一片空白、界面没有任何报错」。
|
|
2278
|
+
* 官方范式(DSH 内置插件全部如此)是 ctx.slots.inject(name, () => ctx.slots.register(...)):
|
|
2279
|
+
* 声明前挂起、声明后自动补注册;这里再叠加定时重试,双保险。
|
|
2280
|
+
*/
|
|
2281
|
+
const slotRegister = (label, options, component) => {
|
|
2282
|
+
const name = options && options.name
|
|
2283
|
+
let off = null
|
|
2284
|
+
let timer = null
|
|
2285
|
+
let tries = 0
|
|
2286
|
+
let disposed = false
|
|
2287
|
+
const attempt = () => {
|
|
2288
|
+
if (disposed) return undefined
|
|
2289
|
+
try {
|
|
2290
|
+
off = ctx.slots.register(options, component)
|
|
2291
|
+
noteRegistration(label, null)
|
|
2292
|
+
return off
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
noteRegistration(label, err)
|
|
2295
|
+
tries += 1
|
|
2296
|
+
if (tries <= SLOT_RETRY_MAX && !timer) {
|
|
2297
|
+
timer = setTimeout(() => {
|
|
2298
|
+
timer = null
|
|
2299
|
+
attempt()
|
|
2300
|
+
}, SLOT_RETRY_MS)
|
|
2301
|
+
}
|
|
2302
|
+
return undefined
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
let offInject = null
|
|
2306
|
+
if (typeof ctx.slots.inject === 'function') {
|
|
2307
|
+
try {
|
|
2308
|
+
offInject = ctx.slots.inject(name, attempt)
|
|
2309
|
+
} catch (err) {
|
|
2310
|
+
noteRegistration(label, err)
|
|
2311
|
+
attempt()
|
|
2312
|
+
}
|
|
2313
|
+
} else {
|
|
2314
|
+
attempt()
|
|
2315
|
+
}
|
|
2316
|
+
return () => {
|
|
2317
|
+
disposed = true
|
|
2318
|
+
if (timer) clearTimeout(timer)
|
|
2319
|
+
timer = null
|
|
2320
|
+
if (typeof offInject === 'function') offInject()
|
|
2321
|
+
if (typeof off === 'function') off()
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
guard('wkgeo-ops:locale', () => ctx.locale.register(NS, { zh: MESSAGES.zh, en: MESSAGES.en }))
|
|
2018
2326
|
|
|
2019
|
-
|
|
2327
|
+
guard('wkgeo-ops:tab', () =>
|
|
2020
2328
|
ctx.sidebarRightTabs.register({
|
|
2021
2329
|
id: PANEL_ID,
|
|
2022
2330
|
kind: 'wkgeo-ops',
|
|
@@ -2043,18 +2351,19 @@ window.__ModuleLoader__.load({
|
|
|
2043
2351
|
}),
|
|
2044
2352
|
)
|
|
2045
2353
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
),
|
|
2354
|
+
guard('wkgeo-ops:pane', () =>
|
|
2355
|
+
slotRegister(
|
|
2356
|
+
'sidebar.right.pane.tab',
|
|
2357
|
+
{ name: 'sidebar.right.pane.tab', key: PANEL_ID, locale: NS, inject: () => ({}) },
|
|
2358
|
+
OpsPanel,
|
|
2052
2359
|
),
|
|
2053
2360
|
)
|
|
2054
2361
|
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2362
|
+
guard('wkgeo-ops:pane-title', () =>
|
|
2363
|
+
slotRegister(
|
|
2364
|
+
'sidebar.right.pane.tab.title',
|
|
2365
|
+
{ name: 'sidebar.right.pane.tab.title', key: PANEL_ID },
|
|
2366
|
+
() => h('span', null, SIDEBAR_ROW_LABEL),
|
|
2058
2367
|
),
|
|
2059
2368
|
)
|
|
2060
2369
|
|
|
@@ -2063,8 +2372,9 @@ window.__ModuleLoader__.load({
|
|
|
2063
2372
|
// ① sidebar.panellist —— 左栏里的一行图标(id = PANEL_ID)
|
|
2064
2373
|
// ② main —— 点该图标后中央区域渲染的面板(key = PANEL_ID)
|
|
2065
2374
|
// 少一条就看不到入口(只有①会点不开,只有②没有图标)。停用插件时把界面交还对话。
|
|
2066
|
-
|
|
2067
|
-
|
|
2375
|
+
guard('wkgeo-ops:sidebar-row', () =>
|
|
2376
|
+
slotRegister(
|
|
2377
|
+
'sidebar.panellist',
|
|
2068
2378
|
{
|
|
2069
2379
|
name: 'sidebar.panellist',
|
|
2070
2380
|
id: PANEL_ID,
|
|
@@ -2073,24 +2383,23 @@ window.__ModuleLoader__.load({
|
|
|
2073
2383
|
locale: NS,
|
|
2074
2384
|
},
|
|
2075
2385
|
(owner = {}) => h(OpsIcon, { size: owner.size }),
|
|
2076
|
-
)
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
)
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
/* 尽力而为 */
|
|
2386
|
+
),
|
|
2387
|
+
)
|
|
2388
|
+
|
|
2389
|
+
guard('wkgeo-ops:main', () =>
|
|
2390
|
+
slotRegister('main', { name: 'main', key: PANEL_ID, locale: NS, inject: () => ({}) }, OpsPanel),
|
|
2391
|
+
)
|
|
2392
|
+
|
|
2393
|
+
guard('wkgeo-ops:cleanup', () => () => {
|
|
2394
|
+
try {
|
|
2395
|
+
const layout = typeof ctx.get === 'function' ? ctx.get('layout') : null
|
|
2396
|
+
const info = layout && layout.panelInfo
|
|
2397
|
+
if (info && typeof info.getSnapshot === 'function') {
|
|
2398
|
+
const snapshot = info.getSnapshot()
|
|
2399
|
+
if (snapshot && snapshot.activePanelId === PANEL_ID) layout.selectPanel(null)
|
|
2091
2400
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2401
|
+
} catch (_err) {
|
|
2402
|
+
/* 尽力而为 */
|
|
2094
2403
|
}
|
|
2095
2404
|
})
|
|
2096
2405
|
}
|