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 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 by count; click the title to go back up
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,命令行 / 网页端用 PATH 上的 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.0";
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
@@ -89,54 +89,237 @@ export function resolveSelfDir(moduleUrl) {
89
89
  }
90
90
 
91
91
  /**
92
- * 定位 pnpm 可执行文件。
92
+ * 取一个「可能是字符串」的注入值:类型不对时退回默认值。
93
+ * 传 `null` / 数字 / 对象都按「显式给了一个不可用的值」处理,不回退。
93
94
  *
94
- * 顺序:
95
- * 1. 环境变量 `DSH_DESKTOP_STATUSBAR_PNPM`:完整命令覆盖。可以是 JSON 数组
96
- * (`["node","/path/pnpm.mjs"]`,用别的 node 跑打包的 pnpm 或包一层脚本时用),
97
- * 也可以是单个可执行文件路径。
98
- * 2. 宿主随包提供的 pnpm:从 `process.execPath` 推出 resources 目录。
99
- * DSH 桌面端把 pnpm 放在 `resources/runtime/pnpm/bin/pnpm.mjs`,
100
- * 用 Electron 可执行文件加 `ELECTRON_RUN_AS_NODE=1` 启动即可当 node 用
101
- * (官方插件管理器走的也是这条路)。
102
- * 3. PATH 上的 `pnpm`(web 端 / CLI 运行时常走这条)。
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
- * @returns {{command: string, args: string[], electron: boolean}|null}
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 resolvePnpm() {
107
- const explicit = process.env.DSH_DESKTOP_STATUSBAR_PNPM
108
- if (typeof explicit === 'string' && explicit.trim().length > 0) {
109
- const text = explicit.trim()
110
- if (text.startsWith('[')) {
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 parsed = JSON.parse(text)
113
- if (Array.isArray(parsed) === true && parsed.length > 0 && parsed.every((one) => typeof one === 'string')) {
114
- return { command: parsed[0], args: parsed.slice(1), electron: false }
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
- const execPath = process.execPath
124
- if (typeof execPath === 'string' && execPath.length > 0) {
125
- const resources = join(dirname(execPath), 'resources')
126
- const candidates = [
127
- join(resources, 'runtime', 'pnpm', 'bin', 'pnpm.mjs'),
128
- join(resources, 'app.asar.unpacked', 'dsh', 'node_modules', 'pnpm', 'bin', 'pnpm.mjs'),
129
- ]
130
- for (const candidate of candidates) {
131
- try {
132
- if (existsSync(candidate)) return { command: execPath, args: [candidate], electron: true }
133
- } catch (error) {
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
- return null
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.7.0 的 `add` 不认这个选项
235
- * (实测报 `Unknown option: 'frozen-lockfile'`)。`add` 本来就会更新锁文件,
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
- const pnpm = resolvePnpm()
263
- if (pnpm === null) {
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 (pnpm.electron === true) env.ELECTRON_RUN_AS_NODE = '1'
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(pnpm.args, target)
490
+ const args = updateArgs(runner.args, target)
292
491
 
293
492
  log('update: 开始安装 ' + target)
294
- const run = await runCommand(pnpm.command, args, {
493
+ const run = await runCommand(runner.command, args, {
295
494
  cwd: profileDir,
296
495
  env,
297
496
  timeoutMs: options.timeoutMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-desktop-statusbar",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "type": "module",
5
5
  "description": "把 DSH 桌面端对话区底部的统计行换成可配置状态栏:10 个字段可选可排序,按官方峰谷口径估算费用,支持自定义模型单价与账户余额。",
6
6
  "icon": "icon.svg",