aaden-cli 1.6.95 → 1.6.97

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.
@@ -1,11 +1,102 @@
1
1
  #!/bin/bash
2
+ # 轻量后端部署(light-update 的执行体)。
3
+ # - composer.lock 门:远端 raw/PHP/composer.lock == 本机? 相同→code.zip(~1MB,保留vendor);不同→latest.zip全量
4
+ # - **先下载校验,确认源存在后才做任何破坏性操作**(避免"删了迁移又没铺上")
5
+ # - 部署前 tar 备份 PHP 代码(排除 vendor);migrate 后健康检查;失败自动回滚
6
+ # - 绝不 snap refresh 前端、绝不 apt
7
+ # 退出码:0=成功(或无需更新),1=失败/已回滚。末行输出 DEPLOY_OK <ver> | ROLLED_BACK | ABORT_*
8
+ set -uo pipefail
9
+ S3="https://aaden-backend.s3.eu-central-1.amazonaws.com"
10
+ STAGE=/var/tmp/aaden-lu-stage # 绝对路径:download 与 sudo cp 必须看到同一个目录(~ 在 sudo 下会变 /root)
11
+ BACKUP=/var/lib/aaden-php-backup
12
+ PW='Innerken322.'
13
+ sudoS(){ echo "$PW" | sudo -S bash -c "$*"; }
2
14
 
3
- cd files/ || exit
4
- ./getFile.sh
15
+ # 早失败:老 aaden 设备的 sudo 密码可能不是共享密码。先探一次 ——
16
+ # 否则会一路穿到 ABORT_DOWNLOAD,把"密码不符"误报成"下载问题",排障跑偏。
17
+ # NOPASSWD 设备此探测同样通过(sudo 不问,管道密码被忽略)。
18
+ if ! echo "$PW" | sudo -S true 2>/dev/null; then echo "ABORT_SUDO_AUTH (aaden sudo 密码不符?)"; exit 1; fi
5
19
 
