dsh-desktop-statusbar 2.2.0 → 2.2.1
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.en.md +2 -1
- package/README.md +2 -2
- package/lib/client.js +1 -1
- package/lib/index.js +3 -0
- package/lib/self-update.js +243 -44
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -38,7 +38,7 @@ Replaces the stats line under the message input in the DSH desktop app with a co
|
|
|
38
38
|
| Session cost | Cumulative token breakdown for this session |
|
|
39
39
|
| Balance | Top-up balance / granted balance / quota left |
|
|
40
40
|
|
|
41
|
-
- **Session data drill-down**: context compactions mark the node they happened at, skills injected counts the skills injected in this session, and tool calls list every tool the session used
|
|
41
|
+
- **Session data drill-down**: context compactions mark the node they happened at, skills injected counts the skills injected in this session, and tool calls list every tool the session used with its call count; click the title to go back up
|
|
42
42
|
- **Version check and update**: DSH checks for the latest version on startup, shows a hint next to the "Status bar" nav item when one is available, and offers a one-click update button in settings
|
|
43
43
|
- **GitHub support**: your support is the biggest motivation for the developer to keep going! Click the button to open the GitHub project page and help with a star — thanks, everyone!
|
|
44
44
|
- **Cost estimation**: priced with the official peak/off-peak rules; each call is computed from the moment it happened and the model it used
|
|
@@ -98,6 +98,7 @@ Peak hours are **09:00–12:00 and 14:00–18:00 Beijing time, Monday to Friday*
|
|
|
98
98
|
|
|
99
99
|
- Fastest / slowest TTFT and speed come from a new host-side projection: **you must fully quit and restart DSH** before it starts collecting
|
|
100
100
|
- A model with no price is not billed; adding the price back-fills its earlier calls
|
|
101
|
+
- One-click update relies on `pnpm`: the desktop app uses the copy bundled with the host, while CLI / web usage needs `pnpm` on the system. If neither is found, the update says so explicitly **and logs every location it tried**; you can also point it at a specific command with the `DSH_DESKTOP_STATUSBAR_PNPM` environment variable
|
|
101
102
|
|
|
102
103
|
## Development
|
|
103
104
|
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
| 总计费用 | 当前会话的累计 token 明细 |
|
|
39
39
|
| 余额 | 充值余额 / 赠金余额 / 剩余配额 |
|
|
40
40
|
|
|
41
|
-
-
|
|
41
|
+
- **会话数据下钻**:上下文压缩逐次标注发生节点,技能注入统计当前会话注入的技能,工具调用列出会话使用的所有工具与各自次数;点击标题返回上一层
|
|
42
42
|
- **版本自检与更新**:DSH 启动时自动检测最新版本,发现新版时在「状态栏」导航项旁显示提示,并在设置页提供一键更新按钮
|
|
43
43
|
- **费用估算**:按官方峰谷口径计价,每条调用按其发生时刻与所用模型分别计算
|
|
44
44
|
- **价格库**:自定义单价、峰谷分档、CNY / USD 计价单位
|
|
@@ -98,7 +98,7 @@ npm view dsh-desktop-statusbar version
|
|
|
98
98
|
|
|
99
99
|
- 最快 / 最慢首字与速度来自 host 新增的投影,**升级后需完全退出并重启 DSH** 才开始统计
|
|
100
100
|
- 价格库中缺少单价的模型不计费,补充单价后自动补算
|
|
101
|
-
- 一键更新依赖 pnpm 可执行文件:桌面端用宿主随包提供的 pnpm,命令行 /
|
|
101
|
+
- 一键更新依赖 pnpm 可执行文件:桌面端用宿主随包提供的 pnpm,命令行 / 网页端需要系统里有 `pnpm`。都找不到时会明确提示「找不到 pnpm」,并把**尝试过的每个位置**一并写进日志;也可以用环境变量 `DSH_DESKTOP_STATUSBAR_PNPM` 直接指定要用的命令
|
|
102
102
|
|
|
103
103
|
## 开发
|
|
104
104
|
|
package/lib/client.js
CHANGED
|
@@ -74,7 +74,7 @@ window.__ModuleLoader__.load({
|
|
|
74
74
|
*/
|
|
75
75
|
const CONFIG_VERSION = 6;
|
|
76
76
|
/** 插件版本:显示在设置页底部,用来确认页面加载的是哪一版客户端代码。 */
|
|
77
|
-
const VERSION = "2.2.
|
|
77
|
+
const VERSION = "2.2.1";
|
|
78
78
|
/** 官方参考价的版本:官方调价时改这个数字,老价格库会自动并入新参考价。 */
|
|
79
79
|
const PRICE_VERSION = 2;
|
|
80
80
|
/** 项目主页:设置页那个「去 GitHub 点 star」按钮跳这里(与 package.json 的 repository 同址)。 */
|
package/lib/index.js
CHANGED
|
@@ -4077,6 +4077,9 @@ async function handleApi(req, res, ctx) {
|
|
|
4077
4077
|
reason: last.reason === undefined ? null : last.reason,
|
|
4078
4078
|
from: last.from === undefined ? null : last.from,
|
|
4079
4079
|
to: last.to === undefined ? null : last.to,
|
|
4080
|
+
/* 找不到 pnpm 时把「试过哪些路径」一并交给界面:排查这类问题时最费时间的就是
|
|
4081
|
+
手工逐条 Test-Path,这里直接摊开。其它失败原因下是空数组。 */
|
|
4082
|
+
tried: Array.isArray(last.tried) === true ? last.tried : [],
|
|
4080
4083
|
},
|
|
4081
4084
|
})
|
|
4082
4085
|
return
|
package/lib/self-update.js
CHANGED
|
@@ -89,54 +89,237 @@ export function resolveSelfDir(moduleUrl) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* 取一个「可能是字符串」的注入值:类型不对时退回默认值。
|
|
93
|
+
* 传 `null` / 数字 / 对象都按「显式给了一个不可用的值」处理,不回退。
|
|
93
94
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
* @param {unknown} value 注入值
|
|
96
|
+
* @param {unknown} fallback 默认值
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function asText(value, fallback) {
|
|
100
|
+
if (value === undefined) return typeof fallback === 'string' ? fallback : ''
|
|
101
|
+
return typeof value === 'string' ? value : ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 判断一段文本看起来是不是绝对路径。
|
|
103
106
|
*
|
|
104
|
-
*
|
|
107
|
+
* 不用 `node:path` 的 `isAbsolute()`:它按**当前进程**的平台判断,而这里允许测试
|
|
108
|
+
* 注入 `options.platform`,两者可能对不上。两种平台的写法都认:
|
|
109
|
+
* `C:\x`(盘符)、`\\server\share`(UNC)、`/x`(POSIX 根)。
|
|
110
|
+
*
|
|
111
|
+
* @param {string} text
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
function looksAbsolutePath(text) {
|
|
115
|
+
return /^[A-Za-z]:[\\/]/.test(text)
|
|
116
|
+
|| text.startsWith('\\\\')
|
|
117
|
+
|| text.startsWith('/')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 去掉路径结尾的分隔符;盘符根(`C:\`)原样保留 —— 去掉会变成 `C:`。
|
|
122
|
+
*
|
|
123
|
+
* @param {string} text
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
function trimTrailingSeparators(text) {
|
|
127
|
+
let out = text
|
|
128
|
+
while (out.length > 1 && /[\\/]$/.test(out) === true) {
|
|
129
|
+
if (/^[A-Za-z]:[\\/]$/.test(out) === true) break
|
|
130
|
+
out = out.slice(0, -1)
|
|
131
|
+
}
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 解析安装根下的 `resources` 目录。
|
|
137
|
+
*
|
|
138
|
+
* 优先级(与宿主自己的 `runtimeResources()` 取值顺序一致):
|
|
139
|
+
* 1. 显式注入的 `resourcesPath`(测试用,也可以是调用方认定的权威值);
|
|
140
|
+
* 2. `process.resourcesPath` —— Electron 打包版给的权威值,比推断更稳;
|
|
141
|
+
* 3. 由 `execPath` 推断:`dirname(execPath)/resources`(非 Electron 环境兜底)。
|
|
142
|
+
*
|
|
143
|
+
* @param {string} resourcesPath 已处理过默认值(可能是空字符串)
|
|
144
|
+
* @param {string} execPath 已处理过默认值(可能是空字符串)
|
|
145
|
+
* @returns {string|null} 推断不出来时返回 null
|
|
146
|
+
*/
|
|
147
|
+
function resolveResourcesDir(resourcesPath, execPath) {
|
|
148
|
+
if (resourcesPath.length > 0 && looksAbsolutePath(resourcesPath) === true) {
|
|
149
|
+
return trimTrailingSeparators(resourcesPath)
|
|
150
|
+
}
|
|
151
|
+
if (execPath.length > 0) return trimTrailingSeparators(join(dirname(execPath), 'resources'))
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 定位 pnpm,返回**结构化结果**(永不抛异常),把「试过哪些候选」一并带回。
|
|
157
|
+
*
|
|
158
|
+
* 只做定位,不执行任何外部命令。
|
|
159
|
+
*
|
|
160
|
+
* 候选顺序(命中即返回,逐个记进 `tried`):
|
|
161
|
+
* 1. `DSH_DESKTOP_STATUSBAR_PNPM`:插件自己的覆盖口,优先级最高。可以是 JSON 数组
|
|
162
|
+
* (`["node","/path/pnpm.mjs"]`,用别的 node 跑打包的 pnpm 时用),也可以是
|
|
163
|
+
* 单个可执行文件路径。**不检查文件是否存在** —— 用户显式指定的入口就该照办。
|
|
164
|
+
* 2. `DSH_DESKTOP_PNPM_ENTRY`:宿主自己的口径(宿主 `runtimeResources()` 在开发
|
|
165
|
+
* 模式下读它)。排在文件候选之前,宿主以后挪位置也不会失配。
|
|
166
|
+
* 3. 宿主随包提供的 pnpm,按 `resources` 目录拼出来,三条候选依次尝试:
|
|
167
|
+
* - `runtime/primary-runtime/dependencies/pnpm/bin/pnpm.mjs`:**当前宿主布局**。
|
|
168
|
+
* 优先用同一套运行时里的 node(`runtime/primary-runtime/dependencies/node/bin/node.exe`)
|
|
169
|
+
* 去跑它,不依赖 Electron 的 `ELECTRON_RUN_AS_NODE`;那套 node 不在时退回宿主可执行文件
|
|
170
|
+
* (此时才需要 `ELECTRON_RUN_AS_NODE=1`)。
|
|
171
|
+
* - `runtime/pnpm/bin/pnpm.mjs`:更旧的宿主布局,保留兼容。
|
|
172
|
+
* - `app.asar.unpacked/dsh/node_modules/pnpm/bin/pnpm.mjs`:再旧的打包布局,保留兼容。
|
|
173
|
+
*
|
|
174
|
+
* 找不到时**不做 PATH 搜索**:Windows 上 PATH 里多半是 `pnpm.cmd`,spawn 它需要
|
|
175
|
+
* `shell: true`,等于把命令解释权交给 shell,风险面大于收益。真需要时请用户用
|
|
176
|
+
* `DSH_DESKTOP_STATUSBAR_PNPM` 直接指定(issue 里的绕过写法就是这么用的)。
|
|
177
|
+
*
|
|
178
|
+
* @param {{
|
|
179
|
+
* env?: {[key: string]: string|undefined},
|
|
180
|
+
* execPath?: string,
|
|
181
|
+
* resourcesPath?: string,
|
|
182
|
+
* exists?: (path: string) => boolean,
|
|
183
|
+
* electron?: boolean,
|
|
184
|
+
* platform?: string,
|
|
185
|
+
* }} [options] 全部可选;只有测试会传
|
|
186
|
+
* @returns {{
|
|
187
|
+
* ok: boolean,
|
|
188
|
+
* runner: {command: string, args: string[], electron: boolean}|null,
|
|
189
|
+
* source: 'custom-env'|'host-env'|'primary-runtime'|'runtime-legacy'|'asar-unpacked'|null,
|
|
190
|
+
* tried: Array<{source: string, path: string|null, note: string}>,
|
|
191
|
+
* }}
|
|
105
192
|
*/
|
|
106
|
-
export function
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
193
|
+
export function resolvePnpmDetailed(options) {
|
|
194
|
+
const opts = options === null || typeof options !== 'object' ? {} : options
|
|
195
|
+
/** 环境变量来源;默认 process.env。 */
|
|
196
|
+
const env = opts.env === undefined ? process.env : (opts.env === null ? {} : opts.env)
|
|
197
|
+
/** 宿主可执行文件;空字符串表示「拿不到」。 */
|
|
198
|
+
const execPath = asText(opts.execPath, process.execPath)
|
|
199
|
+
/** 注入的 resources 目录;未传时才回落到 process.resourcesPath。 */
|
|
200
|
+
const resourcesPath = asText(opts.resourcesPath, process.resourcesPath)
|
|
201
|
+
/** 是否跑在 Electron 里 —— 只有退回宿主可执行文件当 node 用时才需要它。 */
|
|
202
|
+
const electron = opts.electron === undefined
|
|
203
|
+
? (typeof process.versions === 'object'
|
|
204
|
+
&& process.versions !== null
|
|
205
|
+
&& typeof process.versions.electron === 'string'
|
|
206
|
+
&& process.versions.electron.length > 0)
|
|
207
|
+
: opts.electron === true
|
|
208
|
+
/** 平台;只用来决定同套运行时里的 node 叫 node.exe 还是 node。 */
|
|
209
|
+
const platform = asText(opts.platform, process.platform)
|
|
210
|
+
|
|
211
|
+
/** 每次尝试的记录:命中的也记,失败时一并带回给调用方写日志。 */
|
|
212
|
+
const tried = []
|
|
213
|
+
/** 记一条命中并直接返回成功结果。 */
|
|
214
|
+
const hit = (source, runner, path) => {
|
|
215
|
+
tried.push({ source, path: typeof path === 'string' ? path : null, note: '命中' })
|
|
216
|
+
return { ok: true, runner, source, tried }
|
|
217
|
+
}
|
|
218
|
+
/** 记一条未命中,继续试下一个候选。 */
|
|
219
|
+
const miss = (source, path, note) => {
|
|
220
|
+
tried.push({ source, path: typeof path === 'string' ? path : null, note })
|
|
221
|
+
}
|
|
222
|
+
/** 全部落空。 */
|
|
223
|
+
const fail = () => ({ ok: false, runner: null, source: null, tried })
|
|
224
|
+
/** 文件存在性检查;注入的实现抛错也当成「不存在」,保证本函数永不抛。 */
|
|
225
|
+
const exists = (target) => {
|
|
226
|
+
if (typeof opts.exists === 'function') {
|
|
227
|
+
try { return opts.exists(target) === true } catch (error) { return false }
|
|
228
|
+
}
|
|
229
|
+
try { return existsSync(target) === true } catch (error) { return false }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* 1) 插件自己的覆盖口:完全不检查文件是否存在(用户显式指定的入口) */
|
|
233
|
+
const custom = asText(env.DSH_DESKTOP_STATUSBAR_PNPM, '')
|
|
234
|
+
if (custom.trim().length > 0) {
|
|
235
|
+
const text = custom.trim()
|
|
236
|
+
if (text.startsWith('[') === true) {
|
|
237
|
+
let parsed = null
|
|
111
238
|
try {
|
|
112
|
-
const
|
|
113
|
-
if (Array.isArray(
|
|
114
|
-
|
|
239
|
+
const value = JSON.parse(text)
|
|
240
|
+
if (Array.isArray(value) === true && value.length > 0 && value.every((one) => typeof one === 'string') === true) {
|
|
241
|
+
parsed = value
|
|
115
242
|
}
|
|
116
243
|
} catch (error) {
|
|
117
|
-
/*
|
|
244
|
+
/* 不是合法数组,落到下面的单路径处理 */
|
|
118
245
|
}
|
|
246
|
+
if (parsed !== null) {
|
|
247
|
+
return hit('custom-env', { command: parsed[0], args: parsed.slice(1), electron: false }, custom)
|
|
248
|
+
}
|
|
249
|
+
/* `'[]'` 也走这里:空数组不够当命令用,退化成单路径(保持旧行为) */
|
|
250
|
+
miss('custom-env', custom, '不是合法 JSON 数组,按单路径处理')
|
|
119
251
|
}
|
|
120
|
-
return { command: text, args: [], electron: false }
|
|
252
|
+
return hit('custom-env', { command: text, args: [], electron: false }, custom)
|
|
121
253
|
}
|
|
122
254
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/* 试下一个 */
|
|
135
|
-
}
|
|
255
|
+
/* 2) 宿主自己的口径:只存在于开发模式,但插件照样优先认它 */
|
|
256
|
+
const hostEntry = asText(env.DSH_DESKTOP_PNPM_ENTRY, '')
|
|
257
|
+
if (hostEntry.trim().length > 0) {
|
|
258
|
+
const entry = hostEntry.trim()
|
|
259
|
+
if (execPath.length === 0) {
|
|
260
|
+
miss('host-env', hostEntry, '没有可用的宿主可执行文件')
|
|
261
|
+
} else if (exists(entry) === true) {
|
|
262
|
+
/* tried.path 记变量原值(契约口径),实际执行用去掉首尾空白的路径 */
|
|
263
|
+
return hit('host-env', { command: execPath, args: [entry], electron }, hostEntry)
|
|
264
|
+
} else {
|
|
265
|
+
miss('host-env', hostEntry, '不存在')
|
|
136
266
|
}
|
|
137
267
|
}
|
|
138
268
|
|
|
139
|
-
|
|
269
|
+
/* 3) 按 resources 目录拼出来的文件候选 */
|
|
270
|
+
const resources = resolveResourcesDir(resourcesPath, execPath)
|
|
271
|
+
if (resources === null) {
|
|
272
|
+
miss('primary-runtime', null, '无法推断 resources 目录')
|
|
273
|
+
return fail()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* 3.1 当前宿主布局:优先用同一套运行时里的 node,不碰 ELECTRON_RUN_AS_NODE */
|
|
277
|
+
const primaryPnpm = join(resources, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')
|
|
278
|
+
if (exists(primaryPnpm) === true) {
|
|
279
|
+
const primaryNode = join(resources, 'runtime', 'primary-runtime', 'dependencies', 'node', 'bin', platform === 'win32' ? 'node.exe' : 'node')
|
|
280
|
+
if (exists(primaryNode) === true) {
|
|
281
|
+
return hit('primary-runtime', { command: primaryNode, args: [primaryPnpm], electron: false }, primaryPnpm)
|
|
282
|
+
}
|
|
283
|
+
if (execPath.length > 0) {
|
|
284
|
+
return hit('primary-runtime', { command: execPath, args: [primaryPnpm], electron }, primaryPnpm)
|
|
285
|
+
}
|
|
286
|
+
miss('primary-runtime', primaryPnpm, '找到 pnpm,但同套运行时里的 node 与宿主可执行文件都不可用')
|
|
287
|
+
} else {
|
|
288
|
+
miss('primary-runtime', primaryPnpm, '不存在')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* 3.2 / 3.3 旧布局:向后兼容,用宿主可执行文件加 ELECTRON_RUN_AS_NODE 去跑 */
|
|
292
|
+
const legacyCandidates = [
|
|
293
|
+
{ source: 'runtime-legacy', file: join(resources, 'runtime', 'pnpm', 'bin', 'pnpm.mjs') },
|
|
294
|
+
{ source: 'asar-unpacked', file: join(resources, 'app.asar.unpacked', 'dsh', 'node_modules', 'pnpm', 'bin', 'pnpm.mjs') },
|
|
295
|
+
]
|
|
296
|
+
for (const candidate of legacyCandidates) {
|
|
297
|
+
if (exists(candidate.file) !== true) {
|
|
298
|
+
miss(candidate.source, candidate.file, '不存在')
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
if (execPath.length === 0) {
|
|
302
|
+
miss(candidate.source, candidate.file, '找到 pnpm,但没有可用的宿主可执行文件')
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
return hit(candidate.source, { command: execPath, args: [candidate.file], electron }, candidate.file)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return fail()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 定位 pnpm 可执行文件(向后兼容的薄包装)。
|
|
313
|
+
*
|
|
314
|
+
* 内部走 `resolvePnpmDetailed()`,只关心「最终能不能跑」,不关心试过哪些候选。
|
|
315
|
+
* 现有测试与 `verify-self-update.js --online` 用的都是这个签名。
|
|
316
|
+
*
|
|
317
|
+
* @param {object} [options] 透传给 resolvePnpmDetailed(测试注入用)
|
|
318
|
+
* @returns {{command: string, args: string[], electron: boolean}|null}
|
|
319
|
+
*/
|
|
320
|
+
export function resolvePnpm(options) {
|
|
321
|
+
const detail = resolvePnpmDetailed(options)
|
|
322
|
+
return detail.ok === true ? detail.runner : null
|
|
140
323
|
}
|
|
141
324
|
|
|
142
325
|
/**
|
|
@@ -231,9 +414,9 @@ export const RELEASE_AGE_OVERRIDE = '--config.minimum-release-age=0'
|
|
|
231
414
|
* - `--save-exact`:清单里写死精确版本,避免 `^` 范围日后漂移。
|
|
232
415
|
* - 发布年龄归零:见 RELEASE_AGE_OVERRIDE。
|
|
233
416
|
*
|
|
234
|
-
* 不加 `--no-frozen-lockfile`:pnpm 11
|
|
235
|
-
* (实测报 `Unknown option: 'frozen-lockfile'
|
|
236
|
-
*
|
|
417
|
+
* 不加 `--no-frozen-lockfile`:pnpm 11 的 `add` 不认这个选项
|
|
418
|
+
* (实测报 `Unknown option: 'frozen-lockfile'`;宿主当前自带 pnpm 11.28.5,同样如此)。
|
|
419
|
+
* `add` 本来就会更新锁文件,不需要它。
|
|
237
420
|
*
|
|
238
421
|
* @param {string[]} base pnpm 自身的参数(例如打包 pnpm 的 .mjs 路径)
|
|
239
422
|
* @param {string} target 安装目标 `包名@版本`
|
|
@@ -257,14 +440,30 @@ export async function runSelfUpdate(options) {
|
|
|
257
440
|
刷新页面不算数 —— 插件包元数据(含 client.js 的 revision)在启动时缓存、直到重启才更新
|
|
258
441
|
(见 @deepseek-ai/dsh-hmr 的已知限制与 dsh-client-modules 的说明)。
|
|
259
442
|
不用「要不要重启」去猜,直接如实告诉界面。 */
|
|
260
|
-
const result = { ok: false, from, to: options.version, reason: null, detail: null, profileDir: null, changedExclude: false, restartRequired: true }
|
|
443
|
+
const result = { ok: false, from, to: options.version, reason: null, detail: null, profileDir: null, changedExclude: false, restartRequired: true, tried: [] }
|
|
261
444
|
|
|
262
|
-
|
|
263
|
-
|
|
445
|
+
/* 注意:必须在找 profile 之前就报 pnpm-not-found —— 找不到 pnpm 时 profile
|
|
446
|
+
目录是否存在无关紧要,先告诉用户真正的卡点。 */
|
|
447
|
+
const pnpm = resolvePnpmDetailed()
|
|
448
|
+
if (pnpm.ok !== true) {
|
|
264
449
|
result.reason = 'pnpm-not-found'
|
|
450
|
+
/* 把试过的候选写进结果和日志:issue 里那次排查全靠手工逐条 Test-Path,
|
|
451
|
+
以后用户/开发者看日志就能定位,不用再猜插件找的是哪条路径。 */
|
|
452
|
+
result.tried = pnpm.tried
|
|
453
|
+
.slice(0, 12)
|
|
454
|
+
.map((one) => {
|
|
455
|
+
const where = one.path === null ? '—' : one.path
|
|
456
|
+
const line = one.source + ': ' + where + '(' + one.note + ')'
|
|
457
|
+
return line.length > 300 ? line.slice(0, 300) + '…' : line
|
|
458
|
+
})
|
|
459
|
+
log('update: pnpm-not-found,尝试过:')
|
|
460
|
+
for (const line of result.tried) log(' - ' + line)
|
|
265
461
|
return result
|
|
266
462
|
}
|
|
267
463
|
|
|
464
|
+
const runner = pnpm.runner
|
|
465
|
+
log('update: pnpm 来源 ' + pnpm.source + ' → ' + runner.command + ' ' + runner.args.join(' ') + (runner.electron === true ? '(ELECTRON_RUN_AS_NODE)' : ''))
|
|
466
|
+
|
|
268
467
|
const profileDir = findProfileDir(selfDir)
|
|
269
468
|
result.profileDir = profileDir
|
|
270
469
|
if (profileDir === null) {
|
|
@@ -285,13 +484,13 @@ export async function runSelfUpdate(options) {
|
|
|
285
484
|
/* 精确版本,不用 @latest:后者在发布年龄策略下会静默装成旧的成熟版 */
|
|
286
485
|
const target = SELF_NAME + '@' + options.version
|
|
287
486
|
const env = { ...process.env }
|
|
288
|
-
if (
|
|
487
|
+
if (runner.electron === true) env.ELECTRON_RUN_AS_NODE = '1'
|
|
289
488
|
/* CI=true:pnpm 在非交互环境下不会弹提示卡住(与官方插件管理器同一做法) */
|
|
290
489
|
env.CI = 'true'
|
|
291
|
-
const args = updateArgs(
|
|
490
|
+
const args = updateArgs(runner.args, target)
|
|
292
491
|
|
|
293
492
|
log('update: 开始安装 ' + target)
|
|
294
|
-
const run = await runCommand(
|
|
493
|
+
const run = await runCommand(runner.command, args, {
|
|
295
494
|
cwd: profileDir,
|
|
296
495
|
env,
|
|
297
496
|
timeoutMs: options.timeoutMs,
|