easy-codex 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # 更新记录
2
2
 
3
+ ## v0.2.4(2026-10-19)
4
+
5
+ - 菜单新增“更新版本”:后台复用 update,防重复点击;成功后自动重开浮窗,失败保留当前浮窗并提示,设置保持不变。
6
+
7
+ ## 0.2.3 — 2026-10-19
8
+
9
+ ### 修复
10
+
11
+ - update先在线刷新latest并比较版本,按明确版本号安装,防止旧缓存降级;相同或更高的本地版本不安装,检查失败明确报错。
12
+ - 保留上一版新增的update、show自动重开和绿色时段延长至18:00。
13
+
14
+
3
15
  ## 0.2.2 — 2026-10-19
4
16
 
5
17
  ### 新增
package/README.md CHANGED
@@ -2,25 +2,12 @@
2
2
 
3
3
  显示当前本机 Codex 登录账号的剩余额度百分比和重置时间。第三方辅助工具,与 OpenAI 无官方隶属关系。
4
4
 
5
- ## v0.2.2 新增与调整
5
+ ## v0.2.4 更新说明
6
6
 
7
- - **命令更新**:新增 `easy-codex update`,从官方 npm 更新当前全局安装。
8
- - **自动重开**:`easy-codex show` 会先退出自身旧浮窗,再打开新版;设置保持不变。
7
+ - 菜单新增“更新版本”,后台检查并安装最新正式版,更新期间防止重复点击。
8
+ - 更新成功后自动重开新版浮窗,保留时区、城市、位置和显示选择;失败时保留当前浮窗并提示原因。
9
9
 
10
- - 统一时间颜色:所选城市当地时间 **00:00–09:00 红色、09:00–18:00 绿色、18:00–24:00 橙色**,绿色时段延长到18:00。
11
- - 09:00转绿、18:00转橙、午夜转红,自动适应目标时区夏令时。
12
- - 保留已有时区、城市、位置和显示选择;额度圆环颜色阈值保持原有规则。
13
-
14
- 完整版本说明见 [CHANGELOG](CHANGELOG.md)。
15
-
16
- ## v0.2.1 新增功能
17
-
18
- - **快捷启动**:新增 `easy-codex show`,原 `overlay` 命令继续可用。
19
- - **重置倒计时**:周额度显示“5天后”等最大单位;5小时窗口显示“3小时2分后”,不足一小时只显示分钟。同时保留电脑本地时区的重置时刻。
20
- - **时间菜单优化**:开关改为“显示时间”;城市入口直接显示当前当地时间,如 `洛杉矶·05:23`,点号无空格。
21
- - **升级兼容**:保留已保存的时区、城市、位置和显示选择。
22
-
23
- 完整更新记录见 [CHANGELOG](CHANGELOG.md)。
10
+ 历史版本更新记录见 [CHANGELOG](CHANGELOG.md)。
24
11
 
25
12
  ## 安装与使用
26
13
 
@@ -53,7 +40,7 @@ npm i -g easy-codex@latest
53
40
  easy-codex show
