aaden-cli 1.6.91 → 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.
@@ -0,0 +1,84 @@
1
+ #!/bin/bash
2
+
3
+ # 此脚本为指定的网卡设置静态 IP
4
+ # IP: 192.168.121.20/24
5
+ # 网关: 192.168.121.1
6
+ # DNS: 192.168.121.1
7
+ # 在 /etc/netplan/ 下新增文件 30-cotti.yaml 并应用配置
8
+ # 需要 root 权限运行(推荐使用 sudo)
9
+
10
+ set -e # 遇到错误立即退出
11
+
12
+ # 查找所有非 lo 的以太网接口
13
+ mapfile -t INTERFACES < <(ip -o link show | awk -F': ' '/state UP|state DOWN/ && $2 !~ /lo/ {print $2}')
14
+
15
+ if [ ${#INTERFACES[@]} -eq 0 ]; then
16
+ echo "错误:未找到任何可用网卡!"
17
+ exit 1
18
+ fi
19
+
20
+ # 优先尝试自动选择以 enp 开头的网卡
21
+ INTERFACE=""
22
+ for iface in "${INTERFACES[@]}"; do
23
+ if [[ $iface == enp* ]]; then
24
+ INTERFACE="$iface"
25
+ break
26
+ fi
27
+ done
28
+
29
+ # 如果没有找到 enp 开头的网卡,或者用户想手动选择,则进入手动选择流程
30
+ if [ -z "$INTERFACE" ]; then
31
+ echo "未找到以 enp 开头的网卡,下面列出所有可用网卡:"
32
+ for i in "${!INTERFACES[@]}"; do
33
+ idx=$((i+1))
34
+ echo "$idx) ${INTERFACES[i]}"
35
+ done
36
+
37
+ if [ ${#INTERFACES[@]} -eq 1 ]; then
38
+ echo "只检测到一个网卡,将自动选择:${INTERFACES[0]}"
39
+ INTERFACE="${INTERFACES[0]}"
40
+ else
41
+ while true; do
42
+ echo -n "请输入要配置的网卡编号(1-${#INTERFACES[@]}):"
43
+ read choice
44
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#INTERFACES[@]} ]; then
45
+ INTERFACE="${INTERFACES[$((choice-1))]}"
46
+ break
47
+ else
48
+ echo "输入无效,请输入有效的编号!"
49
+ fi
50
+ done
51
+ fi
52
+ else
53
+ echo "自动选择网卡:$INTERFACE"
54
+ fi
55
+
56
+ echo "将为网卡 $INTERFACE 配置静态 IP 192.168.121.20/24"
57
+
58
+ CONFIG_FILE="/etc/netplan/30-cotti.yaml"
59
+
60
+ # 使用 cat 和 heredoc 写入 YAML 文件(注意缩进严格要求)
61
+ sudo bash -c "cat > $CONFIG_FILE" << EOF
62
+ network:
63
+ version: 2
64
+ renderer: NetworkManager
65
+ ethernets:
66
+ $INTERFACE:
67
+ dhcp4: false
68
+ addresses:
69
+ - 192.168.121.20/24
70
+ gateway4: 192.168.121.1
71
+ nameservers:
72
+ addresses:
73
+ - 192.168.121.1
74
+ EOF
75
+
76
+ echo "已生成配置文件:$CONFIG_FILE"
77
+ echo "内容如下:"
78
+ sudo cat $CONFIG_FILE
79
+
80
+ # 应用配置
81
+ echo "正在应用配置..."
82
+ sudo netplan apply
83
+
84
+ echo "配置完成!请使用 ip addr show $INTERFACE 检查 IP 是否已生效。"
File without changes
@@ -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)"
File without changes
File without changes
@@ -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.91"};
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>')
@@ -18630,6 +18631,7 @@ program.command('update-tse').action(updateTse)
18630
18631
  program.command('update-win-tse').action(updateWindowsTse)
18631
18632
  program.command('check-version').action(getBackendVersionOffline)
18632
18633
  program.command('full-update').action(fullUpdate)
18634
+ program.command('cotti-netplan').action(cottiNetplan)
18633
18635
 
18634
18636
  program.command('db').action(enterDatabase)
18635
18637
  program.command('printer-test').action(testPrinter)
@@ -18843,7 +18845,8 @@ async function fullUpdate() {
18843
18845
  'sudo service apache2 restart',
18844
18846
  'aaden-cli fix-cronjob',
18845
18847
  'aaden-cli auto-write-gdpdu',
18846
- 'sudo systemctl daemon-reload'
18848
+ 'sudo systemctl daemon-reload',
18849
+ 'aaden-cli ensure-print-log-rotate',
18847
18850
  ]
18848
18851
  const {code} = await cli.executeInShell(command, program.debug)
18849
18852
  await logAction("进行了full-update", code)
@@ -19070,6 +19073,103 @@ async function update(channel = '') {
19070
19073
 
19071
19074
  }
19072
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
+
19073
19173
  async function install(deviceId) {
19074
19174
  const channel = 'stable'
19075
19175
  const installApp = true
@@ -19335,6 +19435,11 @@ function ensurePrintLogRotate() {
19335
19435
  }
19336
19436
  }
19337
19437
 
19438
+ async function cottiNetplan() {
19439
+ await run('AadenDeploy/Utils/cottiNetplan.sh')
19440
+ logAction("手动设置cotti netplan")
19441
+ }
19442
+
19338
19443
  })();
19339
19444
 
19340
19445
  module.exports = __webpack_exports__;
@@ -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>')
@@ -49,6 +50,7 @@ program.command('update-tse').action(updateTse)
49
50
  program.command('update-win-tse').action(updateWindowsTse)
50
51
  program.command('check-version').action(getBackendVersionOffline)
51
52
  program.command('full-update').action(fullUpdate)
53
+ program.command('cotti-netplan').action(cottiNetplan)
52
54
 
53
55
  program.command('db').action(enterDatabase)
54
56
  program.command('printer-test').action(testPrinter)
@@ -262,7 +264,8 @@ async function fullUpdate() {
262
264
  'sudo service apache2 restart',
263
265
  'aaden-cli fix-cronjob',
264
266
  'aaden-cli auto-write-gdpdu',
265
- 'sudo systemctl daemon-reload'
267
+ 'sudo systemctl daemon-reload',
268
+ 'aaden-cli ensure-print-log-rotate',
266
269
  ]
267
270
  const {code} = await cli.executeInShell(command, program.debug)
268
271
  await logAction("进行了full-update", code)
@@ -489,6 +492,103 @@ async function update(channel = '') {
489
492
 
490
493
  }
491
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
+
492
592
  async function install(deviceId) {
493
593
  const channel = 'stable'
494
594
  const installApp = true
@@ -753,3 +853,8 @@ function ensurePrintLogRotate() {
753
853
  cli.executeInShell(command, program.debug)
754
854
  }
755
855
  }
856
+
857
+ async function cottiNetplan() {
858
+ await run('AadenDeploy/Utils/cottiNetplan.sh')
859
+ logAction("手动设置cotti netplan")
860
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aaden-cli",
3
- "version": "1.6.91",
3
+ "version": "1.6.97",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -8,11 +8,11 @@
8
8
  "update": "git add . && release-it --ci"
9
9
  },
10
10
  "bin": {
11
- "aaden-cli": "./dist/index.js"
11
+ "aaden-cli": "dist/index.js"
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "https://github.com/innerken-gmbh/aaden-cli.git"
15
+ "url": "git+https://github.com/innerken-gmbh/aaden-cli.git"
16
16
  },
17
17
  "keywords": [],
18
18
  "author": "JuHaodong@innerken",
@@ -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()