dsh-opencode-palette 2.0.5 → 2.0.7
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 +1 -1
- package/lib/client.js +201 -31
- package/lib/event-list.json +87 -42
- package/lib/index.js +198 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ dsh plugin --profile web add dsh-opencode-palette
|
|
|
45
45
|
|
|
46
46
|
<h2 align="center"><sub>EXTENSIONS</sub><br>What the opencode themes come with</h2>
|
|
47
47
|
|
|
48
|
-
**Plenty of fonts.** Typography is its own dimension: apply it to all text or code only, 11–
|
|
48
|
+
**Plenty of fonts.** Typography is its own dimension: apply it to all text or code only, 11–24 px, your pick of code font — orthogonal to color, kept across restarts.
|
|
49
49
|
|
|
50
50
|
**A panel you can find.** 38 entries grouped by color family, searchable, and following your DSH interface language (中文 / English).
|
|
51
51
|
|
package/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-opencode-palette v2.0.
|
|
2
|
+
* dsh-opencode-palette v2.0.7 — 浏览器半(构建产物,勿手改)
|
|
3
3
|
* 数据驱动管线:opencode v1.18.12 官方主题 JSON → 颜色解析 → DSH 适配注入
|
|
4
4
|
* 面板「检查更新」:dsh-plugin-update 客户端入口经构建期内联(vendor)
|
|
5
5
|
* 源:src/engine/* + runtime/*.mjs + npm 包 dsh-log / dsh-plugin-update 的客户端入口
|
|
@@ -1525,24 +1525,32 @@ function buildTypographyCss(typography) {
|
|
|
1525
1525
|
const bodyFont = mode === 'mono' ? codeFont : SANS_STACK
|
|
1526
1526
|
const lh = size + 9
|
|
1527
1527
|
const small = size - 1
|
|
1528
|
+
// 标题阶梯跟随 size(size=13 时恰好还原旧固定值 16/15/14,避免默认视觉漂移):
|
|
1529
|
+
// H1 = size+3 / lh+2,H2 = size+2 / lh,H3 = size+1 / lh-1
|
|
1530
|
+
const h1 = size + 3
|
|
1531
|
+
const h1lh = lh + 2
|
|
1532
|
+
const h2 = size + 2
|
|
1533
|
+
const h2lh = lh
|
|
1534
|
+
const h3 = size + 1
|
|
1535
|
+
const h3lh = lh - 1
|
|
1528
1536
|
return [FONT_FACE_CSS,
|
|
1529
1537
|
'body,body[data-ds-dark-theme]{',
|
|
1530
1538
|
'--dsw-font-family:' + bodyFont + ';',
|
|
1531
1539
|
'--ds-font-family-code:' + codeFont + ';',
|
|
1532
|
-
// F4 新卡片字号(DSH 0.1.7 CodeCard .card/.body 只读此变量;size=13 默认恰好还原上游 11px/19px,跟随 11–
|
|
1540
|
+
// F4 新卡片字号(DSH 0.1.7 CodeCard .card/.body 只读此变量;size=13 默认恰好还原上游 11px/19px,跟随 11–24 档)
|
|
1533
1541
|
'--dsw-font-markdown-code-block:' + (size - 2) + 'px/' + (size + 6) + 'px var(--ds-font-family-code);',
|
|
1534
1542
|
'--dsw-font-markdown-base:' + size + 'px/' + lh + 'px var(--dsw-font-family);',
|
|
1535
1543
|
'--dsw-font-markdown-base-font-size:' + size + 'px;',
|
|
1536
1544
|
'--dsw-font-markdown-base-line-height:' + lh + 'px;',
|
|
1537
|
-
'--dsw-font-markdown-h1:700
|
|
1538
|
-
'--dsw-font-markdown-h1-font-size:
|
|
1539
|
-
'--dsw-font-markdown-h1-line-height:
|
|
1540
|
-
'--dsw-font-markdown-h2:700
|
|
1541
|
-
'--dsw-font-markdown-h2-font-size:
|
|
1542
|
-
'--dsw-font-markdown-h2-line-height:
|
|
1543
|
-
'--dsw-font-markdown-h3:600
|
|
1544
|
-
'--dsw-font-markdown-h3-font-size:
|
|
1545
|
-
'--dsw-font-markdown-h3-line-height:
|
|
1545
|
+
'--dsw-font-markdown-h1:700 ' + h1 + 'px/' + h1lh + 'px var(--dsw-font-family);',
|
|
1546
|
+
'--dsw-font-markdown-h1-font-size:' + h1 + 'px;',
|
|
1547
|
+
'--dsw-font-markdown-h1-line-height:' + h1lh + 'px;',
|
|
1548
|
+
'--dsw-font-markdown-h2:700 ' + h2 + 'px/' + h2lh + 'px var(--dsw-font-family);',
|
|
1549
|
+
'--dsw-font-markdown-h2-font-size:' + h2 + 'px;',
|
|
1550
|
+
'--dsw-font-markdown-h2-line-height:' + h2lh + 'px;',
|
|
1551
|
+
'--dsw-font-markdown-h3:600 ' + h3 + 'px/' + h3lh + 'px var(--dsw-font-family);',
|
|
1552
|
+
'--dsw-font-markdown-h3-font-size:' + h3 + 'px;',
|
|
1553
|
+
'--dsw-font-markdown-h3-line-height:' + h3lh + 'px;',
|
|
1546
1554
|
'--dsw-font-markdown-small:' + small + 'px/' + (small + 8) + 'px var(--dsw-font-family);',
|
|
1547
1555
|
'--dsw-font-markdown-small-font-size:' + small + 'px;',
|
|
1548
1556
|
'--dsw-font-markdown-small-line-height:' + (small + 8) + 'px;',
|
|
@@ -9716,6 +9724,9 @@ __mods["channel"] = { CHANNEL, ENDPOINT, ROUTE_PATH, PLUGIN_ID, PHONE_PREFIX, TA
|
|
|
9716
9724
|
* 电话名与轮询间隔由调用方从更新包的客户端入口派生后传进来(本文件不写字面量)。
|
|
9717
9725
|
*/
|
|
9718
9726
|
|
|
9727
|
+
/** 与 runtime/channel.mjs 的 PLUGIN_ID 同值:本文件是纯逻辑模块,不引运行时模块,故就地声明一次。 */
|
|
9728
|
+
const PLUGIN_ID = 'dsh-opencode-palette'
|
|
9729
|
+
|
|
9719
9730
|
/** 宿主回包 → 面板要的几个事实(纯函数,便于离线核对)。 */
|
|
9720
9731
|
function readSnapshot(res) {
|
|
9721
9732
|
const snap = res && res.snapshot ? res.snapshot : null
|
|
@@ -9797,6 +9808,9 @@ function createUpdateController(deps) {
|
|
|
9797
9808
|
jobMessage: null,
|
|
9798
9809
|
pending: false,
|
|
9799
9810
|
failure: null,
|
|
9811
|
+
// 弹窗外的可见结果({key, kind} 或 null):失败与「装好了但没进待重启」都走这里,
|
|
9812
|
+
// 否则用户点一下没动静,看不出是成功、失败还是没反应(2026-09-30 真机反馈)。
|
|
9813
|
+
notice: null,
|
|
9800
9814
|
}
|
|
9801
9815
|
let pollTimer = null
|
|
9802
9816
|
const listeners = []
|
|
@@ -9813,6 +9827,14 @@ function createUpdateController(deps) {
|
|
|
9813
9827
|
function record(level, event, fields, latencyMs) {
|
|
9814
9828
|
try { log(level, event, fields) } catch (e) { /* 忽略 */ }
|
|
9815
9829
|
}
|
|
9830
|
+
/** 回包里的错误码(宿主半判词,如 invalid-release / check-failed);取不到就退成 unknown。
|
|
9831
|
+
* 更新包的失败形状是 {ok:false, error:'<code>', errorKind}——error 是字符串,不是对象。 */
|
|
9832
|
+
function codeOf(res) {
|
|
9833
|
+
const error = res && res.error !== undefined && res.error !== null ? res.error : null
|
|
9834
|
+
if (typeof error === 'string') return error.slice(0, 40)
|
|
9835
|
+
const code = error && (error.code || error.message) ? String(error.code || error.message) : 'unknown'
|
|
9836
|
+
return code.slice(0, 40)
|
|
9837
|
+
}
|
|
9816
9838
|
function stopPolling() {
|
|
9817
9839
|
if (pollTimer !== null) {
|
|
9818
9840
|
try { clearTimer(pollTimer) } catch (e) { /* 忽略 */ }
|
|
@@ -9853,7 +9875,7 @@ function createUpdateController(deps) {
|
|
|
9853
9875
|
method: phone,
|
|
9854
9876
|
kind: kind,
|
|
9855
9877
|
errorHash: hash8(String((error && error.message) || error || kind)),
|
|
9856
|
-
pluginId:
|
|
9878
|
+
pluginId: PLUGIN_ID,
|
|
9857
9879
|
})
|
|
9858
9880
|
}
|
|
9859
9881
|
|
|
@@ -9866,7 +9888,7 @@ function createUpdateController(deps) {
|
|
|
9866
9888
|
latencyMs: Date.now() - startedAt,
|
|
9867
9889
|
ok: true,
|
|
9868
9890
|
kind: String((args && args.kind) || phone),
|
|
9869
|
-
pluginId:
|
|
9891
|
+
pluginId: PLUGIN_ID,
|
|
9870
9892
|
})
|
|
9871
9893
|
} else {
|
|
9872
9894
|
transportFail(phone, 'not-ok', (res && res.error) || 'not-ok')
|
|
@@ -9892,19 +9914,34 @@ function createUpdateController(deps) {
|
|
|
9892
9914
|
if (!state.available) return 'failed'
|
|
9893
9915
|
if (state.checking || state.installing) return 'busy'
|
|
9894
9916
|
// 已查到有新版且凭证还在:直接开弹窗,不重复联网
|
|
9895
|
-
if (state.hasNew && state.checkId) { patch({ dialogOpen: true, failure: null }); return 'new' }
|
|
9896
|
-
patch({ checking: true, failure: null })
|
|
9917
|
+
if (state.hasNew && state.checkId) { patch({ dialogOpen: true, failure: null, notice: null }); return 'new' }
|
|
9918
|
+
patch({ checking: true, failure: null, notice: null })
|
|
9919
|
+
record('info', 'update.check.start', { trigger: 'manual', pluginId: PLUGIN_ID })
|
|
9920
|
+
const startedAt = Date.now()
|
|
9897
9921
|
try {
|
|
9898
9922
|
const res = await invoke(phones.updateCheck, {})
|
|
9899
9923
|
patch({ checking: false })
|
|
9900
|
-
if (!res || res.ok !== true) {
|
|
9924
|
+
if (!res || res.ok !== true) {
|
|
9925
|
+
const code = codeOf(res)
|
|
9926
|
+
record('warn', 'update.check.fail', { trigger: 'manual', code: code, errorHash: hash8(code), pluginId: PLUGIN_ID })
|
|
9927
|
+
patch({ notice: { key: 'updateCheckFail', kind: 'error' } })
|
|
9928
|
+
return 'failed'
|
|
9929
|
+
}
|
|
9901
9930
|
const facts = absorb(res)
|
|
9931
|
+
record('info', 'update.check.ok', {
|
|
9932
|
+
trigger: 'manual', hasNew: facts.canInstall, latest: facts.latest || '',
|
|
9933
|
+
latencyMs: Date.now() - startedAt, pluginId: PLUGIN_ID,
|
|
9934
|
+
})
|
|
9902
9935
|
if (facts.canInstall) { patch({ dialogOpen: true }); return 'new' }
|
|
9903
9936
|
if (facts.pending) return 'latest'
|
|
9904
9937
|
return 'latest'
|
|
9905
9938
|
} catch (e) {
|
|
9906
9939
|
patch({ checking: false })
|
|
9907
9940
|
transportFail(phones.updateCheck, 'throw', e)
|
|
9941
|
+
record('warn', 'update.check.fail', {
|
|
9942
|
+
trigger: 'manual', code: 'transport', errorHash: hash8(String((e && e.message) || e)), pluginId: PLUGIN_ID,
|
|
9943
|
+
})
|
|
9944
|
+
patch({ notice: { key: 'updateCheckFail', kind: 'error' } })
|
|
9908
9945
|
return 'failed'
|
|
9909
9946
|
}
|
|
9910
9947
|
}
|
|
@@ -9918,16 +9955,29 @@ function createUpdateController(deps) {
|
|
|
9918
9955
|
if (state.pending) return 'latest'
|
|
9919
9956
|
if (state.hasNew && state.checkId) return 'new'
|
|
9920
9957
|
patch({ checking: true, failure: null })
|
|
9958
|
+
record('info', 'update.check.start', { trigger: 'auto', pluginId: PLUGIN_ID })
|
|
9959
|
+
const startedAt = Date.now()
|
|
9921
9960
|
try {
|
|
9922
9961
|
const res = await invoke(phones.updateCheck, {})
|
|
9923
9962
|
patch({ checking: false })
|
|
9924
|
-
if (!res || res.ok !== true)
|
|
9963
|
+
if (!res || res.ok !== true) {
|
|
9964
|
+
const code = codeOf(res)
|
|
9965
|
+
record('warn', 'update.check.fail', { trigger: 'auto', code: code, errorHash: hash8(code), pluginId: PLUGIN_ID })
|
|
9966
|
+
return 'failed'
|
|
9967
|
+
}
|
|
9925
9968
|
const facts = absorb(res)
|
|
9969
|
+
record('info', 'update.check.ok', {
|
|
9970
|
+
trigger: 'auto', hasNew: facts.canInstall, latest: facts.latest || '',
|
|
9971
|
+
latencyMs: Date.now() - startedAt, pluginId: PLUGIN_ID,
|
|
9972
|
+
})
|
|
9926
9973
|
if (facts.canInstall) return 'new'
|
|
9927
9974
|
return 'latest'
|
|
9928
9975
|
} catch (e) {
|
|
9929
9976
|
patch({ checking: false })
|
|
9930
9977
|
transportFail(phones.updateCheck, 'throw', e)
|
|
9978
|
+
record('warn', 'update.check.fail', {
|
|
9979
|
+
trigger: 'auto', code: 'transport', errorHash: hash8(String((e && e.message) || e)), pluginId: PLUGIN_ID,
|
|
9980
|
+
})
|
|
9931
9981
|
return 'failed'
|
|
9932
9982
|
}
|
|
9933
9983
|
}
|
|
@@ -9946,23 +9996,38 @@ function createUpdateController(deps) {
|
|
|
9946
9996
|
async function install() {
|
|
9947
9997
|
if (!state.available || state.installing) return false
|
|
9948
9998
|
if (!state.checkId) return false
|
|
9949
|
-
patch({ installing: true, failure: null })
|
|
9999
|
+
patch({ installing: true, failure: null, notice: null })
|
|
10000
|
+
record('info', 'update.install.start', { latest: String(state.latest || ''), pluginId: PLUGIN_ID })
|
|
9950
10001
|
const requestId = 'req-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000))
|
|
9951
10002
|
try {
|
|
9952
10003
|
const res = await invoke(phones.updateInstall, { checkId: state.checkId, requestId: requestId })
|
|
9953
10004
|
patch({ installing: false })
|
|
9954
10005
|
if (res && res.ok === true) {
|
|
9955
|
-
absorb(res)
|
|
10006
|
+
const facts = absorb(res)
|
|
9956
10007
|
patch({ dialogOpen: false })
|
|
10008
|
+
record('info', 'update.install.ok', {
|
|
10009
|
+
latest: String((facts && facts.latest) || state.latest || ''),
|
|
10010
|
+
pending: !!(facts && facts.pending),
|
|
10011
|
+
pluginId: PLUGIN_ID,
|
|
10012
|
+
})
|
|
10013
|
+
// 待重启由常驻横幅承接;万一没进待重启,至少留一句「重启后生效」,别让用户对着没动静的按钮猜
|
|
10014
|
+
if (!(facts && facts.pending)) patch({ notice: { key: 'updateRestartHint', kind: 'warn' } })
|
|
9957
10015
|
return true
|
|
9958
10016
|
}
|
|
9959
10017
|
const facts = readSnapshot(res)
|
|
10018
|
+
const code = codeOf(res)
|
|
9960
10019
|
patch({ blocked: facts.blocked || state.blocked, failure: 'install-failed' })
|
|
10020
|
+
record('warn', 'update.install.fail', { code: code, errorHash: hash8(code), pluginId: PLUGIN_ID })
|
|
10021
|
+
patch({ notice: { key: facts.blocked ? blockedReasonKey(facts.blocked) : 'updateFailInstall', kind: 'error' } })
|
|
9961
10022
|
await readStatus()
|
|
9962
10023
|
return false
|
|
9963
10024
|
} catch (e) {
|
|
9964
10025
|
patch({ installing: false, failure: 'install-failed' })
|
|
9965
10026
|
transportFail(phones.updateInstall, 'throw', e)
|
|
10027
|
+
record('warn', 'update.install.fail', {
|
|
10028
|
+
code: 'transport', errorHash: hash8(String((e && e.message) || e)), pluginId: PLUGIN_ID,
|
|
10029
|
+
})
|
|
10030
|
+
patch({ notice: { key: 'updateFailInstall', kind: 'error' } })
|
|
9966
10031
|
try { await readStatus() } catch (e2) { /* 忽略 */ }
|
|
9967
10032
|
return false
|
|
9968
10033
|
}
|
|
@@ -9984,6 +10049,8 @@ function createUpdateController(deps) {
|
|
|
9984
10049
|
install: install,
|
|
9985
10050
|
openDialog: function () { patch({ dialogOpen: true }) },
|
|
9986
10051
|
closeDialog: function () { patch({ dialogOpen: false, failure: null }) },
|
|
10052
|
+
/** 面板其它部件(如日志开关)也能借这行可见提示,别让它们的失败静默掉。 */
|
|
10053
|
+
setNotice: function (key, kind) { patch({ notice: key ? { key: key, kind: kind || 'error' } : null }) },
|
|
9987
10054
|
dispose: function () { stopPolling(); listeners.length = 0 },
|
|
9988
10055
|
}
|
|
9989
10056
|
}
|
|
@@ -10023,7 +10090,7 @@ const STORAGE_KEY = 'dsh.opencode-palette.v2'
|
|
|
10023
10090
|
const LEGACY_STORAGE_KEY = 'dsh.opencode-tui-theme.v2'
|
|
10024
10091
|
const DEFAULT_STATE = { enabled: true, theme: 'opencode', mode: 'mono', size: 13, fontKey: 'JetBrains Mono' }
|
|
10025
10092
|
// 构建时由 scripts/build-client.mjs 替换为 package.json 版本(面板底部署小字)
|
|
10026
|
-
const PALETTE_VERSION = '2.0.
|
|
10093
|
+
const PALETTE_VERSION = '2.0.7'
|
|
10027
10094
|
|
|
10028
10095
|
function getReact() {
|
|
10029
10096
|
if (typeof require === 'function') { try { return require('react') } catch (e) { /* 动态版无 require */ } }
|
|
@@ -10167,6 +10234,14 @@ const I18N = {
|
|
|
10167
10234
|
'blocked.incompatible-node': { zh: '新版要求的 Node 更高,先升级 Node', en: 'The new version needs a newer Node — upgrade it first' },
|
|
10168
10235
|
'blocked.recovery-required': { zh: '上次安装被打断,重新点一次安装', en: 'The last install was interrupted — retry the install' },
|
|
10169
10236
|
'blocked.unknown': { zh: '当前装不了:重开 DSH 再查一次', en: 'Cannot update right now — reopen DSH and try again' },
|
|
10237
|
+
// 升级已写入但没进「待重启」时的兜底提示(正常路径由常驻横幅承接)
|
|
10238
|
+
updateRestartHint: { zh: '升级已写入,重启 DSH 后生效', en: 'Update written — restart DSH to apply' },
|
|
10239
|
+
// ── 日志开关(面板头行的小开关;开的是 dsh-log 的落盘开关,排障用)──
|
|
10240
|
+
logSwitch: { zh: '日志', en: 'Log' },
|
|
10241
|
+
logOn: { zh: '开', en: 'On' },
|
|
10242
|
+
logOff: { zh: '关', en: 'Off' },
|
|
10243
|
+
logSwitchHint: { zh: '打开后把关键节点写进 <DSH_HOME>/logs/dsh-opencode-palette/(排障用;失败级别的日志恒记,不受此开关控制)', en: 'Write key nodes to <DSH_HOME>/logs/dsh-opencode-palette/ (for troubleshooting; warn/error lines are always kept)' },
|
|
10244
|
+
logSwitchFail: { zh: '日志开关没能写入宿主,请重开面板再试', en: 'The host did not accept the log switch — reopen the panel and retry' },
|
|
10170
10245
|
}
|
|
10171
10246
|
|
|
10172
10247
|
// 语言检测(回退):html[lang] 优先,回退浏览器语言
|
|
@@ -10250,8 +10325,10 @@ function createClient(slotTarget) {
|
|
|
10250
10325
|
pollMs: CLIENT_POLL.defaultMs,
|
|
10251
10326
|
log: function (level, event, fields) { try { clientLog.log(level, event, fields) } catch (e) { /* 忽略 */ } },
|
|
10252
10327
|
})
|
|
10253
|
-
//
|
|
10254
|
-
|
|
10328
|
+
// 启动时向宿主对账调试开关(以宿主为准);宿主不可用时静默,不抛错。
|
|
10329
|
+
// 面板的日志开关复用这一次对账的结果,不重复打扰宿主(对账失败会记一条 watchdog warn)。
|
|
10330
|
+
let logSwitchReady = null
|
|
10331
|
+
try { logSwitchReady = clientLog.reconcileLogSwitch() } catch (e) { /* 忽略 */ }
|
|
10255
10332
|
try { clientLog.log('info', 'host.call', { method: 'boot', latencyMs: 0, ok: true, kind: 'boot', pluginId: PLUGIN_ID }) } catch (e) { /* 忽略 */ }
|
|
10256
10333
|
|
|
10257
10334
|
// ── i18n 运行时:语言跟随 DSH(官方 locale 服务为信号源,html[lang] 仅作回退)──
|
|
@@ -10560,6 +10637,36 @@ function createClient(slotTarget) {
|
|
|
10560
10637
|
}, tr('fontRetry')) : null,
|
|
10561
10638
|
])
|
|
10562
10639
|
: null
|
|
10640
|
+
// 分组标题:非点击的分区线(替代旧“本机字体 · 90”文本分隔)。
|
|
10641
|
+
// 文本用主题色 var(--dsw-alias-brand-primary)(随当前 opencode 主题走 primary),
|
|
10642
|
+
// 数量做成描边徽章 + 中间细分割线,不再用“·”拼接字符串。
|
|
10643
|
+
const secHeader = function (key, label, count) {
|
|
10644
|
+
const badge = (count === null || count === undefined) ? null : h('span', {
|
|
10645
|
+
key: 'n',
|
|
10646
|
+
style: {
|
|
10647
|
+
fontSize: 11, lineHeight: '16px', padding: '0 7px', borderRadius: 999,
|
|
10648
|
+
border: '1px solid var(--dsw-alias-brand-primary)',
|
|
10649
|
+
color: 'var(--dsw-alias-brand-primary)', background: 'transparent', flex: 'none',
|
|
10650
|
+
},
|
|
10651
|
+
}, String(count))
|
|
10652
|
+
return h('div', {
|
|
10653
|
+
key: key,
|
|
10654
|
+
style: {
|
|
10655
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
10656
|
+
padding: '10px 10px 4px', cursor: 'default', userSelect: 'none',
|
|
10657
|
+
},
|
|
10658
|
+
}, [
|
|
10659
|
+
h('span', {
|
|
10660
|
+
key: 't',
|
|
10661
|
+
style: {
|
|
10662
|
+
fontSize: 11, fontWeight: 600, letterSpacing: '.08em', whiteSpace: 'nowrap',
|
|
10663
|
+
color: 'var(--dsw-alias-brand-primary)',
|
|
10664
|
+
},
|
|
10665
|
+
}, label),
|
|
10666
|
+
h('span', { key: 'l', style: { flex: '1 1 auto', height: 1, background: 'var(--dsw-alias-border-l1)', borderRadius: 1 } }),
|
|
10667
|
+
badge,
|
|
10668
|
+
])
|
|
10669
|
+
}
|
|
10563
10670
|
const menuItem = function (key, label, onClick) {
|
|
10564
10671
|
return h('div', {
|
|
10565
10672
|
key: key,
|
|
@@ -10576,11 +10683,11 @@ function createClient(slotTarget) {
|
|
|
10576
10683
|
const locals = all.slice(0, 400)
|
|
10577
10684
|
const rows = []
|
|
10578
10685
|
if (presets.length > 0) {
|
|
10579
|
-
rows.push(
|
|
10686
|
+
rows.push(secHeader('sec-presets', tr('fontPresets'), presets.length))
|
|
10580
10687
|
for (const k of presets) rows.push(fontItem(k))
|
|
10581
10688
|
}
|
|
10582
10689
|
if (locals.length > 0) {
|
|
10583
|
-
rows.push(
|
|
10690
|
+
rows.push(secHeader('sec-locals', tr('fontLocals'), all.length))
|
|
10584
10691
|
for (const k of locals) rows.push(fontItem(k))
|
|
10585
10692
|
}
|
|
10586
10693
|
if (rows.length === 0) rows.push(menuItem('no-match', tr('fontNoMatch'), function () {}))
|
|
@@ -10826,32 +10933,78 @@ function createClient(slotTarget) {
|
|
|
10826
10933
|
]))
|
|
10827
10934
|
: null
|
|
10828
10935
|
|
|
10936
|
+
// ── 日志开关(排障用的小开关)──
|
|
10937
|
+
// 状态以宿主为准;首帧先按本地缓存渲染,对账回来再校正。开关本体归 dsh-log 管,
|
|
10938
|
+
// 这里只调它的 setLogSwitch,不自己写开关文件(否则会成第二份真源)。
|
|
10939
|
+
const [logOn, setLogOn] = react.useState(!!(clientLog && clientLog.logSwitch && clientLog.logSwitch.enabled))
|
|
10940
|
+
react.useEffect(function () {
|
|
10941
|
+
let alive = true
|
|
10942
|
+
Promise.resolve(props.reconcileLog()).then(function (res) {
|
|
10943
|
+
if (!alive || !res) return
|
|
10944
|
+
if (typeof res.enabled === 'boolean') setLogOn(res.enabled)
|
|
10945
|
+
}, function () { /* 读不到就维持本地缓存值 */ })
|
|
10946
|
+
return function () { alive = false }
|
|
10947
|
+
}, [])
|
|
10948
|
+
const flipLog = function () {
|
|
10949
|
+
const next = !logOn
|
|
10950
|
+
setLogOn(next)
|
|
10951
|
+
Promise.resolve(props.log.setLogSwitch(next)).then(function (res) {
|
|
10952
|
+
const ok = !!(res && res.ok)
|
|
10953
|
+
if (res && typeof res.enabled === 'boolean') setLogOn(res.enabled)
|
|
10954
|
+
else if (!ok) setLogOn(!next)
|
|
10955
|
+
try {
|
|
10956
|
+
props.log.log('info', 'log.switch.set', {
|
|
10957
|
+
enabled: ok ? !!next : logOn,
|
|
10958
|
+
ok: ok,
|
|
10959
|
+
reason: ok ? 'ok' : String((res && res.error) || 'unknown').slice(0, 40),
|
|
10960
|
+
pluginId: PLUGIN_ID,
|
|
10961
|
+
})
|
|
10962
|
+
} catch (e) { /* 忽略 */ }
|
|
10963
|
+
if (!ok) props.update.setNotice('logSwitchFail')
|
|
10964
|
+
}, function () { setLogOn(!next) })
|
|
10965
|
+
}
|
|
10966
|
+
|
|
10829
10967
|
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 920 } }, [
|
|
10830
10968
|
restartBanner,
|
|
10831
10969
|
// 头行:标题 + 状态开关(一个状态一个控制)
|
|
10832
10970
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, [
|
|
10833
10971
|
h('strong', null, '🎨 ' + tr('panelName')),
|
|
10834
10972
|
h('div', { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } }, [
|
|
10973
|
+
h('span', {
|
|
10974
|
+
key: 'log',
|
|
10975
|
+
onClick: flipLog,
|
|
10976
|
+
title: tr('logSwitchHint'),
|
|
10977
|
+
style: {
|
|
10978
|
+
fontSize: 11, cursor: 'pointer', userSelect: 'none',
|
|
10979
|
+
color: logOn ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-tertiary)',
|
|
10980
|
+
},
|
|
10981
|
+
}, tr('logSwitch') + ' ' + (logOn ? tr('logOn') : tr('logOff'))),
|
|
10835
10982
|
h('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)' } }, 'v' + PALETTE_VERSION),
|
|
10836
10983
|
updateButton,
|
|
10837
10984
|
h('a', {
|
|
10838
10985
|
href: 'https://github.com/FeatherHunter/dsh-opencode-palette',
|
|
10839
10986
|
target: '_blank', rel: 'noopener noreferrer',
|
|
10840
10987
|
title: '你的 ⭐是我夜空中最亮的星 🌹',
|
|
10841
|
-
style: {
|
|
10842
|
-
}, '
|
|
10843
|
-
|
|
10988
|
+
style: { display: 'inline-flex', cursor: 'pointer', lineHeight: 1, textDecoration: 'none' },
|
|
10989
|
+
}, h('svg', {
|
|
10990
|
+
width: 15, height: 15, viewBox: '0 0 24 24',
|
|
10991
|
+
style: { display: 'block' },
|
|
10992
|
+
}, h('path', {
|
|
10993
|
+
d: 'M12 2.5l2.92 6.14 6.58.6-4.93 4.4 1.42 6.46L12 16.77l-5.99 3.33 1.42-6.46-4.93-4.4 6.58-.6L12 2.5z',
|
|
10994
|
+
fill: '#FFC53D', stroke: '#B45309', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
10995
|
+
}))),
|
|
10996
|
+
// ISSUE 入口:彩色消息气泡(信息图标认不出「提需求」,气泡才读得出是反馈;浅底深底都可见的定值配色)
|
|
10844
10997
|
h('a', {
|
|
10845
10998
|
href: 'https://github.com/FeatherHunter/dsh-opencode-palette/issues',
|
|
10846
10999
|
target: '_blank', rel: 'noopener noreferrer',
|
|
10847
11000
|
title: '任何功能需求、故障、建议、意见都可以提ISSUE',
|
|
10848
|
-
style: {
|
|
11001
|
+
style: { display: 'inline-flex', cursor: 'pointer' },
|
|
10849
11002
|
}, h('svg', {
|
|
10850
|
-
width: 14, height: 14, viewBox: '0 0 24 24',
|
|
10851
|
-
stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
11003
|
+
width: 14, height: 14, viewBox: '0 0 24 24',
|
|
10852
11004
|
style: { display: 'block' },
|
|
10853
11005
|
}, h('path', {
|
|
10854
11006
|
d: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z',
|
|
11007
|
+
fill: '#7DD3FC', stroke: '#0369A1', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
10855
11008
|
}))),
|
|
10856
11009
|
h('span', { style: { color: st.enabled ? 'var(--dsw-alias-state-success-primary)' : muted, fontSize: 12 } },
|
|
10857
11010
|
st.enabled ? tr('enabled') : tr('disabled')),
|
|
@@ -10876,6 +11029,18 @@ function createClient(slotTarget) {
|
|
|
10876
11029
|
]),
|
|
10877
11030
|
h('div', { style: { fontSize: 12, color: muted } },
|
|
10878
11031
|
tr('subtitle')),
|
|
11032
|
+
// 弹窗之外的可见结果:检查/升级失败、以及「装好了但没进待重启」都在这里落地,
|
|
11033
|
+
// 否则用户点一下没动静,分不清是成功、失败还是没反应。
|
|
11034
|
+
(updState && updState.notice)
|
|
11035
|
+
? h('div', {
|
|
11036
|
+
style: {
|
|
11037
|
+
fontSize: 12,
|
|
11038
|
+
color: updState.notice.kind === 'warn'
|
|
11039
|
+
? 'var(--dsw-alias-state-warn-primary)'
|
|
11040
|
+
: 'var(--dsw-alias-state-error-primary)',
|
|
11041
|
+
},
|
|
11042
|
+
}, tr(updState.notice.key))
|
|
11043
|
+
: null,
|
|
10879
11044
|
// ── 排印调节(置顶)──
|
|
10880
11045
|
h('div', { style: secTitle }, [
|
|
10881
11046
|
h('span', null, tr('typography')),
|
|
@@ -10887,7 +11052,7 @@ function createClient(slotTarget) {
|
|
|
10887
11052
|
], function (v) { props.refresh(v, st.size, st.fontKey); setUi(props.getState()) }),
|
|
10888
11053
|
dd(sizeOpen, setSizeOpen, sizeRef,
|
|
10889
11054
|
h('span', null, tr('fontSize') + ' ' + st.size + 'px'),
|
|
10890
|
-
[11, 12, 13, 14, 15, 16, 17, 18].map(function (s) {
|
|
11055
|
+
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24].map(function (s) {
|
|
10891
11056
|
const on = s === st.size
|
|
10892
11057
|
return h('div', {
|
|
10893
11058
|
key: String(s),
|
|
@@ -10945,6 +11110,11 @@ function createClient(slotTarget) {
|
|
|
10945
11110
|
// 检查更新:控制器 + 日志器交给面板(宿主不可用时 update.available 为假,按钮不渲染)
|
|
10946
11111
|
update: update,
|
|
10947
11112
|
log: clientLog,
|
|
11113
|
+
// 日志开关的初始状态:复用启动时那次对账,别为显示一个开关再打扰一次宿主
|
|
11114
|
+
reconcileLog: function () {
|
|
11115
|
+
if (logSwitchReady && typeof logSwitchReady.then === 'function') return logSwitchReady
|
|
11116
|
+
try { return clientLog.reconcileLogSwitch() } catch (e) { return Promise.resolve(null) }
|
|
11117
|
+
},
|
|
10948
11118
|
// 本机字体清单:面板打开下拉时调(要用户手势),未打开下拉不读;失败恒回退预设
|
|
10949
11119
|
collectFonts: collectFontCandidates,
|
|
10950
11120
|
// 面板文案(测试/调试用,走同一份双语表)
|
package/lib/event-list.json
CHANGED
|
@@ -1,42 +1,87 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"pluginId": "dsh-opencode-palette",
|
|
4
|
-
"counts": { "resident":
|
|
5
|
-
"events": {
|
|
6
|
-
"host.call": {
|
|
7
|
-
"level": "info",
|
|
8
|
-
"kind": "resident",
|
|
9
|
-
"fields": ["method", "latencyMs", "ok", "kind", "pluginId"]
|
|
10
|
-
},
|
|
11
|
-
"host.call.fail": {
|
|
12
|
-
"level": "warn",
|
|
13
|
-
"kind": "resident",
|
|
14
|
-
"fields": ["method", "kind", "errorHash", "pluginId"]
|
|
15
|
-
},
|
|
16
|
-
"update.install.exec": {
|
|
17
|
-
"level": "info",
|
|
18
|
-
"kind": "resident",
|
|
19
|
-
"fields": ["route", "ok", "exitCode", "durationMs", "pluginId"]
|
|
20
|
-
},
|
|
21
|
-
"
|
|
22
|
-
"level": "
|
|
23
|
-
"kind": "resident",
|
|
24
|
-
"fields": ["
|
|
25
|
-
},
|
|
26
|
-
"
|
|
27
|
-
"level": "
|
|
28
|
-
"kind": "resident",
|
|
29
|
-
"fields": ["
|
|
30
|
-
},
|
|
31
|
-
"
|
|
32
|
-
"level": "warn",
|
|
33
|
-
"kind": "resident",
|
|
34
|
-
"fields": ["
|
|
35
|
-
},
|
|
36
|
-
"
|
|
37
|
-
"level": "
|
|
38
|
-
"kind": "resident",
|
|
39
|
-
"fields": ["
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"pluginId": "dsh-opencode-palette",
|
|
4
|
+
"counts": { "resident": 16, "ondemand": 0, "selfmon": 0 },
|
|
5
|
+
"events": {
|
|
6
|
+
"host.call": {
|
|
7
|
+
"level": "info",
|
|
8
|
+
"kind": "resident",
|
|
9
|
+
"fields": ["method", "latencyMs", "ok", "kind", "pluginId"]
|
|
10
|
+
},
|
|
11
|
+
"host.call.fail": {
|
|
12
|
+
"level": "warn",
|
|
13
|
+
"kind": "resident",
|
|
14
|
+
"fields": ["method", "kind", "errorHash", "pluginId"]
|
|
15
|
+
},
|
|
16
|
+
"update.install.exec": {
|
|
17
|
+
"level": "info",
|
|
18
|
+
"kind": "resident",
|
|
19
|
+
"fields": ["route", "ok", "exitCode", "durationMs", "pluginId"]
|
|
20
|
+
},
|
|
21
|
+
"update.check.start": {
|
|
22
|
+
"level": "info",
|
|
23
|
+
"kind": "resident",
|
|
24
|
+
"fields": ["trigger", "pluginId"]
|
|
25
|
+
},
|
|
26
|
+
"update.check.ok": {
|
|
27
|
+
"level": "info",
|
|
28
|
+
"kind": "resident",
|
|
29
|
+
"fields": ["trigger", "hasNew", "latest", "latencyMs", "pluginId"]
|
|
30
|
+
},
|
|
31
|
+
"update.check.fail": {
|
|
32
|
+
"level": "warn",
|
|
33
|
+
"kind": "resident",
|
|
34
|
+
"fields": ["trigger", "code", "errorHash", "pluginId"]
|
|
35
|
+
},
|
|
36
|
+
"update.check.retry": {
|
|
37
|
+
"level": "warn",
|
|
38
|
+
"kind": "resident",
|
|
39
|
+
"fields": ["attempt", "ok", "reason", "pluginId"]
|
|
40
|
+
},
|
|
41
|
+
"update.check.probe": {
|
|
42
|
+
"level": "warn",
|
|
43
|
+
"kind": "resident",
|
|
44
|
+
"fields": ["stage", "httpStatus", "bytes", "reason", "detail", "pluginId"]
|
|
45
|
+
},
|
|
46
|
+
"update.install.start": {
|
|
47
|
+
"level": "info",
|
|
48
|
+
"kind": "resident",
|
|
49
|
+
"fields": ["latest", "pluginId"]
|
|
50
|
+
},
|
|
51
|
+
"update.install.ok": {
|
|
52
|
+
"level": "info",
|
|
53
|
+
"kind": "resident",
|
|
54
|
+
"fields": ["latest", "pending", "pluginId"]
|
|
55
|
+
},
|
|
56
|
+
"update.install.fail": {
|
|
57
|
+
"level": "warn",
|
|
58
|
+
"kind": "resident",
|
|
59
|
+
"fields": ["code", "errorHash", "pluginId"]
|
|
60
|
+
},
|
|
61
|
+
"log.switch.set": {
|
|
62
|
+
"level": "info",
|
|
63
|
+
"kind": "resident",
|
|
64
|
+
"fields": ["enabled", "ok", "reason", "pluginId"]
|
|
65
|
+
},
|
|
66
|
+
"log.forward.summary": {
|
|
67
|
+
"level": "warn",
|
|
68
|
+
"kind": "resident",
|
|
69
|
+
"fields": ["droppedDelta", "totalDropped", "reason", "windowMs"]
|
|
70
|
+
},
|
|
71
|
+
"log.switch.watchdog": {
|
|
72
|
+
"level": "warn",
|
|
73
|
+
"kind": "resident",
|
|
74
|
+
"fields": ["op", "timeoutMs", "stage"]
|
|
75
|
+
},
|
|
76
|
+
"log.export.fail": {
|
|
77
|
+
"level": "warn",
|
|
78
|
+
"kind": "resident",
|
|
79
|
+
"fields": ["op", "reason", "errorHash"]
|
|
80
|
+
},
|
|
81
|
+
"host.channel.fail": {
|
|
82
|
+
"level": "error",
|
|
83
|
+
"kind": "resident",
|
|
84
|
+
"fields": ["stage", "path", "reason"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { dirname, join } from 'node:path'
|
|
|
26
26
|
import { fileURLToPath } from 'node:url'
|
|
27
27
|
import { createHostLog, registerHostLogPhones } from 'dsh-log/host'
|
|
28
28
|
import { createHostUpdate } from './vendor/dsh-plugin-update/host.js'
|
|
29
|
+
import { buildPhoneNames, DEFAULT_REGISTRY } from './vendor/dsh-plugin-update/config.js'
|
|
29
30
|
import { PLUGIN_ID, PHONE_PREFIX, ROUTE_PATH, TARGET_PACKAGE_NAME } from './channel.mjs'
|
|
30
31
|
|
|
31
32
|
export const name = PLUGIN_ID
|
|
@@ -111,6 +112,189 @@ export function createLogFileService(readTextImpl, writeTextImpl, mkdirImpl, unl
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// ── 查更新的自检探针与重试(2026-09-30 加;起因:面板点「检查更新」静默无反应,日志只剩一个散列)──
|
|
116
|
+
// 更新包把 fetch→校验链路里每一步失败都归成同一个 invalid-release(service.js 的兜底 catch),
|
|
117
|
+
// 线上因此只剩 8 位散列,分不清是「响应被截断」还是「tarball 指向了别的源」。
|
|
118
|
+
// 探针按同一条链路再走一遍,但一步一记、绝不抛错:把现象写进日志,而不是只给判词。
|
|
119
|
+
// 重试一次:截断、抖动这类瞬时故障本会自愈,用户不该为它再看一次失败。
|
|
120
|
+
/** 探针阈值:必须与更新包 service.js 里的同名常量一致(tests/update-panel.test.mjs 有门禁比对)。 */
|
|
121
|
+
export const PROBE_LIMITS = {
|
|
122
|
+
maxBytes: 256 * 1024,
|
|
123
|
+
integrityPattern: '^sha512-[A-Za-z0-9+/]{86}==$',
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 取一小段文本进日志:够定位就行,别把整份响应写进去。 */
|
|
127
|
+
function probeBrief(value, limit) {
|
|
128
|
+
const text = value === null || value === undefined ? '' : String(value)
|
|
129
|
+
return text.length > limit ? text.slice(0, limit) : text
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 按更新包的同一条链路重走一遍发布清单,把观察到的现象原样回给调用方(不抛错)。
|
|
134
|
+
* @returns {Promise<{stage: string, httpStatus: number|null, bytes: number, reason: string, detail: string}>}
|
|
135
|
+
*/
|
|
136
|
+
export async function probeRelease(fetchImpl, options) {
|
|
137
|
+
const opts = options || {}
|
|
138
|
+
const targetName = String(opts.targetPackageName || TARGET_PACKAGE_NAME)
|
|
139
|
+
const registry = String(opts.registryUrl || DEFAULT_REGISTRY)
|
|
140
|
+
const timeoutMs = typeof opts.timeoutMs === 'number' && opts.timeoutMs > 0 ? opts.timeoutMs : 10000
|
|
141
|
+
const limits = opts.limits || PROBE_LIMITS
|
|
142
|
+
const facts = { stage: 'fetch', httpStatus: null, bytes: 0, reason: 'fetch-threw', detail: '' }
|
|
143
|
+
|
|
144
|
+
let response = null
|
|
145
|
+
try {
|
|
146
|
+
const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
147
|
+
? AbortSignal.timeout(timeoutMs)
|
|
148
|
+
: undefined
|
|
149
|
+
response = await fetchImpl(`${registry}${encodeURIComponent(targetName)}/latest`, {
|
|
150
|
+
headers: { accept: 'application/json' },
|
|
151
|
+
redirect: 'error',
|
|
152
|
+
signal: signal,
|
|
153
|
+
})
|
|
154
|
+
} catch (e) {
|
|
155
|
+
facts.detail = probeBrief((e && e.message) || e, 120)
|
|
156
|
+
return facts
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
facts.stage = 'status'
|
|
160
|
+
facts.httpStatus = response && typeof response.status === 'number' ? response.status : null
|
|
161
|
+
if (!response || response.ok !== true) {
|
|
162
|
+
facts.reason = 'not-ok'
|
|
163
|
+
return facts
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
facts.stage = 'body'
|
|
167
|
+
let text = ''
|
|
168
|
+
try {
|
|
169
|
+
text = await response.text()
|
|
170
|
+
} catch (e) {
|
|
171
|
+
facts.reason = 'body-threw'
|
|
172
|
+
facts.detail = probeBrief((e && e.message) || e, 120)
|
|
173
|
+
return facts
|
|
174
|
+
}
|
|
175
|
+
facts.bytes = Buffer.byteLength(text, 'utf8')
|
|
176
|
+
if (facts.bytes > limits.maxBytes) {
|
|
177
|
+
facts.reason = 'body-too-large'
|
|
178
|
+
return facts
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
facts.stage = 'json'
|
|
182
|
+
let value = null
|
|
183
|
+
try {
|
|
184
|
+
value = JSON.parse(text)
|
|
185
|
+
} catch (e) {
|
|
186
|
+
facts.reason = 'json-parse'
|
|
187
|
+
facts.detail = probeBrief(text.slice(0, 60), 60)
|
|
188
|
+
return facts
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
facts.stage = 'name'
|
|
192
|
+
if (value.name !== targetName) {
|
|
193
|
+
facts.reason = 'name-mismatch'
|
|
194
|
+
facts.detail = probeBrief(value.name, 60)
|
|
195
|
+
return facts
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
facts.stage = 'version'
|
|
199
|
+
if (!(typeof value.version === 'string' && /^\d+\.\d+\.\d+$/.test(value.version))) {
|
|
200
|
+
facts.reason = 'version-invalid'
|
|
201
|
+
facts.detail = probeBrief(value.version, 40)
|
|
202
|
+
return facts
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
facts.stage = 'engines'
|
|
206
|
+
if (value.engines && value.engines.node !== undefined && typeof value.engines.node !== 'string') {
|
|
207
|
+
facts.reason = 'engines-node-type'
|
|
208
|
+
return facts
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
facts.stage = 'tarball'
|
|
212
|
+
let tarball = null
|
|
213
|
+
try {
|
|
214
|
+
tarball = new URL(String(value.dist && value.dist.tarball))
|
|
215
|
+
} catch (e) {
|
|
216
|
+
facts.reason = 'tarball-url'
|
|
217
|
+
facts.detail = probeBrief(value.dist && value.dist.tarball, 80)
|
|
218
|
+
return facts
|
|
219
|
+
}
|
|
220
|
+
const wantPath = `/${targetName}/-/${targetName}-${value.version}.tgz`
|
|
221
|
+
if (tarball.origin !== new URL(registry).origin) {
|
|
222
|
+
facts.reason = 'tarball-origin'
|
|
223
|
+
facts.detail = probeBrief(tarball.origin, 80)
|
|
224
|
+
return facts
|
|
225
|
+
}
|
|
226
|
+
if (tarball.pathname !== wantPath) {
|
|
227
|
+
facts.reason = 'tarball-path'
|
|
228
|
+
facts.detail = probeBrief(tarball.pathname, 80)
|
|
229
|
+
return facts
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
facts.stage = 'integrity'
|
|
233
|
+
const integrity = value.dist && value.dist.integrity
|
|
234
|
+
if (typeof integrity !== 'string') {
|
|
235
|
+
facts.reason = 'integrity-missing'
|
|
236
|
+
return facts
|
|
237
|
+
}
|
|
238
|
+
if (!new RegExp(limits.integrityPattern).test(integrity)) {
|
|
239
|
+
facts.reason = 'integrity-shape'
|
|
240
|
+
facts.detail = probeBrief(integrity, 40)
|
|
241
|
+
return facts
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
facts.stage = 'ok'
|
|
245
|
+
facts.reason = 'valid'
|
|
246
|
+
return facts
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 从更新包的回包形状里取错误码(没有就退成 unknown)。
|
|
250
|
+
* 更新包的失败形状是 {ok:false, error:'<code>', errorKind}——error 是字符串,不是对象。 */
|
|
251
|
+
function errorCodeOf(res) {
|
|
252
|
+
const error = res && res.error !== undefined && res.error !== null ? res.error : null
|
|
253
|
+
if (typeof error === 'string') return error.slice(0, 40)
|
|
254
|
+
const code = error && (error.code || error.message) ? String(error.code || error.message) : 'unknown'
|
|
255
|
+
return code.slice(0, 40)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 给「查更新」这条电话包一层:失败 → 重试一次 → 仍失败则跑探针并落日志。
|
|
260
|
+
* 只包这一条:查状态与装更新各自有明确的失败语义,重试它们只会拖长用户的等待。
|
|
261
|
+
*/
|
|
262
|
+
export function wrapUpdateCheck(handler, deps) {
|
|
263
|
+
const input = deps || {}
|
|
264
|
+
const log = typeof input.log === 'function' ? input.log : function () {}
|
|
265
|
+
const probe = typeof input.probe === 'function' ? input.probe : null
|
|
266
|
+
const pluginId = input.pluginId || PLUGIN_ID
|
|
267
|
+
return async function (args) {
|
|
268
|
+
const first = await handler(args)
|
|
269
|
+
if (first && first.ok === true) return first
|
|
270
|
+
const reason = errorCodeOf(first)
|
|
271
|
+
log('warn', 'update.check.retry', { attempt: 1, ok: false, reason: reason, pluginId: pluginId })
|
|
272
|
+
let second = null
|
|
273
|
+
try {
|
|
274
|
+
second = await handler(args)
|
|
275
|
+
} catch (e) {
|
|
276
|
+
second = null
|
|
277
|
+
}
|
|
278
|
+
const recovered = !!(second && second.ok === true)
|
|
279
|
+
log('warn', 'update.check.retry', { attempt: 2, ok: recovered, reason: reason, pluginId: pluginId })
|
|
280
|
+
if (recovered) return second
|
|
281
|
+
if (probe !== null) {
|
|
282
|
+
try {
|
|
283
|
+
const facts = await probe()
|
|
284
|
+
log('warn', 'update.check.probe', {
|
|
285
|
+
stage: facts.stage,
|
|
286
|
+
httpStatus: facts.httpStatus,
|
|
287
|
+
bytes: facts.bytes,
|
|
288
|
+
reason: facts.reason,
|
|
289
|
+
detail: facts.detail,
|
|
290
|
+
pluginId: pluginId,
|
|
291
|
+
})
|
|
292
|
+
} catch (e) { /* 探针自己失败不该改变回给面板的结果 */ }
|
|
293
|
+
}
|
|
294
|
+
return second || first
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
114
298
|
/** 宿主半装配入口:建日志库 → 建更新电话 → 合成电话表 → 注册通道。 */
|
|
115
299
|
export function apply(ctx) {
|
|
116
300
|
const homeDir = dshHomeDir(process.env, homedir())
|
|
@@ -144,6 +328,20 @@ export function apply(ctx) {
|
|
|
144
328
|
)
|
|
145
329
|
for (const phoneName of Object.keys(update.handlers)) registry.set(phoneName, update.handlers[phoneName])
|
|
146
330
|
|
|
331
|
+
// 查更新外面再包一层:失败重试一次,仍失败就跑探针把真实原因落日志。
|
|
332
|
+
// 电话名从更新包派生(本文件不写字面量),装更新与查状态两条保持原样。
|
|
333
|
+
const updateCheckPhone = buildPhoneNames(PHONE_PREFIX).updateCheck
|
|
334
|
+
const rawUpdateCheck = registry.get(updateCheckPhone)
|
|
335
|
+
if (typeof rawUpdateCheck === 'function') {
|
|
336
|
+
registry.set(updateCheckPhone, wrapUpdateCheck(rawUpdateCheck, {
|
|
337
|
+
log: function (level, event, fields) {
|
|
338
|
+
try { hostLog.store.log(level, event, fields) } catch (e) { /* 忽略 */ }
|
|
339
|
+
},
|
|
340
|
+
probe: function () { return probeRelease(globalThis.fetch, { targetPackageName: TARGET_PACKAGE_NAME }) },
|
|
341
|
+
pluginId: PLUGIN_ID,
|
|
342
|
+
}))
|
|
343
|
+
}
|
|
344
|
+
|
|
147
345
|
const fail = function (stage, reason) {
|
|
148
346
|
try { hostLog.store.log('error', 'host.channel.fail', { stage: stage, path: ROUTE_PATH, reason: String(reason).slice(0, 120) }) } catch (e) { /* 忽略 */ }
|
|
149
347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-opencode-palette",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"description": "让 DeepSeek Harness 穿上 38 款经典皮肤——东京的霓虹夜色、德古拉的暗红月光、复古工坊的暖黄灯火、黑客帝国的数字雨、玫瑰松林间的风……一键换肤,即点即换,重启不丢。38 legendary skins for DeepSeek Harness — tokyonight's neon dusk, dracula's crimson moon, gruvbox's retro glow, the matrix's digital rain, rose-pine's rosewood calm… one click, instant, persisted.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"dsh-log": "0.2.1"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
|
-
"dsh": ">=0.
|
|
39
|
+
"dsh": ">=0.2.0-rc.1"
|
|
40
40
|
},
|
|
41
41
|
"dsh": {
|
|
42
42
|
"bundle": {
|