54
41
  ```
55
42
 
56
- `show` 从 v0.2.1 正式提供,`overlay` 保留兼容。更新前从 EC 菜单退出旧浮窗,再运行 `show`。已保存的城市、时区、位置与显示选择会保留;“恢复默认位置”只恢复位置,不重置时区。
43
+ `show` 从 v0.2.1 正式提供,`overlay` 保留兼容。运行 `show` 会自动退出旧浮窗并打开新版。已保存的城市、时区、位置与显示选择会保留;“恢复默认位置”只恢复位置,不重置时区。
57
44
 
58
45
  周额度菜单示例:`周 · 77% · 5天后 25日 06:17 重置`。周倒计时只显示最大单位;若账号返回5小时窗口,其倒计时保留时分组合,例如 `3小时2分后 07:59 重置`,不足一小时仅显示分钟。绝对重置时间按电脑系统时区,独立时间浮窗按所选城市时区。
59
46
 
@@ -123,3 +110,5 @@ GitHub Actions 在 `v*` 标签上测试、构建通用 macOS 浮窗,并发布
123
110
 
124
111
 
125
112
  命令更新:`easy-codex update`,更新完成后运行 `easy-codex show`,会自动退出旧浮窗并打开新版。旧版本尚未提供 update 时,可先执行 `npm i -g easy-codex@latest`。更新过程中需要正常网络及原安装目录写入权限,不会自动使用 sudo。
113
+
114
+ 菜单中的“更新版本”会后台检查并更新,成功后自动重开浮窗;失败时保留当前浮窗并提示原因。更新期间不能重复点击,已保存设置保持不变。
@@ -233,6 +233,7 @@ final class Overlay: NSObject, NSApplicationDelegate, NSMenuDelegate {
233
233
  var status: NSStatusItem?
234
234
  var timer: Timer?
235
235
  var query: Process?
236
+ var updater: Process?
236
237
  var snapshot: Snapshot?
237
238
  var error: String?
238
239
  var lastAttempt = Date.distantPast
@@ -419,6 +420,7 @@ final class Overlay: NSObject, NSApplicationDelegate, NSMenuDelegate {
419
420
  zoneItem.submenu = zones
420
421
  menu.addItem(zoneItem)
421
422
  menu.addItem(.separator())
423
+ menu.addItem(item(updater == nil ? "更新版本" : "正在更新…", updater == nil ? #selector(updateVersion) : nil))
422
424
  menu.addItem(item("退出浮窗", #selector(quit)))
423
425
  return menu
424
426
  }
@@ -477,6 +479,54 @@ final class Overlay: NSObject, NSApplicationDelegate, NSMenuDelegate {
477
479
  UserDefaults.standard.removeObject(forKey: "bottomInset")
478
480
  tick()
479
481
  }
482
+ @objc func updateVersion() {
483
+ guard updater == nil else { return }
484
+ let cli = URL(fileURLWithPath: script).deletingLastPathComponent()
485
+ .appendingPathComponent("overlay-update.js")
486
+ let process = Process()
487
+ process.executableURL = URL(fileURLWithPath: node)
488
+ process.arguments = [cli.path]
489
+ var env = ProcessInfo.processInfo.environment
490
+ env["PATH"] = searchPath
491
+ process.environment = env
492
+ // npm 输出可能超过管道容量,写临时文件以免等待退出时死锁。
493
+ let output = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
494
+ guard FileManager.default.createFile(atPath: output.path, contents: nil,
495
+ attributes: [.posixPermissions: 0o600]), let handle = try? FileHandle(forWritingTo: output) else {
496
+ showUpdateResult("无法创建更新日志,请检查临时目录权限。")
497
+ return
498
+ }
499
+ process.standardOutput = handle
500
+ process.standardError = handle
501
+ process.terminationHandler = { [weak self] process in
502
+ try? handle.close()
503
+ let data = (try? Data(contentsOf: output)) ?? Data()
504
+ try? FileManager.default.removeItem(at: output)
505
+ let message = String(decoding: data.suffix(4096), as: UTF8.self)
506
+ Task { @MainActor in
507
+ guard let self else { return }
508
+ self.updater = nil
509
+ self.showUpdateResult(process.terminationStatus == 0
510
+ ? message : "更新失败,请检查网络或安装目录权限。\n" + message)
511
+ }
512
+ }
513
+ updater = process
514
+ do { try process.run() }
515
+ catch {
516
+ updater = nil
517
+ try? handle.close()
518
+ try? FileManager.default.removeItem(at: output)
519
+ showUpdateResult("无法启动更新,请重新运行 easy-codex show。")
520
+ }
521
+ }
522
+ func showUpdateResult(_ message: String) {
523
+ let alert = NSAlert()
524
+ alert.messageText = "更新版本"
525
+ alert.informativeText = message
526
+ alert.addButton(withTitle: "好")
527
+ NSApp.activate(ignoringOtherApps: true)
528
+ alert.runModal()
529
+ }
480
530
  @objc func quit() { NSApp.terminate(nil) }
481
531
  func applicationWillTerminate(_ notification: Notification) {
482
532
  timer?.invalidate()
@@ -5,7 +5,7 @@
5
5
  <key>CFBundleIdentifier</key><string>com.unionschool.easy-codex.overlay</string>
6
6
  <key>CFBundleName</key><string>Easy Codex Overlay</string>
7
7
  <key>CFBundlePackageType</key><string>APPL</string>
8
- <key>CFBundleShortVersionString</key><string>0.2.2</string>
8
+ <key>CFBundleShortVersionString</key><string>0.2.4</string>
9
9
  <key>LSMinimumSystemVersion</key><string>14.0</string>
10
10
  <key>LSUIElement</key><true/>
11
11
  <key>NSHighResolutionCapable</key><true/>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-codex",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Codex usage limits and transparent world clock overlay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,12 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { updatePackage } from './update.js';
3
+
4
+ export async function updateAndShow({ update = updatePackage,
5
+ show = async () => (await import('./overlay.js')).launchOverlay() } = {}) {
6
+ await update('easy-codex');
7
+ await show();
8
+ }
9
+
10
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
11
+ updateAndShow().catch(error => { console.error(error.message); process.exitCode = 1; });
12
+ }
package/src/update.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { access, realpath } from 'node:fs/promises';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { dirname, basename, join, delimiter } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
- import { spawn } from 'node:child_process';
5
+ import { spawn, execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
5
8
 
6
9
  export function installedPrefix(root, platform = process.platform) {
7
10
  const modules = dirname(root.replace(/[\\/]$/, ''));
@@ -10,6 +13,15 @@ export function installedPrefix(root, platform = process.platform) {
10
13
  return basename(parent) === 'lib' ? dirname(parent) : platform === 'win32' ? parent : null;
11
14
  }
12
15
 
16
+ export function isNewer(latest, current) {
17
+ if (!/^\d+\.\d+\.\d+$/.test(latest) || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(current)) {
18
+ throw new Error('无法识别正式版本号,未执行更新。');
19
+ }
20
+ const a = latest.split('.').map(Number), b = current.split('-')[0].split('.').map(Number);
21
+ for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i]; }
22
+ return current.includes('-');
23
+ }
24
+
13
25
  async function findNpmCli() {
14
26
  const directories = [dirname(process.execPath), ...(process.env.PATH ?? '').split(delimiter)];
15
27
  for (const directory of directories.filter(Boolean)) {
@@ -22,6 +34,14 @@ async function findNpmCli() {
22
34
  throw new Error('未找到 npm,请先安装 Node.js/npm,再使用 npm i -g 更新。');
23
35
  }
24
36
 
37
+ async function readLatest(npmCli, name) {
38
+ try {
39
+ const { stdout } = await promisify(execFile)(process.execPath, [npmCli, 'view', name, 'dist-tags.latest',
40
+ '--json', '--prefer-online', '--registry=https://registry.npmjs.org'], { timeout: 30000, maxBuffer: 4096 });
41
+ return JSON.parse(stdout);
42
+ } catch { throw new Error('无法在线检查 npm 最新版本,请检查网络后重试。'); }
43
+ }
44
+
25
45
  function runNpm(command, args) {
26
46
  return new Promise((resolve, reject) => {
27
47
  const child = spawn(command, args, { stdio: 'inherit', shell: false });
@@ -34,12 +54,19 @@ function runNpm(command, args) {
34
54
  }
35
55
 
36
56
  export async function updatePackage(name, { root = fileURLToPath(new URL('../', import.meta.url)),
37
- getNpmCli = findNpmCli, run = runNpm, platform = process.platform } = {}) {
57
+ getNpmCli = findNpmCli, getLatest = readLatest, currentVersion = version,
58
+ run = runNpm, platform = process.platform } = {}) {
38
59
  if (!['easy-codex', 'easy-claude'].includes(name)) throw new Error('不支持的更新包名。');
39
60
  const npmCli = await getNpmCli();
61
+ const latest = await getLatest(npmCli, name);
62
+ if (!isNewer(latest, currentVersion)) {
63
+ console.log(`${name} 当前 ${currentVersion},npm 最新 ${latest},无需更新。`);
64
+ return;
65
+ }
40
66
  const prefix = installedPrefix(root, platform);
67
+ // 先在线检查再锁定版本,避免安装阶段复用旧 latest 缓存而降级。
41
68
  const args = [npmCli, 'install', '--global', ...(prefix ? ['--prefix', prefix] : []),
42
- '--registry=https://registry.npmjs.org', `${name}@latest`];
69
+ '--prefer-online', '--registry=https://registry.npmjs.org', `${name}@${latest}`];
43
70
  await run(process.execPath, args);
44
- console.log(`${name} 已更新;运行 ${name} show 自动重启浮窗。`);
71
+ console.log(`${name} 已更新到 ${latest};运行 ${name} show 自动重启浮窗。`);
45
72
  }