dsh-opencode-palette 2.0.6 → 2.0.8
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/lib/client.js +138 -12
- package/lib/event-list.json +87 -42
- package/lib/index.js +198 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-opencode-palette v2.0.
|
|
2
|
+
* dsh-opencode-palette v2.0.8 — 浏览器半(构建产物,勿手改)
|
|
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 的客户端入口
|
|
@@ -9724,6 +9724,9 @@ __mods["channel"] = { CHANNEL, ENDPOINT, ROUTE_PATH, PLUGIN_ID, PHONE_PREFIX, TA
|
|
|
9724
9724
|
* 电话名与轮询间隔由调用方从更新包的客户端入口派生后传进来(本文件不写字面量)。
|
|
9725
9725
|
*/
|
|
9726
9726
|
|
|
9727
|
+
/** 与 runtime/channel.mjs 的 PLUGIN_ID 同值:本文件是纯逻辑模块,不引运行时模块,故就地声明一次。 */
|
|
9728
|
+
const PLUGIN_ID = 'dsh-opencode-palette'
|
|
9729
|
+
|
|
9727
9730
|
/** 宿主回包 → 面板要的几个事实(纯函数,便于离线核对)。 */
|
|
9728
9731
|
function readSnapshot(res) {
|
|
9729
9732
|
const snap = res && res.snapshot ? res.snapshot : null
|
|
@@ -9805,6 +9808,9 @@ function createUpdateController(deps) {
|
|
|
9805
9808
|
jobMessage: null,
|
|
9806
9809
|
pending: false,
|
|
9807
9810
|
failure: null,
|
|
9811
|
+
// 弹窗外的可见结果({key, kind} 或 null):失败与「装好了但没进待重启」都走这里,
|
|
9812
|
+
// 否则用户点一下没动静,看不出是成功、失败还是没反应(2026-09-30 真机反馈)。
|
|
9813
|
+
notice: null,
|
|
9808
9814
|
}
|
|
9809
9815
|
let pollTimer = null
|
|
9810
9816
|
const listeners = []
|
|
@@ -9821,6 +9827,14 @@ function createUpdateController(deps) {
|
|
|
9821
9827
|
function record(level, event, fields, latencyMs) {
|
|
9822
9828
|
try { log(level, event, fields) } catch (e) { /* 忽略 */ }
|
|
9823
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
|
+
}
|
|
9824
9838
|
function stopPolling() {
|
|
9825
9839
|
if (pollTimer !== null) {
|
|
9826
9840
|
try { clearTimer(pollTimer) } catch (e) { /* 忽略 */ }
|
|
@@ -9861,7 +9875,7 @@ function createUpdateController(deps) {
|
|
|
9861
9875
|
method: phone,
|
|
9862
9876
|
kind: kind,
|
|
9863
9877
|
errorHash: hash8(String((error && error.message) || error || kind)),
|
|
9864
|
-
pluginId:
|
|
9878
|
+
pluginId: PLUGIN_ID,
|
|
9865
9879
|
})
|
|
9866
9880
|
}
|
|
9867
9881
|
|
|
@@ -9874,7 +9888,7 @@ function createUpdateController(deps) {
|
|
|
9874
9888
|
latencyMs: Date.now() - startedAt,
|
|
9875
9889
|
ok: true,
|
|
9876
9890
|
kind: String((args && args.kind) || phone),
|
|
9877
|
-
pluginId:
|
|
9891
|
+
pluginId: PLUGIN_ID,
|
|
9878
9892
|
})
|
|
9879
9893
|
} else {
|
|
9880
9894
|
transportFail(phone, 'not-ok', (res && res.error) || 'not-ok')
|
|
@@ -9900,19 +9914,34 @@ function createUpdateController(deps) {
|
|
|
9900
9914
|
if (!state.available) return 'failed'
|
|
9901
9915
|
if (state.checking || state.installing) return 'busy'
|
|
9902
9916
|
// 已查到有新版且凭证还在:直接开弹窗,不重复联网
|
|
9903
|
-
if (state.hasNew && state.checkId) { patch({ dialogOpen: true, failure: null }); return 'new' }
|
|
9904
|
-
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()
|
|
9905
9921
|
try {
|
|
9906
9922
|
const res = await invoke(phones.updateCheck, {})
|
|
9907
9923
|
patch({ checking: false })
|
|
9908
|
-
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
|
+
}
|
|
9909
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
|
+
})
|
|
9910
9935
|
if (facts.canInstall) { patch({ dialogOpen: true }); return 'new' }
|
|
9911
9936
|
if (facts.pending) return 'latest'
|
|
9912
9937
|
return 'latest'
|
|
9913
9938
|
} catch (e) {
|
|
9914
9939
|
patch({ checking: false })
|
|
9915
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' } })
|
|
9916
9945
|
return 'failed'
|
|
9917
9946
|
}
|
|
9918
9947
|
}
|
|
@@ -9926,16 +9955,29 @@ function createUpdateController(deps) {
|
|
|
9926
9955
|
if (state.pending) return 'latest'
|
|
9927
9956
|
if (state.hasNew && state.checkId) return 'new'
|
|
9928
9957
|
patch({ checking: true, failure: null })
|
|
9958
|
+
record('info', 'update.check.start', { trigger: 'auto', pluginId: PLUGIN_ID })
|
|
9959
|
+
const startedAt = Date.now()
|
|
9929
9960
|
try {
|
|
9930
9961
|
const res = await invoke(phones.updateCheck, {})
|
|
9931
9962
|
patch({ checking: false })
|
|
9932
|
-
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
|
+
}
|
|
9933
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
|
+
})
|
|
9934
9973
|
if (facts.canInstall) return 'new'
|
|
9935
9974
|
return 'latest'
|
|
9936
9975
|
} catch (e) {
|
|
9937
9976
|
patch({ checking: false })
|
|
9938
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
|
+
})
|
|
9939
9981
|
return 'failed'
|
|
9940
9982
|
}
|
|
9941
9983
|
}
|
|
@@ -9954,23 +9996,38 @@ function createUpdateController(deps) {
|
|
|
9954
9996
|
async function install() {
|
|
9955
9997
|
if (!state.available || state.installing) return false
|
|
9956
9998
|
if (!state.checkId) return false
|
|
9957
|
-
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 })
|
|
9958
10001
|
const requestId = 'req-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000))
|
|
9959
10002
|
try {
|
|
9960
10003
|
const res = await invoke(phones.updateInstall, { checkId: state.checkId, requestId: requestId })
|
|
9961
10004
|
patch({ installing: false })
|
|
9962
10005
|
if (res && res.ok === true) {
|
|
9963
|
-
absorb(res)
|
|
10006
|
+
const facts = absorb(res)
|
|
9964
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' } })
|
|
9965
10015
|
return true
|
|
9966
10016
|
}
|
|
9967
10017
|
const facts = readSnapshot(res)
|
|
10018
|
+
const code = codeOf(res)
|
|
9968
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' } })
|
|
9969
10022
|
await readStatus()
|
|
9970
10023
|
return false
|
|
9971
10024
|
} catch (e) {
|
|
9972
10025
|
patch({ installing: false, failure: 'install-failed' })
|
|
9973
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' } })
|
|
9974
10031
|
try { await readStatus() } catch (e2) { /* 忽略 */ }
|
|
9975
10032
|
return false
|
|
9976
10033
|
}
|
|
@@ -9992,6 +10049,8 @@ function createUpdateController(deps) {
|
|
|
9992
10049
|
install: install,
|
|
9993
10050
|
openDialog: function () { patch({ dialogOpen: true }) },
|
|
9994
10051
|
closeDialog: function () { patch({ dialogOpen: false, failure: null }) },
|
|
10052
|
+
/** 面板其它部件(如日志开关)也能借这行可见提示,别让它们的失败静默掉。 */
|
|
10053
|
+
setNotice: function (key, kind) { patch({ notice: key ? { key: key, kind: kind || 'error' } : null }) },
|
|
9995
10054
|
dispose: function () { stopPolling(); listeners.length = 0 },
|
|
9996
10055
|
}
|
|
9997
10056
|
}
|
|
@@ -10031,7 +10090,7 @@ const STORAGE_KEY = 'dsh.opencode-palette.v2'
|
|
|
10031
10090
|
const LEGACY_STORAGE_KEY = 'dsh.opencode-tui-theme.v2'
|
|
10032
10091
|
const DEFAULT_STATE = { enabled: true, theme: 'opencode', mode: 'mono', size: 13, fontKey: 'JetBrains Mono' }
|
|
10033
10092
|
// 构建时由 scripts/build-client.mjs 替换为 package.json 版本(面板底部署小字)
|
|
10034
|
-
const PALETTE_VERSION = '2.0.
|
|
10093
|
+
const PALETTE_VERSION = '2.0.8'
|
|
10035
10094
|
|
|
10036
10095
|
function getReact() {
|
|
10037
10096
|
if (typeof require === 'function') { try { return require('react') } catch (e) { /* 动态版无 require */ } }
|
|
@@ -10175,6 +10234,14 @@ const I18N = {
|
|
|
10175
10234
|
'blocked.incompatible-node': { zh: '新版要求的 Node 更高,先升级 Node', en: 'The new version needs a newer Node — upgrade it first' },
|
|
10176
10235
|
'blocked.recovery-required': { zh: '上次安装被打断,重新点一次安装', en: 'The last install was interrupted — retry the install' },
|
|
10177
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' },
|
|
10178
10245
|
}
|
|
10179
10246
|
|
|
10180
10247
|
// 语言检测(回退):html[lang] 优先,回退浏览器语言
|
|
@@ -10258,8 +10325,10 @@ function createClient(slotTarget) {
|
|
|
10258
10325
|
pollMs: CLIENT_POLL.defaultMs,
|
|
10259
10326
|
log: function (level, event, fields) { try { clientLog.log(level, event, fields) } catch (e) { /* 忽略 */ } },
|
|
10260
10327
|
})
|
|
10261
|
-
//
|
|
10262
|
-
|
|
10328
|
+
// 启动时向宿主对账调试开关(以宿主为准);宿主不可用时静默,不抛错。
|
|
10329
|
+
// 面板的日志开关复用这一次对账的结果,不重复打扰宿主(对账失败会记一条 watchdog warn)。
|
|
10330
|
+
let logSwitchReady = null
|
|
10331
|
+
try { logSwitchReady = clientLog.reconcileLogSwitch() } catch (e) { /* 忽略 */ }
|
|
10263
10332
|
try { clientLog.log('info', 'host.call', { method: 'boot', latencyMs: 0, ok: true, kind: 'boot', pluginId: PLUGIN_ID }) } catch (e) { /* 忽略 */ }
|
|
10264
10333
|
|
|
10265
10334
|
// ── i18n 运行时:语言跟随 DSH(官方 locale 服务为信号源,html[lang] 仅作回退)──
|
|
@@ -10864,12 +10933,52 @@ function createClient(slotTarget) {
|
|
|
10864
10933
|
]))
|
|
10865
10934
|
: null
|
|
10866
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
|
+
|
|
10867
10967
|
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 920 } }, [
|
|
10868
10968
|
restartBanner,
|
|
10869
10969
|
// 头行:标题 + 状态开关(一个状态一个控制)
|
|
10870
10970
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, [
|
|
10871
10971
|
h('strong', null, '🎨 ' + tr('panelName')),
|
|
10872
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'))),
|
|
10873
10982
|
h('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)' } }, 'v' + PALETTE_VERSION),
|
|
10874
10983
|
updateButton,
|
|
10875
10984
|
h('a', {
|
|
@@ -10920,6 +11029,18 @@ function createClient(slotTarget) {
|
|
|
10920
11029
|
]),
|
|
10921
11030
|
h('div', { style: { fontSize: 12, color: muted } },
|
|
10922
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,
|
|
10923
11044
|
// ── 排印调节(置顶)──
|
|
10924
11045
|
h('div', { style: secTitle }, [
|
|
10925
11046
|
h('span', null, tr('typography')),
|
|
@@ -10989,6 +11110,11 @@ function createClient(slotTarget) {
|
|
|
10989
11110
|
// 检查更新:控制器 + 日志器交给面板(宿主不可用时 update.available 为假,按钮不渲染)
|
|
10990
11111
|
update: update,
|
|
10991
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
|
+
},
|
|
10992
11118
|
// 本机字体清单:面板打开下拉时调(要用户手势),未打开下拉不读;失败恒回退预设
|
|
10993
11119
|
collectFonts: collectFontCandidates,
|
|
10994
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.8",
|
|
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",
|