6
- cd
7
- cd files || exit
8
- cp -rf backendFiles/* /var/www/html/
20
+ # ---- composer.lock 门 ----
21
+ REMOTE_LOCK=$(curl -s "$S3/raw/PHP/composer.lock" --max-time 30)
22
+ LOCAL_LOCK=$(cat /var/www/html/PHP/composer.lock 2>/dev/null)
23
+ RH=$(printf '%s' "$REMOTE_LOCK" | shasum -a 256 | cut -d' ' -f1)
24
+ LH=$(printf '%s' "$LOCAL_LOCK" | shasum -a 256 | cut -d' ' -f1)
25
+ if [ -n "$REMOTE_LOCK" ] && [ "$RH" = "$LH" ]; then
26
+ ARTIFACT="code.zip"; echo "composer.lock 一致 → 轻量 code.zip"
27
+ else
28
+ ARTIFACT="latest.zip"; echo "composer.lock 变化(或取不到) → 全量 latest.zip"
29
+ fi
9
30
 
10
- cd /var/www/html/PHP || exit
11
- vendor/bin/phinx migrate
31
+ # ---- 下载 + 解压到绝对暂存 ----
32
+ sudoS "rm -rf $STAGE && mkdir -p $STAGE && chown $(id -un):$(id -gn) $STAGE"
33
+ # 用 curl 而非 axel:axel 在设备上是 snap 包,受限写不进 /var/tmp。
34
+ # code.zip 仅 ~1MB,curl 瞬间完成;latest.zip 全量走 curl 稍慢但那是罕见 fallback。
35
+ ZIP="$STAGE/$ARTIFACT"
36
+ curl -fsSL "$S3/$ARTIFACT" -o "$ZIP" --max-time 1200
37
+ if [ ! -s "$ZIP" ]; then echo "下载失败或为空: $ARTIFACT"; echo "ABORT_DOWNLOAD"; exit 1; fi
38
+ unzip -q -o "$ZIP" -d "$STAGE" && rm -f "$ZIP"
39
+
40
+ # ---- 关键:破坏任何东西之前,先确认源 PHP 代码确实在 ----
41
+ if [ ! -f "$STAGE/PHP/composer.lock" ] || [ ! -d "$STAGE/PHP" ]; then
42
+ echo "源包缺 PHP/ 或 composer.lock,中止(未改动任何文件)"
43
+ echo "ABORT_BAD_ARTIFACT"; exit 1
44
+ fi
45
+
46
+ # ---- TSE 门:绝不在有进行中的交易/TSE 签名时部署 ----
47
+ # 结账把 START/FINISH 塞进 TseExecuteQueue,后台 worker 签名后置 finished=1(出错也置 1)。
48
+ # 信号 = "未完成 且 已到期 且 新鲜(<5min)":
49
+ # · 开着的桌吃两小时 → 不阻塞(TSE 任务早签完置 1 了)——正是要避免的"活动单门过严"
50
+ # · 正在签/待签的交易 → 有 finished=0 行 → 跳过等下轮 ——正是不能打断的瞬间
51
+ # · 5min 上界 → worker 挂了留下的卡死行不会永久拦住更新
52
+ # 紧贴 cp 之前查,消掉"下载几秒期间开了新交易"的竞态。DB 凭据从 config.ini 读(全网 aaden/共享密码)。
53
+ CONF=/var/www/html/Resource/config/config.ini
54
+ DBCREDS=$(sudoS "php -r '\$c=@parse_ini_file(\"$CONF\",true);\$d=\$c[\"DB\"]??[];echo (\$d[\"host\"]??\"localhost\").\"|\".(\$d[\"user\"]??\"aaden\").\"|\".(\$d[\"password\"]??\"\");'" 2>/dev/null)
55
+ DB_HOST="${DBCREDS%%|*}"; _r="${DBCREDS#*|}"; DB_USER="${_r%%|*}"; DB_PASS="${_r#*|}"
56
+ if [ -n "$DB_USER" ] && [ -n "$DB_PASS" ]; then
57
+ DUE=$(MYSQL_PWD="$DB_PASS" mysql -u"$DB_USER" -h"${DB_HOST:-localhost}" -N -e \
58
+ "SELECT COUNT(*) FROM aaden.TseExecuteQueue WHERE finished=0 AND nextExecuteTime<=NOW() AND createdAt>=NOW()-INTERVAL 5 MINUTE;" 2>/dev/null)
59
+ else
60
+ DUE=""
61
+ fi
62
+ if [ -z "$DUE" ]; then
63
+ # 查不到 = 无法确认 TSE 状态 → 对财政数据保守跳过(不部署也不冒险打断签名)。
64
+ # 非破坏性:此刻还没动任何文件。版本停滞会被 fleet-rollout-status 监控发现。
65
+ echo "TSE 队列查询失败,保守跳过本轮(未改动任何文件)"; echo "SKIP_TSE_QUERY"; exit 0
66
+ fi
67
+ if [ "$DUE" -gt 0 ]; then
68
+ echo "有进行中的交易/TSE 签名 (due=$DUE),跳过本轮,等下个周期"; echo "SKIP_TSE_BUSY"; exit 0
69
+ fi
70
+ echo "TSE 队列空闲 (due=0),可安全部署"
71
+
72
+ # ---- 备份 PHP 代码(排除 vendor) ----
73
+ sudoS "rm -rf $BACKUP && mkdir -p $BACKUP && tar czf $BACKUP/php-code.tgz --exclude='PHP/vendor' -C /var/www/html PHP package.json 2>/dev/null"
74
+
75
+ # ---- 部署(源已校验,可安全 rm+cp)----
76
+ sudoS "rm -rf /var/www/html/PHP/DB/migrations/*"
77
+ if [ "$ARTIFACT" = "code.zip" ]; then
78
+ sudoS "cp -rf $STAGE/PHP/. /var/www/html/PHP/"
79
+ sudoS "curl -s '$S3/raw/package.json' -o /var/www/html/package.json --max-time 30"
80
+ else
81
+ sudoS "cp -rf $STAGE/. /var/www/html/"
82
+ fi
83
+ sudoS "chmod -R 777 /var/www/html/Resource; chmod +x /var/www/html/crontab.sh /var/www/html/PHP/vendor/bin/phinx 2>/dev/null; chown -hR www-data:www-data /var/www/html"
84
+
85
+ # ---- migrate ----
86
+ sudoS "cd /var/www/html/PHP && vendor/bin/phinx migrate" 2>&1 | tail -3
87
+
88
+ # ---- 健康检查 ----
89
+ HEALTHY=1
90
+ for ep in "Orders.php?op=getCurrentActiveOrders&lang=DE" "MyVersion.php?op=version"; do
91
+ c=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost/PHP/$ep" --max-time 15)
92
+ echo " 健康检查 $ep -> $c"; [ "$c" = "200" ] || HEALTHY=0
93
+ done
94
+
95
+ # ---- 回滚 or 成功 ----
96
+ if [ "$HEALTHY" != "1" ]; then
97
+ echo "健康检查失败 → 回滚"
98
+ sudoS "tar xzf $BACKUP/php-code.tgz -C /var/www/html && chown -hR www-data:www-data /var/www/html"
99
+ sudoS "rm -rf $STAGE"; echo "ROLLED_BACK"; exit 1
100
+ fi
101
+ sudoS "rm -rf $STAGE"
102
+ echo "DEPLOY_OK $(python3 -c "import json;print(json.load(open('/var/www/html/package.json'))['version'])" 2>/dev/null)"
@@ -8,4 +8,7 @@ sudo npm i aaden-cli -g
8
8
 
9
9
  aaden-cli patch
10
10
 
11
-
11
+ # 后端也走 aaden-cli 通道自愈:light-update 自带 版本门/灰度门/活动单门,
12
+ # 已最新则空跑,营业时间被 @reboot 触发也不会打断收银(不 snap refresh 前端、不 apt)。
13
+ # 这条让"后端自动更新"重新依赖活着的 npm+S3 管线,而不是失效的 git token。
14
+ aaden-cli light-update
package/dist/index.js CHANGED
@@ -18507,7 +18507,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
18507
18507
  /***/ ((module) => {
18508
18508
 
18509
18509
  "use strict";
18510
- module.exports = {"i8":"1.6.95"};
18510
+ module.exports = {"i8":"1.6.97"};
18511
18511
 
18512
18512
  /***/ })
