aaden-cli 1.6.118 → 1.6.121-rc.0

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.
@@ -32,6 +32,8 @@ DEVICE_ID="$1"
32
32
  if [ -z "$DEVICE_ID" ]; then
33
33
  read -r -p '设备号: ' DEVICE_ID
34
34
  fi
35
+ # 测试门店号段先拦(不用联网),再查云端撞号
36
+ source "$DIR/farmRange.sh" && farm_range_guard "$DEVICE_ID" || exit 1
35
37
  bash "$DIR/checkDeviceId.sh" "$DEVICE_ID" || exit 1
36
38
  DEVICE_ID=$((10#$DEVICE_ID))
37
39
  export AADEN_SKIP_DEVICE_CHECK=1 # 后面 set-device-id 不再问第二遍
@@ -4,6 +4,10 @@
4
4
  #
5
5
  # light-update 只发 PHP/,AppCenter 以前只能跟 465MB 的 latest.zip 走;这里单独按应用同步。
6
6
  # - 灰度门:manifest 的 rollout = {"devices":[...], "percent":N},本机在名单里或 deviceId%100 < N 才动;没有 rollout = 全量
7
+ # - 根目录应用:manifest 顶层 rootApps 里的应用装到 docroot 根(/var/www/html/<App>,如老的 /App),其余逻辑同 apps;
8
+ # 只认白名单 APPCENTER_ROOT_APPS(默认 "App"),防止 manifest 写错把 PHP 之类的目录换掉。老 CLI 只读 apps,不受影响
9
+ # - 按应用灰度:manifest 带 perAppRollout=true 时,每个应用按自己的 apps.<App>.rollout 判断(没有 = 全量),
10
+ # 顶层 rollout 只管 ensureNoCache;老版本 CLI 不认 perAppRollout,照旧整体按顶层 rollout
7
11
  # - ensureNoCache:manifest 打开时先确保 AppCenter 的 index.html 带 no-cache(ensureAppCenterNoCache.sh)
8
12
  # - manifest 门:本机 .appcenter-manifest 的 sha256 == manifest 的 treeSha256 且 `sha256sum -c` 通过 → 什么都不做,
9
13
  # 每天只花一次几 KB 的请求。被 latest.zip 全量覆盖过、文件对不上 → 校验失败 → 重新铺一遍(自愈)
@@ -16,15 +20,18 @@
16
20
  # 用法: appCenterSync.sh [channel]
17
21
  # APPCENTER_BASE 产物根地址,默认 S3;测试可用 file:///var/tmp/...(curl 都支持)
18
22
  # APPCENTER_APPS 只同步这些应用(空格分隔);不传 = manifest 里列出的全部应用,发哪些由发布端决定
23
+ # APPCENTER_ROOT_APPS 允许同步到 docroot 根的应用白名单(空格分隔),默认 "App"
19
24
  # APPCENTER_DEVICE_ID 本机 deviceId(CLI 会传);不传就问本机 PHP
20
25
  # APPCENTER_RESULT 结果行另存的文件(CLI 会传)
21
26
  # 结果行(同时写进 $RESULT 给 CLI 上报):APP_OK <App> <sha12> | APP_UP_TO_DATE <App> | APP_SKIP <App> <原因> |
22
- # APP_ROLLED_BACK <App> | APP_ABORT_* <App> | NOCACHE_* | SKIP_ROLLOUT | SKIP_DEPS | SKIP_LOCKED | ABORT_*
27
+ # APP_ROLLED_BACK <App> | APP_ABORT_* <App> | APP_SKIP_ROLLOUT <App> | NOCACHE_* | SKIP_ROLLOUT | SKIP_DEPS | SKIP_LOCKED | ABORT_*
23
28
  # 退出码:0=全部成功、无需更新或不在灰度内;1=有失败(失败的已回滚或未动线上目录)
24
29
  set -uo pipefail
25
30
  BASE="${APPCENTER_BASE:-https://aaden-backend.s3.eu-central-1.amazonaws.com/appcenter}"
26
31
  CHANNEL="${1:-stable}"
27
- WEB=/var/www/html/AppCenter
32
+ DOCROOT=/var/www/html
33
+ WEB=$DOCROOT/AppCenter
34
+ ROOT_ALLOW=" ${APPCENTER_ROOT_APPS:-App} "
28
35
  STAGE=/var/tmp/aaden-appcenter-stage
29
36
  BACKUP=/var/lib/aaden-appcenter-backup
30
37
  LOCK=/var/tmp/aaden-appcenter.lock
@@ -48,21 +55,31 @@ flock -n 9 || { res "SKIP_LOCKED"; exit 0; }
48
55
 
49
56
  MANIFEST=$(curl -fsSL --max-time 30 "$BASE/$CHANNEL/manifest.json") \
50
57
  || { echo "取不到 $BASE/$CHANNEL/manifest.json"; res "ABORT_MANIFEST"; exit 1; }
51
- # 读 manifest:mf apps.Admin.zip / mf ensureNoCache / mf eligible <deviceId>
58
+ # 读 manifest:mf apps.Admin.zip / mf ensureNoCache / mf eligible <deviceId> / mf eligible_app <App> <deviceId> [apps|rootApps]
52
59
  mf(){
53
60
  printf '%s' "$MANIFEST" | python3 -c '
54
61
  import json, sys
55
62
  m = json.load(sys.stdin)
56
- if sys.argv[1] == "eligible":
57
- r = m.get("rollout")
63
+ def gate(r, dev):
58
64
  if r is None:
59
- print("yes"); sys.exit()
65
+ return "yes"
60
66
  try:
61
- d = int(sys.argv[2])
67
+ d = int(dev)
62
68
  except ValueError:
63
- print("no"); sys.exit()
69
+ return "no"
64
70
  ok = d in [int(x) for x in r.get("devices", [])] or d % 100 < int(r.get("percent", 0))
65
- print("yes" if ok else "no"); sys.exit()
71
+ return "yes" if ok else "no"
72
+ if sys.argv[1] == "eligible":
73
+ print(gate(m.get("rollout"), sys.argv[2])); sys.exit()
74
+ if sys.argv[1] == "eligible_app":
75
+ # eligible_app <App> <deviceId> [apps|rootApps]
76
+ kind = sys.argv[4] if len(sys.argv) > 4 else "apps"
77
+ if m.get("perAppRollout") is True:
78
+ app = (m.get(kind) or {}).get(sys.argv[2]) or {}
79
+ print(gate(app.get("rollout"), sys.argv[3]))
80
+ else:
81
+ print(gate(m.get("rollout"), sys.argv[3]))
82
+ sys.exit()
66
83
  v = m
67
84
  for k in sys.argv[1].split("."):
68
85
  v = v.get(k) if isinstance(v, dict) else None
@@ -76,13 +93,14 @@ if [ -z "$DEVICE_ID" ] || [ "$DEVICE_ID" = "-1" ]; then
76
93
  DEVICE_ID=$(curl -s --max-time 10 "http://localhost/PHP/AccessLog.php?op=deviceId" \
77
94
  | python3 -c 'import json,sys; print(json.load(sys.stdin).get("content",""))' 2>/dev/null)
78
95
  fi
79
- if [ "$(mf eligible "${DEVICE_ID:-x}")" != "yes" ]; then
96
+ TOP_ELIGIBLE=$(mf eligible "${DEVICE_ID:-x}")
97
+ if [ "$TOP_ELIGIBLE" != "yes" ] && [ "$(mf perAppRollout)" != "true" ]; then
80
98
  res "SKIP_ROLLOUT device=${DEVICE_ID:-?}"; exit 0
81
99
  fi
82
100
 
83
101
  FAILED=0
84
102
  # ---- index.html no-cache(先于换前端:让之后拿到的 index.html 都不再被启发式缓存)----
85
- if [ "$(mf ensureNoCache)" = "true" ]; then
103
+ if [ "$TOP_ELIGIBLE" = "yes" ] && [ "$(mf ensureNoCache)" = "true" ]; then
86
104
  NC=$(bash "$HERE/ensureAppCenterNoCache.sh"); NC_CODE=$?
87
105
  printf '%s\n' "$NC" | sed '$d'
88
106
  res "$(printf '%s\n' "$NC" | tail -1)"
@@ -98,25 +116,32 @@ tree_ok(){ # $1=目录 $2=期望 treeSha256
98
116
  }
99
117
 
100
118
  sync_app(){
101
- local APP="$1" LIVE="$WEB/$1" NEW="$STAGE/$1" TREE ZIP ZSHA
102
- TREE=$(mf "apps.$APP.treeSha256"); ZIP=$(mf "apps.$APP.zip"); ZSHA=$(mf "apps.$APP.zipSha256")
103
- if [ -z "$TREE" ]; then res "APP_SKIP $APP 不在manifest里"; return 0; fi
119
+ # $1=应用名 $2=apps(AppCenter/<App>,默认)或 rootApps(docroot 根 /<App>)
120
+ local APP="$1" KIND="${2:-apps}" LIVE NEW BAK URLP TAG TREE ZIP ZSHA
121
+ if [ "$KIND" = "rootApps" ]; then
122
+ LIVE="$DOCROOT/$APP"; NEW="$STAGE/root-$APP"; BAK="root-$APP"; URLP="$APP"; TAG="/$APP"
123
+ else
124
+ LIVE="$WEB/$APP"; NEW="$STAGE/$APP"; BAK="$APP"; URLP="AppCenter/$APP"; TAG="$APP"
125
+ fi
126
+ TREE=$(mf "$KIND.$APP.treeSha256"); ZIP=$(mf "$KIND.$APP.zip"); ZSHA=$(mf "$KIND.$APP.zipSha256")
127
+ if [ -z "$TREE" ]; then res "APP_SKIP $TAG 不在manifest里"; return 0; fi
128
+ if [ "$(mf eligible_app "$APP" "${DEVICE_ID:-x}" "$KIND")" != "yes" ]; then res "APP_SKIP_ROLLOUT $TAG"; return 0; fi
104
129
  # manifest 来自网络:只接受预期格式,防止路径穿越
105
130
  if ! [[ "$TREE" =~ ^[0-9a-f]{64}$ && "$ZSHA" =~ ^[0-9a-f]{64}$ && "$ZIP" =~ ^blobs/[A-Za-z0-9._-]+\.zip$ ]]; then
106
- res "APP_ABORT_BAD_MANIFEST $APP"; return 1
131
+ res "APP_ABORT_BAD_MANIFEST $TAG"; return 1
107
132
  fi
108
133
 
109
134
  # ---- manifest 门 ----
110
- if tree_ok "$LIVE" "$TREE"; then res "APP_UP_TO_DATE $APP"; return 0; fi
111
- echo "$APP: 本机与 manifest 不一致 → 同步到 ${TREE:0:12}"
135
+ if tree_ok "$LIVE" "$TREE"; then res "APP_UP_TO_DATE $TAG"; return 0; fi
136
+ echo "$TAG: 本机与 manifest 不一致 → 同步到 ${TREE:0:12}"
112
137
 
113
138
  # ---- 下载 + 校验(此时还没动线上目录)----
114
139
  sudoS "rm -rf '$NEW' '$NEW.zip' '$NEW.failed'"
115
- curl -fsSL --max-time 1200 "$BASE/$ZIP" -o "$NEW.zip" || { res "APP_ABORT_DOWNLOAD $APP"; return 1; }
116
- [ "$(sha256sum "$NEW.zip" | cut -d' ' -f1)" = "$ZSHA" ] || { res "APP_ABORT_CHECKSUM $APP"; rm -f "$NEW.zip"; return 1; }
140
+ curl -fsSL --max-time 1200 "$BASE/$ZIP" -o "$NEW.zip" || { res "APP_ABORT_DOWNLOAD $TAG"; return 1; }
141
+ [ "$(sha256sum "$NEW.zip" | cut -d' ' -f1)" = "$ZSHA" ] || { res "APP_ABORT_CHECKSUM $TAG"; rm -f "$NEW.zip"; return 1; }
117
142
  mkdir -p "$NEW" && unzip -q -o "$NEW.zip" -d "$NEW"; rm -f "$NEW.zip"
118
143
  if ! { [ -f "$NEW/index.html" ] && tree_ok "$NEW" "$TREE"; }; then
119
- res "APP_ABORT_BAD_ARTIFACT $APP"; rm -rf "$NEW"; return 1
144
+ res "APP_ABORT_BAD_ARTIFACT $TAG"; rm -rf "$NEW"; return 1
120
145
  fi
121
146
  # zip 里带的是打包时的旧 mtime;浏览器按 Last-Modified 的年龄启发式缓存,所以改成现在(只改 mtime 不改内容,不影响校验)
122
147
  touch "$NEW/index.html"
@@ -125,7 +150,7 @@ sync_app(){
125
150
  local CARRIED=0
126
151
  if [ -d "$LIVE" ]; then
127
152
  python3 "$HERE/appcenterRefs.py" "$LIVE" | LC_ALL=C sort > "$NEW.old" \
128
- || { res "APP_ABORT_REFS $APP"; rm -rf "$NEW" "$NEW.old"; return 1; }
153
+ || { res "APP_ABORT_REFS $TAG"; rm -rf "$NEW" "$NEW.old"; return 1; }
129
154
  paths "$NEW/.appcenter-manifest" > "$NEW.new"
130
155
  LC_ALL=C comm -23 "$NEW.old" "$NEW.new" > "$NEW.carry"
131
156
  CARRIED=$(wc -l < "$NEW.carry" | tr -d ' ')
@@ -133,34 +158,42 @@ sync_app(){
133
158
  [ "$CARRIED" -gt 0 ] && tar -C "$LIVE" -cf - -T "$NEW.carry" | tar -C "$NEW" -xf -
134
159
  rm -f "$NEW.old" "$NEW.new" "$NEW.carry"
135
160
  fi
136
- echo "$APP: 保留上一代文件 $CARRIED 个"
161
+ echo "$TAG: 保留上一代文件 $CARRIED 个"
137
162
 
138
163
  # ---- 替换:两次 rename,中间只有毫秒级空窗 ----
139
- sudoS "mkdir -p '$BACKUP' && rm -rf '$BACKUP/$APP' && chown -R www-data:www-data '$NEW'"
140
- [ -d "$LIVE" ] && sudoS "mv '$LIVE' '$BACKUP/$APP'"
164
+ sudoS "mkdir -p '$BACKUP' && rm -rf '$BACKUP/$BAK' && chown -R www-data:www-data '$NEW'"
165
+ [ -d "$LIVE" ] && sudoS "mv '$LIVE' '$BACKUP/$BAK'"
141
166
  sudoS "mv '$NEW' '$LIVE'"
142
167
 
143
168
  # ---- 健康检查:首页、入口 js、文件校验 ----
144
169
  local HEALTHY=1 MAIN c
145
170
  MAIN=$(grep -oE 'src=["'"'"']?[^"'"'"' >]+\.js' "$LIVE/index.html" | grep -vE '//' | head -1 | sed -E 's/^src=["'"'"']?(\.\/)?//')
146
171
  for p in "" "$MAIN"; do
147
- c=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost/AppCenter/$APP/$p" --max-time 15)
148
- echo " 健康检查 /AppCenter/$APP/$p -> $c"; [ "$c" = "200" ] || HEALTHY=0
172
+ c=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost/$URLP/$p" --max-time 15)
173
+ echo " 健康检查 /$URLP/$p -> $c"; [ "$c" = "200" ] || HEALTHY=0
149
174
  done
150
175
  tree_ok "$LIVE" "$TREE" || { echo " 线上目录校验失败"; HEALTHY=0; }
151
176
 
152
177
  if [ "$HEALTHY" != "1" ]; then
153
- sudoS "mv '$LIVE' '$NEW.failed'; [ -d '$BACKUP/$APP' ] && mv '$BACKUP/$APP' '$LIVE'"
154
- res "APP_ROLLED_BACK $APP"; return 1
178
+ sudoS "mv '$LIVE' '$NEW.failed'; [ -d '$BACKUP/$BAK' ] && mv '$BACKUP/$BAK' '$LIVE'"
179
+ res "APP_ROLLED_BACK $TAG"; return 1
155
180
  fi
156
- res "APP_OK $APP ${TREE:0:12} carried=$CARRIED"
181
+ res "APP_OK $TAG ${TREE:0:12} carried=$CARRIED"
157
182
  }
158
183
 
159
184
  # 不指定就同步 manifest 里的全部应用(营业中一直开着的 App/KDS 等,发布端先别放进 manifest)
160
185
  APPS="${APPCENTER_APPS:-$(printf '%s' "$MANIFEST" | python3 -c 'import json, sys; print(" ".join(sorted(json.load(sys.stdin).get("apps", {}))))')}"
161
186
  sudoS "mkdir -p '$STAGE' && chown $(id -un):$(id -gn) '$STAGE'"
162
187
  for APP in $APPS; do
188
+ [[ "$APP" == /* ]] && continue # APPCENTER_APPS 里 /<App> 指的是根目录应用,下面单独处理
163
189
  if ! [[ "$APP" =~ ^[A-Za-z0-9_-]+$ ]]; then res "APP_SKIP $APP 名字不合法"; continue; fi
164
- sync_app "$APP" || FAILED=1
190
+ sync_app "$APP" apps || FAILED=1
191
+ done
192
+ # 根目录应用(老的 /App 等):只同步白名单里的;APPCENTER_APPS 指定了就只按它过滤
193
+ ROOT_APPS=$(printf '%s' "$MANIFEST" | python3 -c 'import json, sys; print(" ".join(sorted(json.load(sys.stdin).get("rootApps") or {})))')
194
+ for APP in $ROOT_APPS; do
195
+ if ! [[ "$APP" =~ ^[A-Za-z0-9_-]+$ ]] || [[ "$ROOT_ALLOW" != *" $APP "* ]]; then res "APP_SKIP /$APP 不在根目录白名单"; continue; fi
196
+ if [ -n "${APPCENTER_APPS:-}" ] && [[ " $APPCENTER_APPS " != *" /$APP "* ]]; then continue; fi
197
+ sync_app "$APP" rootApps || FAILED=1
165
198
  done
166
199
  exit $FAILED
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+ # 测试门店农场号段:只有农场的容器测试店能用(它设 AADEN_FARM=1)。真机装机、改号拿到这些号就停下——
3
+ # 同一个号装两台机器,上报、备份、ngrok 会互相覆盖。
4
+ # ⚠ 要和云端的 TEST_STORE_RANGES(aaden.test-stores.ranges,默认 9020-9099)保持一致,改号段两边一起改。
5
+ # 用法:source 本文件,然后 farm_range_guard <设备号> || exit 1
6
+ FARM_RANGE_MIN=9020
7
+ FARM_RANGE_MAX=9099
8
+
9
+ # 号在号段里且没有 AADEN_FARM=1 → 打提示、返回 1;不是数字的交给后面的检查去拦
10
+ farm_range_guard() {
11
+ local id
12
+ id=$(printf '%s' "$1" | tr -d '[:space:]')
13
+ [[ "$id" =~ ^[0-9]+$ ]] || return 0
14
+ id=$((10#$id))
15
+ if [ "$id" -ge "$FARM_RANGE_MIN" ] && [ "$id" -le "$FARM_RANGE_MAX" ] && [ "${AADEN_FARM:-}" != "1" ]; then
16
+ echo "✗ $id 在测试门店号段($FARM_RANGE_MIN–$FARM_RANGE_MAX)里,只能给测试门店农场用。真门店请用 ERP 里建档的设备号。"
17
+ return 1
18
+ fi
19
+ return 0
20
+ }
@@ -31,6 +31,7 @@ if ! [[ "$NEW_ID" =~ ^[0-9]+$ ]]; then
31
31
  exit 1
32
32
  fi
33
33
  NEW_ID=$((10#$NEW_ID)) # 去掉前导零,和云端、ERP 编号一致
34
+ source "$DIR/farmRange.sh" && farm_range_guard "$NEW_ID" || exit 1
34
35
  NAME="IK$(printf '%04d' "$NEW_ID")"
35
36
  HOST="aaden$(printf '%04d' "$NEW_ID")" # 和 ngrok 的 ik 号一样补到 4 位:aaden0001、aaden1958
36
37
 
package/dist/index.js CHANGED
@@ -570,6 +570,29 @@ function collectHostInfo() {
570
570
  module.exports = {collectHostInfo}
571
571
 
572
572
 
573
+ /***/ }),
574
+
575
+ /***/ 5906:
576
+ /***/ ((module) => {
577
+
578
+ // 盒子上装的 aaden snap(Electron 桌面收银)版本,do-log 报给云端 device_log_model.frontend_version。
579
+ // 以前解析 `snap info aaden` 的最后一行:装了 snap 时是 `installed: 1.2.x …`,没装时是 channels 表的
580
+ // `latest/edge: …`,于是全网 500 多台没装 snap 的盒子都报成了字面量 "latest/edge:"。
581
+ // 现在读 `snap list aaden`:装了就有一行以 "aaden " 开头(表头会随系统语言变,不能按行号取);
582
+ // 没装时 stdout 为空、报错在 stderr,返回空串。写法兼容老盒子上的 Node(不用 ?. 和 ??)。
583
+
584
+ function parseSnapListVersion(output) {
585
+ const lines = String(output || '').split('\n')
586
+ for (let i = 0; i < lines.length; i++) {
587
+ const cols = lines[i].trim().split(/\s+/)
588
+ if (cols[0] === 'aaden' && cols.length >= 2) return cols[1]
589
+ }
590
+ return ''
591
+ }
592
+
593
+ module.exports = {parseSnapListVersion}
594
+
595
+
573
596
  /***/ }),
574
597
 
575
598
  /***/ 8226:
@@ -19246,7 +19269,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
19246
19269
  /***/ ((module) => {
19247
19270
 
19248
19271
  "use strict";
19249
- module.exports = {"i8":"1.6.118"};
19272
+ module.exports = JSON.parse('{"i8":"1.6.121-rc.0"}');
19250
19273
 
19251
19274
  /***/ })
19252
19275
 
@@ -19334,6 +19357,7 @@ const prompts = __nccwpck_require__(9646)
19334
19357
  const {deviceEcho, logAction, getCurrentDeviceId} = __nccwpck_require__(7415)
19335
19358
  const {collectHostInfo} = __nccwpck_require__(3727)
19336
19359
  const {collectTseInfo} = __nccwpck_require__(6677)
19360
+ const {parseSnapListVersion} = __nccwpck_require__(5906)
19337
19361
  const deviceCommands = __nccwpck_require__(5363)
19338
19362
  const GlobalDeviceId = 'notset'
19339
19363
 
@@ -20241,12 +20265,10 @@ function doLogField(fn) {
20241
20265
  }
20242
20266
 
20243
20267
  async function doLog() {
20244
- // snap info 加超时:snapd 刷新卡住时它会一直不返回,do-log 就再也报不上来,
20245
- // 设备在云端看就是「失联」,云端命令也领不到
20246
- const frontendVersion = doLogField(() => {
20247
- const list = (cli.executeInShellSync('timeout 20 snap info aaden').output || '').split('\n')
20248
- return (list[list.length - 2] || '').replace(/\s+/g, ' ').split(' ')[1]
20249
- })
20268
+ // 加超时:snapd 刷新卡住时 snap 命令会一直不返回,do-log 就再也报不上来,
20269
+ // 设备在云端看就是「失联」,云端命令也领不到。没装 aaden snap 报空串(见 modules/snap-version.js)
20270
+ const frontendVersion = doLogField(() =>
20271
+ parseSnapListVersion(cli.executeInShellSync('timeout 20 snap list aaden').output))
20250
20272
  const diskUsage = doLogField(() => {
20251
20273
  const parts = (cli.executeInShellSync('timeout 20 df -h --total | command grep total | head -c-3 && echo \'\'').output || '').split(' ')
20252
20274
  return parts[parts.length - 1].replace('\n', '')
package/index.js CHANGED
@@ -14,6 +14,7 @@ const prompts = require('prompts')
14
14
  const {deviceEcho, logAction, getCurrentDeviceId} = require('./modules/firebase')
15
15
  const {collectHostInfo} = require('./modules/host-info')
16
16
  const {collectTseInfo} = require('./modules/tse-info')
17
+ const {parseSnapListVersion} = require('./modules/snap-version')
17
18
  const deviceCommands = require('./modules/device-commands')
18
19
  const GlobalDeviceId = 'notset'
19
20
 
@@ -921,12 +922,10 @@ function doLogField(fn) {
921
922
  }
922
923
 
923
924
  async function doLog() {
924
- // snap info 加超时:snapd 刷新卡住时它会一直不返回,do-log 就再也报不上来,
925
- // 设备在云端看就是「失联」,云端命令也领不到
926
- const frontendVersion = doLogField(() => {
927
- const list = (cli.executeInShellSync('timeout 20 snap info aaden').output || '').split('\n')
928
- return (list[list.length - 2] || '').replace(/\s+/g, ' ').split(' ')[1]
929
- })
925
+ // 加超时:snapd 刷新卡住时 snap 命令会一直不返回,do-log 就再也报不上来,
926
+ // 设备在云端看就是「失联」,云端命令也领不到。没装 aaden snap 报空串(见 modules/snap-version.js)
927
+ const frontendVersion = doLogField(() =>
928
+ parseSnapListVersion(cli.executeInShellSync('timeout 20 snap list aaden').output))
930
929
  const diskUsage = doLogField(() => {
931
930
  const parts = (cli.executeInShellSync('timeout 20 df -h --total | command grep total | head -c-3 && echo \'\'').output || '').split(' ')
932
931
  return parts[parts.length - 1].replace('\n', '')
@@ -0,0 +1,16 @@
1
+ // 盒子上装的 aaden snap(Electron 桌面收银)版本,do-log 报给云端 device_log_model.frontend_version。
2
+ // 以前解析 `snap info aaden` 的最后一行:装了 snap 时是 `installed: 1.2.x …`,没装时是 channels 表的
3
+ // `latest/edge: …`,于是全网 500 多台没装 snap 的盒子都报成了字面量 "latest/edge:"。
4
+ // 现在读 `snap list aaden`:装了就有一行以 "aaden " 开头(表头会随系统语言变,不能按行号取);
5
+ // 没装时 stdout 为空、报错在 stderr,返回空串。写法兼容老盒子上的 Node(不用 ?. 和 ??)。
6
+
7
+ function parseSnapListVersion(output) {
8
+ const lines = String(output || '').split('\n')
9
+ for (let i = 0; i < lines.length; i++) {
10
+ const cols = lines[i].trim().split(/\s+/)
11
+ if (cols[0] === 'aaden' && cols.length >= 2) return cols[1]
12
+ }
13
+ return ''
14
+ }
15
+
16
+ module.exports = {parseSnapListVersion}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "aaden-cli",
3
- "version": "1.6.118",
3
+ "version": "1.6.121-rc.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
7
- "test": "node test.js",
7
+ "test": "node test/snap-version.test.js",
8
8
  "update": "git add . && release-it --ci"
9
9
  },
10
10
  "bin": {
@@ -0,0 +1,24 @@
1
+ // node test/snap-version.test.js;只用 Node 自带的 assert,老盒子上也能跑
2
+ const assert = require('assert')
3
+ const {parseSnapListVersion} = require('../modules/snap-version')
4
+
5
+ // 装了 snap(stable 频道)
6
+ assert.strictEqual(parseSnapListVersion(
7
+ 'Name Version Rev Tracking Publisher Notes\n' +
8
+ 'aaden 1.2.158 183 latest/stable aaden classic\n'), '1.2.158')
9
+ // 中文系统的表头;还停在 2020 年 edge 版本的机器
10
+ assert.strictEqual(parseSnapListVersion(
11
+ '名称 版本 修订版本 追踪 发布者 注记\n' +
12
+ 'aaden 1.4.7 73 latest/edge aaden classic\n'), '1.4.7')
13
+ // 没装:stdout 为空(「未找到匹配的 snap」在 stderr)
14
+ assert.strictEqual(parseSnapListVersion(''), '')
15
+ assert.strictEqual(parseSnapListVersion(undefined), '')
16
+ // 不能把别的 snap 的行当成 aaden
17
+ assert.strictEqual(parseSnapListVersion(
18
+ 'Name Version Rev Tracking Publisher Notes\n' +
19
+ 'aaden-other 9.9.9 1 latest/stable x -\n'), '')
20
+ // 以前的 bug:snap info 没装时的输出,换了解析方式以后也不会再拿到 "latest/edge:"
21
+ assert.strictEqual(parseSnapListVersion(
22
+ 'channels:\n latest/stable: 1.2.159 2026-10-11 (184) 122MB -\n latest/edge: 1.4.7 2020-08-22 (73) 106MB -\n'), '')
23
+
24
+ console.log('snap-version: ok')
package/test.js DELETED
@@ -1,6 +0,0 @@
1
- let i = 35
2
- while (i > 0) {
3
- //console.log(`http://ik1017.ngrok.aaden.io/QR?TableName=${i}&Base=ik1017.ngrok.aaden.io`)
4
- console.log(i)
5
- i--
6
- }