18513
18513
 
@@ -18589,7 +18589,7 @@ const colors = __nccwpck_require__(3045)
18589
18589
  const fs = __nccwpck_require__(7147)
18590
18590
  const InnerKenTimeStampFormat = 'YYYYMMDDHHmmss'
18591
18591
  const prompts = __nccwpck_require__(9646)
18592
- const {deviceEcho, logAction} = __nccwpck_require__(7415)
18592
+ const {deviceEcho, logAction, getCurrentDeviceId} = __nccwpck_require__(7415)
18593
18593
  const GlobalDeviceId = 'notset'
18594
18594
 
18595
18595
  const {program} = __nccwpck_require__(1904)
@@ -18609,6 +18609,7 @@ console.log(
18609
18609
  program
18610
18610
  .option('-d, --debug', 'use Debug Out put')
18611
18611
  program.command('update [channel]').action(update)
18612
+ program.command('light-update').action(lightUpdate) // 轻量+带闸门的后端部署,供 auto-upgrade/patch 调用
18612
18613
  program.command('rollback').action(rollback)
18613
18614
  program
18614
18615
  .command('install <deviceId>')
@@ -19072,6 +19073,103 @@ async function update(channel = '') {
19072
19073
 
19073
19074
  }
19074
19075
 
19076
+ /**
19077
+ * 轻量、带闸门的后端部署 —— 专为「每日 auto-upgrade / 一次性 patch」自动调用而设计。
19078
+ *
19079
+ * 与 `update`(走 update.sh)的区别:
19080
+ * - **不 snap refresh aaden-vue-app**(不会重启收银前端)
19081
+ * - **不 apt install**
19082
+ * - 三道闸门保证「营业时间被触发也安全」:
19083
+ * ① 版本门:S3 版本 == 本机版本 → 跳过(已最新,重启也不白部署)
19084
+ * ② 灰度门:只部署到 cloud `deviceLog.maxVersion` 允许的版本(-1=不设限)
19085
+ * → 可先给少数设备设 maxVersion 做金丝雀,其余自动跳过
19086
+ * ③ TSE 门:有进行中的交易/TSE 签名 → 跳过(在 lightUpdate.sh 里查 TseExecuteQueue,
19087
+ * 紧贴 cp 之前,避免"下载期间开了新交易"的竞态;开桌吃饭不阻塞,只有正在签的交易阻塞)
19088
+ * - 结果(部署/跳过/失败)经 logAction 写入 cloud 事件日志,可在 deviceLog/eventList 观测
19089
+ */
19090
+ async function lightUpdate() {
19091
+ try {
19092
+ const localVersion = getBackendVersionOffline()
19093
+ const remoteVersion = (await hillo.get('https://aaden-backend.s3.eu-central-1.amazonaws.com/raw/package.json')).version
19094
+
19095
+ // ① 版本门:只在 S3 **严格更新** 时部署。
19096
+ // 不能只判 `===`:raw/package.json 可能比 latest.zip 晚几分钟才刷新,
19097
+ // 那个窗口里某台设备可能已经比 raw 标记的版本更新——只判相等会把它降级重铺。
19098
+ // 只往上升、绝不降级。
19099
+ if (!versionGt(remoteVersion, localVersion)) {
19100
+ console.log(('后端已是最新或更高 (本机 ' + localVersion + ' / S3 ' + remoteVersion + '),跳过 light-update').green)
19101
+ return
19102
+ }
19103
+
19104
+ // ② 灰度门(maxVersion 来自 cloud deviceLog;-1 或取不到 = 不设限,与既有 git 逻辑一致)
19105
+ const deviceId = await getCurrentDeviceId()
19106
+ const maxVersion = await getDeviceMaxVersion(deviceId)
19107
+ if (maxVersion && maxVersion !== '-1' && versionGt(remoteVersion, maxVersion)) {
19108
+ const msg = 'light-update 跳过:灰度闸门 remote=' + remoteVersion + ' > maxVersion=' + maxVersion
19109
+ console.log(msg.yellow)
19110
+ await logAction(msg, 0)
19111
+ return
19112
+ }
19113
+
19114
+ // ③ TSE 门已下移到 lightUpdate.sh:在 cp 之前查 TseExecuteQueue(finished=0 且到期且新鲜),
19115
+ // 有进行中的交易/TSE 签名就跳过。放在脚本里而非这里,是为了紧贴破坏性 cp、
19116
+ // 消掉"这里查过没单 → 下载几秒 → 期间开了新交易 → cp 打断签名"的竞态窗口。
19117
+
19118
+ // lightUpdate.sh 自己完成:TSE 门 + composer.lock 门(选 code.zip 或 latest.zip) + 下载 + 备份
19119
+ // + 部署 + migrate + 健康检查/失败回滚。退出码:0=成功,1=已回滚。
19120
+ const command = [
19121
+ ...getPrivilegeCMD('AadenDeploy/lightUpdate.sh'),
19122
+ 'cd AadenDeploy',
19123
+ './lightUpdate.sh'
19124
+ ]
19125
+ const {code} = await cli.executeInShell(command, program.debug)
19126
+
19127
+ const newVersion = readBackendVersionFresh()
19128
+ if (code !== 0) {
19129
+ await logAction('light-update 失败/已回滚 (仍 ' + newVersion + ')', code || 1)
19130
+ } else if (newVersion !== localVersion) {
19131
+ await logAction('light-update 完成 ' + localVersion + ' -> ' + newVersion, 0)
19132
+ } else {
19133
+ await logAction('light-update 无实际变化 (' + newVersion + ')', 0)
19134
+ }
19135
+ } catch (e) {
19136
+ console.error(e)
19137
+ try { await logAction('light-update 异常: ' + (e.message || e), 1) } catch (_) {}
19138
+ }
19139
+ }
19140
+
19141
+ /** 比较点分数字版本,a > b 返回 true。'1.9.61' > '1.7.908' === true。 */
19142
+ function versionGt(a, b) {
19143
+ const pa = String(a).split('.').map(n => parseInt(n) || 0)
19144
+ const pb = String(b).split('.').map(n => parseInt(n) || 0)
19145
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
19146
+ const x = pa[i] || 0, y = pb[i] || 0
19147
+ if (x > y) return true
19148
+ if (x < y) return false
19149
+ }
19150
+ return false
19151
+ }
19152
+
19153
+ /** 从 cloud deviceLog 取本机版本天花板;取不到按不设限(-1),与 UpdateSelf 的 getDeviceLog 一致。 */
19154
+ async function getDeviceMaxVersion(deviceId) {
19155
+ try {
19156
+ const res = await hillo.get('https://cloud-v2.aaden.io/deviceLog/get/' + deviceId)
19157
+ return (res && res.maxVersion) ? res.maxVersion : '-1'
19158
+ } catch (e) {
19159
+ console.log('取 maxVersion 失败,按不设限处理:', e.message)
19160
+ return '-1'
19161
+ }
19162
+ }
19163
+
19164
+ /** 直接读文件、绕过 require 缓存 —— 部署后读到的必须是新版本号,不能是进程启动时缓存的旧值。 */
19165
+ function readBackendVersionFresh() {
19166
+ try {
19167
+ return JSON.parse(fs.readFileSync('/var/www/html/package.json', 'utf8')).version
19168
+ } catch (e) {
19169
+ return '0.0.0'
19170
+ }
19171
+ }
19172
+
19075
19173
  async function install(deviceId) {
19076
19174
  const channel = 'stable'
19077
19175
  const installApp = true
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ # 一次性修复:因 GitHub token 失效,git 后端自动更新已停摆,几百台设备卡在旧版本。
3
+ # 改走活着的 S3 通道把当前 stable 铺下来。light-update 幂等且带三道闸门,重复跑安全。
4
+ aaden-cli light-update
package/index.js CHANGED
@@ -8,7 +8,7 @@ const colors = require('colors')
8
8
  const fs = require('fs')
9
9
  const InnerKenTimeStampFormat = 'YYYYMMDDHHmmss'
10
10
  const prompts = require('prompts')
11
- const {deviceEcho, logAction} = require('./modules/firebase')
11
+ const {deviceEcho, logAction, getCurrentDeviceId} = require('./modules/firebase')
12
12
  const GlobalDeviceId = 'notset'
13
13
 
14
14
  const {program} = require('commander')
@@ -28,6 +28,7 @@ console.log(
28
28
  program
29
29
  .option('-d, --debug', 'use Debug Out put')
30
30
  program.command('update [channel]').action(update)
31
+ program.command('light-update').action(lightUpdate) // 轻量+带闸门的后端部署,供 auto-upgrade/patch 调用
31
32
  program.command('rollback').action(rollback)
32
33
  program
33
34
  .command('install <deviceId>')
@@ -491,6 +492,103 @@ async function update(channel = '') {
491
492
 
492
493
  }
493
494
 
495
+ /**
496
+ * 轻量、带闸门的后端部署 —— 专为「每日 auto-upgrade / 一次性 patch」自动调用而设计。
497
+ *
498
+ * 与 `update`(走 update.sh)的区别:
499
+ * - **不 snap refresh aaden-vue-app**(不会重启收银前端)
500
+ * - **不 apt install**
501
+ * - 三道闸门保证「营业时间被触发也安全」:
502
+ * ① 版本门:S3 版本 == 本机版本 → 跳过(已最新,重启也不白部署)
503
+ * ② 灰度门:只部署到 cloud `deviceLog.maxVersion` 允许的版本(-1=不设限)
504
+ * → 可先给少数设备设 maxVersion 做金丝雀,其余自动跳过
505
+ * ③ TSE 门:有进行中的交易/TSE 签名 → 跳过(在 lightUpdate.sh 里查 TseExecuteQueue,
506
+ * 紧贴 cp 之前,避免"下载期间开了新交易"的竞态;开桌吃饭不阻塞,只有正在签的交易阻塞)
507
+ * - 结果(部署/跳过/失败)经 logAction 写入 cloud 事件日志,可在 deviceLog/eventList 观测
508
+ */
509
+ async function lightUpdate() {
510
+ try {
511
+ const localVersion = getBackendVersionOffline()
512
+ const remoteVersion = (await hillo.get('https://aaden-backend.s3.eu-central-1.amazonaws.com/raw/package.json')).version
513
+
514
+ // ① 版本门:只在 S3 **严格更新** 时部署。
515
+ // 不能只判 `===`:raw/package.json 可能比 latest.zip 晚几分钟才刷新,
516
+ // 那个窗口里某台设备可能已经比 raw 标记的版本更新——只判相等会把它降级重铺。
517
+ // 只往上升、绝不降级。
518
+ if (!versionGt(remoteVersion, localVersion)) {
519
+ console.log(('后端已是最新或更高 (本机 ' + localVersion + ' / S3 ' + remoteVersion + '),跳过 light-update').green)
520
+ return
521
+ }
522
+
523
+ // ② 灰度门(maxVersion 来自 cloud deviceLog;-1 或取不到 = 不设限,与既有 git 逻辑一致)
524
+ const deviceId = await getCurrentDeviceId()
525
+ const maxVersion = await getDeviceMaxVersion(deviceId)
526
+ if (maxVersion && maxVersion !== '-1' && versionGt(remoteVersion, maxVersion)) {
527
+ const msg = 'light-update 跳过:灰度闸门 remote=' + remoteVersion + ' > maxVersion=' + maxVersion
528
+ console.log(msg.yellow)
529
+ await logAction(msg, 0)
530
+ return
531
+ }
532
+
533
+ // ③ TSE 门已下移到 lightUpdate.sh:在 cp 之前查 TseExecuteQueue(finished=0 且到期且新鲜),
534
+ // 有进行中的交易/TSE 签名就跳过。放在脚本里而非这里,是为了紧贴破坏性 cp、
535
+ // 消掉"这里查过没单 → 下载几秒 → 期间开了新交易 → cp 打断签名"的竞态窗口。
536
+
537
+ // lightUpdate.sh 自己完成:TSE 门 + composer.lock 门(选 code.zip 或 latest.zip) + 下载 + 备份
538
+ // + 部署 + migrate + 健康检查/失败回滚。退出码:0=成功,1=已回滚。
539
+ const command = [
540
+ ...getPrivilegeCMD('AadenDeploy/lightUpdate.sh'),
541
+ 'cd AadenDeploy',
542
+ './lightUpdate.sh'
543
+ ]
544
+ const {code} = await cli.executeInShell(command, program.debug)
545
+
546
+ const newVersion = readBackendVersionFresh()
547
+ if (code !== 0) {
548
+ await logAction('light-update 失败/已回滚 (仍 ' + newVersion + ')', code || 1)
549
+ } else if (newVersion !== localVersion) {
550
+ await logAction('light-update 完成 ' + localVersion + ' -> ' + newVersion, 0)
551
+ } else {
552
+ await logAction('light-update 无实际变化 (' + newVersion + ')', 0)
553
+ }
554
+ } catch (e) {
555
+ console.error(e)
556
+ try { await logAction('light-update 异常: ' + (e.message || e), 1) } catch (_) {}
557
+ }
558
+ }
559
+
560
+ /** 比较点分数字版本,a > b 返回 true。'1.9.61' > '1.7.908' === true。 */
561
+ function versionGt(a, b) {
562
+ const pa = String(a).split('.').map(n => parseInt(n) || 0)
563
+ const pb = String(b).split('.').map(n => parseInt(n) || 0)
564
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
565
+ const x = pa[i] || 0, y = pb[i] || 0
566
+ if (x > y) return true
567
+ if (x < y) return false
568
+ }
569
+ return false
570
+ }
571
+
572
+ /** 从 cloud deviceLog 取本机版本天花板;取不到按不设限(-1),与 UpdateSelf 的 getDeviceLog 一致。 */
573
+ async function getDeviceMaxVersion(deviceId) {
574
+ try {
575
+ const res = await hillo.get('https://cloud-v2.aaden.io/deviceLog/get/' + deviceId)
576
+ return (res && res.maxVersion) ? res.maxVersion : '-1'
577
+ } catch (e) {
578
+ console.log('取 maxVersion 失败,按不设限处理:', e.message)
579
+ return '-1'
580
+ }
581
+ }
582
+
583
+ /** 直接读文件、绕过 require 缓存 —— 部署后读到的必须是新版本号,不能是进程启动时缓存的旧值。 */
584
+ function readBackendVersionFresh() {
585
+ try {
586
+ return JSON.parse(fs.readFileSync('/var/www/html/package.json', 'utf8')).version
587
+ } catch (e) {
588
+ return '0.0.0'
589
+ }
590
+ }
591
+
494
592
  async function install(deviceId) {
495
593
  const channel = 'stable'
496
594
  const installApp = true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aaden-cli",
3
- "version": "1.6.95",
3
+ "version": "1.6.97",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ # 一次性修复:因 GitHub token 失效,git 后端自动更新已停摆,几百台设备卡在旧版本。
3
+ # 改走活着的 S3 通道把当前 stable 铺下来。light-update 幂等且带三道闸门,重复跑安全。
4
+ aaden-cli light-update
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ """车队后端版本滚动进度表。数据源:cloud-v2 的 deviceLog/list(每台每 5 分钟上报)。
3
+
4
+ 用法:
5
+ python3 scripts/fleet-rollout-status.py 1.9.62 # 看向 1.9.62 铺开的进度
6
+ python3 scripts/fleet-rollout-status.py 1.9.62 --behind # additionally 列出落后设备
7
+ python3 scripts/fleet-rollout-status.py # 不给目标版本则只看分布
8
+ """
9
+ import sys, json, time, urllib.request
10
+ from collections import Counter
11
+
12
+ LIST_URL = "https://cloud-v2.aaden.io/deviceLog/list"
13
+ STALE_MIN = 30 # 超过这么多分钟没上报,视为离线/失联
14
+
15
+ def fetch():
16
+ with urllib.request.urlopen(LIST_URL, timeout=60) as r:
17
+ return json.load(r)
18
+
19
+ def main():
20
+ target = next((a for a in sys.argv[1:] if not a.startswith('-')), None)
21
+ show_behind = '--behind' in sys.argv
22
+ d = fetch()
23
+ now = int(time.time() * 1000)
24
+ total = len(d)
25
+ dist = Counter(x.get('backendVersion', '?') for x in d)
26
+ stale = [x for x in d if now - int(x.get('timestamp', 0) or 0) > STALE_MIN * 60 * 1000]
27
+
28
+ print(f"车队共 {total} 台 (数据源 deviceLog/list,每台每 5 分钟上报)")
29
+ print(f"失联(>{STALE_MIN}分钟无上报): {len(stale)} 台")
30
+ print()
31
+ if target:
32
+ on = [x for x in d if x.get('backendVersion') == target]
33
+ behind = [x for x in d if x.get('backendVersion') != target]
34
+ pct = len(on) * 100.0 / total if total else 0
35
+ bar = '#' * int(pct // 2.5) + '.' * (40 - int(pct // 2.5))
36
+ print(f"目标后端版本: {target}")
37
+ print(f" 已到位 [{bar}] {len(on)}/{total} {pct:.1f}%")
38
+ print(f" 未到位 {len(behind)} 台")
39
+ print()
40
+ print("未到位版本分布:")
41
+ for v, n in Counter(x.get('backendVersion', '?') for x in behind).most_common():
42
+ print(f" {v:>10} {n} 台")
43
+ if show_behind:
44
+ print()
45
+ print("未到位设备(deviceId / 版本 / 名称首行):")
46
+ for x in sorted(behind, key=lambda x: x.get('deviceId', '')):
47
+ name = (x.get('deviceName') or '').split('\n')[0][:32]
48
+ print(f" {str(x.get('deviceId','?')):>6} {x.get('backendVersion','?'):>10} {name}")
49
+ else:
50
+ print("后端版本分布:")
51
+ for v, n in dist.most_common():
52
+ print(f" {v:>10} {n} 台")
53
+
54
+ if __name__ == '__main__':
55
+ main()