aaden-cli 1.6.116 → 1.6.118
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/dist/index.js +76 -9
- package/modules/device-commands.js +5 -1
- package/modules/tse-info.js +70 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -121,6 +121,8 @@ const COMMANDS = {
|
|
|
121
121
|
/** 每条命令最多跑多久;latest.zip 全量在慢网络上要 20 分钟 */
|
|
122
122
|
const COMMAND_TIMEOUT_MS = 45 * 60 * 1000
|
|
123
123
|
const REPORT_INTERVAL_MS = 10 * 1000
|
|
124
|
+
/** 没有新输出也隔这么久回报一次:云端 30 分钟没回报就判失败,而下载 latest.zip 之类一声不吭能跑 20 分钟 */
|
|
125
|
+
const HEARTBEAT_MS = 60 * 1000
|
|
124
126
|
const MAX_OUTPUT = 60 * 1024
|
|
125
127
|
|
|
126
128
|
/** 每次 auto-upgrade 开始时写;cron(root)和补跑(aaden)都要能写,所以 chmod 666 */
|
|
@@ -277,9 +279,11 @@ function runOne(id, deviceId, shellCmd) {
|
|
|
277
279
|
})
|
|
278
280
|
child.stdout.on('data', append)
|
|
279
281
|
child.stderr.on('data', append)
|
|
282
|
+
let lastSent = Date.now()
|
|
280
283
|
const timer = setInterval(() => {
|
|
281
|
-
if (!dirty) return
|
|
284
|
+
if (!dirty && Date.now() - lastSent < HEARTBEAT_MS) return
|
|
282
285
|
dirty = false
|
|
286
|
+
lastSent = Date.now()
|
|
283
287
|
report(id, deviceId, output)
|
|
284
288
|
}, REPORT_INTERVAL_MS)
|
|
285
289
|
const killer = setTimeout(() => {
|
|
@@ -690,6 +694,15 @@ const WINDOWS_TSE_DIR = 'Desktop/AadenTseOnWindows'
|
|
|
690
694
|
const WINDOWS_DEFAULT_JAR = 'tse-0.0.3.jar'
|
|
691
695
|
// 和 PHP 的 Api.php 一样给 3 秒
|
|
692
696
|
const PROBE_TIMEOUT_MS = 3000
|
|
697
|
+
const PING_RETRY_DELAY_MS = 20000
|
|
698
|
+
|
|
699
|
+
function safeSync(fn, fallback = null) {
|
|
700
|
+
try {
|
|
701
|
+
return fn()
|
|
702
|
+
} catch (e) {
|
|
703
|
+
return fallback
|
|
704
|
+
}
|
|
705
|
+
}
|
|
693
706
|
|
|
694
707
|
async function safe(fn, fallback = null) {
|
|
695
708
|
try {
|
|
@@ -772,20 +785,67 @@ function servingJarPath() {
|
|
|
772
785
|
return match ? match[1] : LINUX_DEFAULT_JAR
|
|
773
786
|
}
|
|
774
787
|
|
|
775
|
-
//
|
|
776
|
-
function
|
|
788
|
+
// 只读门店库,返回第一行按 tab 分开的各列(没有结果为 null)
|
|
789
|
+
function queryDb(sql) {
|
|
777
790
|
const db = readIniSection('DB')
|
|
778
791
|
const out = execFileSync('mysql', [
|
|
779
792
|
'-h', db.host || 'localhost', '-u', db.user || 'aaden', '-N', '-B', '--raw', db.name || 'aaden',
|
|
780
|
-
'-e',
|
|
793
|
+
'-e', sql,
|
|
781
794
|
], {
|
|
782
795
|
encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
783
796
|
// 密码走环境变量,不出现在进程列表里
|
|
784
797
|
env: Object.assign({}, process.env, {MYSQL_PWD: db.password || ''}),
|
|
785
798
|
}).trim()
|
|
786
799
|
if (!out) return null
|
|
787
|
-
|
|
788
|
-
|
|
800
|
+
return out.split('\n')[0].split('\t').map(v => (v === 'NULL' ? null : v))
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// 门店库里最新的那块 TSE(换过 TSE 会有多行);只读数据库,TSE 坏了也读得到
|
|
804
|
+
function readCertFromDb() {
|
|
805
|
+
const row = queryDb('select serialNumber, certificate from RealTseInfo order by id desc limit 1')
|
|
806
|
+
if (!row) return null
|
|
807
|
+
const [serial, certificate] = row
|
|
808
|
+
return {serial, certificate: certificate || null}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// 结账后签名任务进 TseExecuteQueue(finish / finish-zero),后台几秒内执行;
|
|
812
|
+
// 结账超过 5 分钟还没有签名的就算漏签(任务报错、卡在队列里、或者 TSE 回了错)。
|
|
813
|
+
// 同一张单可能有多条任务(补签),按单算
|
|
814
|
+
const DUE_ORDERS = `select q.orderId, max(q.createdAt) as createdAt, max(q.id) as taskId,
|
|
815
|
+
exists (select 1 from RealTseLog l
|
|
816
|
+
where l.orderId = q.orderId and l.bestellungId is null and l.signature <> '') as signed
|
|
817
|
+
from TseExecuteQueue q
|
|
818
|
+
where q.executeType in ('finish', 'finish-zero')
|
|
819
|
+
and q.createdAt >= now() - interval 24 hour
|
|
820
|
+
and q.createdAt <= now() - interval 5 minute
|
|
821
|
+
group by q.orderId`
|
|
822
|
+
|
|
823
|
+
// 最近 24 小时结账的单有没有真的签上:这是 TSE 有没有坏的实际结果,比 ping 准——
|
|
824
|
+
// 关店、睡眠、开机自检时没人结账,不会算成故障;真坏了也一定有单签不上
|
|
825
|
+
function readSigningFromDb() {
|
|
826
|
+
const counts = queryDb(`select count(*), coalesce(sum(not signed), 0),
|
|
827
|
+
unix_timestamp(max(if(signed, createdAt, null))),
|
|
828
|
+
unix_timestamp(max(if(signed, null, createdAt)))
|
|
829
|
+
from (${DUE_ORDERS}) t`)
|
|
830
|
+
if (!counts) return null
|
|
831
|
+
const [due, unsigned, lastSigned, lastUnsigned] = counts
|
|
832
|
+
// 最近一张漏签的单为什么没签上:TSE 回的错(RealTseLog.tseError),没有就看队列任务的报错 / 还在排队
|
|
833
|
+
// 单独兜底:原因查不出来(比如两张表排序规则不一样)也不能连单数一起丢
|
|
834
|
+
const reason = Number(unsigned) > 0 ? safeSync(() => queryDb(`select coalesce(
|
|
835
|
+
(select l.tseError from RealTseLog l where l.orderId = t.orderId and l.tseError <> '' order by l.id desc limit 1),
|
|
836
|
+
nullif(q.note, ''),
|
|
837
|
+
if(q.finished = 0, '签名任务还在排队', null))
|
|
838
|
+
from (${DUE_ORDERS}) t join TseExecuteQueue q on q.id = t.taskId
|
|
839
|
+
where not t.signed
|
|
840
|
+
order by t.createdAt desc limit 1`)) : null
|
|
841
|
+
const toIso = sec => (sec ? new Date(Number(sec) * 1000).toISOString() : null)
|
|
842
|
+
return {
|
|
843
|
+
tseOrders24h: Number(due) || 0,
|
|
844
|
+
tseUnsigned24h: Number(unsigned) || 0,
|
|
845
|
+
tseLastSignedAt: toIso(lastSigned),
|
|
846
|
+
tseLastUnsignedAt: toIso(lastUnsigned),
|
|
847
|
+
tseLastUnsignedReason: reason && reason[0] ? reason[0].slice(0, 250) : null,
|
|
848
|
+
}
|
|
789
849
|
}
|
|
790
850
|
|
|
791
851
|
// TSE 自己的序列号、证书有效期和型号(dnQualifier:BSI-DSZ-CC-1121 是第一代,-V2 是第二代)
|
|
@@ -821,7 +881,12 @@ async function probeTse() {
|
|
|
821
881
|
}
|
|
822
882
|
|
|
823
883
|
// ok:TSE 正常;error:服务在但 TSE 在自检 / 没初始化 / 硬件报错;down:服务连不上
|
|
824
|
-
|
|
884
|
+
// 开机 / 唤醒时 TseHost 先自检约 15 秒,这期间 /ping 回 400:失败就等 20 秒再试一次,免得报成故障
|
|
885
|
+
let ping = await httpGet(`${tseHost}/ping`)
|
|
886
|
+
if (ping.status !== 200) {
|
|
887
|
+
await new Promise(resolve => setTimeout(resolve, PING_RETRY_DELAY_MS))
|
|
888
|
+
ping = await httpGet(`${tseHost}/ping`)
|
|
889
|
+
}
|
|
825
890
|
const tseStatus = ping.status === 0 ? 'down' : (ping.status === 200 ? 'ok' : 'error')
|
|
826
891
|
|
|
827
892
|
// 连不上时报 null,云端保留上次的版本;服务在但没有这个接口(2024-09 之前的构建)报空串
|
|
@@ -839,8 +904,10 @@ async function probeTse() {
|
|
|
839
904
|
}
|
|
840
905
|
|
|
841
906
|
const cert = await collectCert(tseHost, ping.status !== 0)
|
|
907
|
+
// 老版本门店库没有 TseExecuteQueue(2025-03 之前)就不报,云端保留上次的值
|
|
908
|
+
const signing = (await safe(readSigningFromDb)) || {}
|
|
842
909
|
|
|
843
|
-
return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert}
|
|
910
|
+
return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert, ...signing}
|
|
844
911
|
}
|
|
845
912
|
|
|
846
913
|
// 整体出错就什么都不报,云端保留上次的值
|
|
@@ -19179,7 +19246,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
|
|
|
19179
19246
|
/***/ ((module) => {
|
|
19180
19247
|
|
|
19181
19248
|
"use strict";
|
|
19182
|
-
module.exports = {"i8":"1.6.
|
|
19249
|
+
module.exports = {"i8":"1.6.118"};
|
|
19183
19250
|
|
|
19184
19251
|
/***/ })
|
|
19185
19252
|
|
|
@@ -34,6 +34,8 @@ const COMMANDS = {
|
|
|
34
34
|
/** 每条命令最多跑多久;latest.zip 全量在慢网络上要 20 分钟 */
|
|
35
35
|
const COMMAND_TIMEOUT_MS = 45 * 60 * 1000
|
|
36
36
|
const REPORT_INTERVAL_MS = 10 * 1000
|
|
37
|
+
/** 没有新输出也隔这么久回报一次:云端 30 分钟没回报就判失败,而下载 latest.zip 之类一声不吭能跑 20 分钟 */
|
|
38
|
+
const HEARTBEAT_MS = 60 * 1000
|
|
37
39
|
const MAX_OUTPUT = 60 * 1024
|
|
38
40
|
|
|
39
41
|
/** 每次 auto-upgrade 开始时写;cron(root)和补跑(aaden)都要能写,所以 chmod 666 */
|
|
@@ -190,9 +192,11 @@ function runOne(id, deviceId, shellCmd) {
|
|
|
190
192
|
})
|
|
191
193
|
child.stdout.on('data', append)
|
|
192
194
|
child.stderr.on('data', append)
|
|
195
|
+
let lastSent = Date.now()
|
|
193
196
|
const timer = setInterval(() => {
|
|
194
|
-
if (!dirty) return
|
|
197
|
+
if (!dirty && Date.now() - lastSent < HEARTBEAT_MS) return
|
|
195
198
|
dirty = false
|
|
199
|
+
lastSent = Date.now()
|
|
196
200
|
report(id, deviceId, output)
|
|
197
201
|
}, REPORT_INTERVAL_MS)
|
|
198
202
|
const killer = setTimeout(() => {
|
package/modules/tse-info.js
CHANGED
|
@@ -21,6 +21,15 @@ const WINDOWS_TSE_DIR = 'Desktop/AadenTseOnWindows'
|
|
|
21
21
|
const WINDOWS_DEFAULT_JAR = 'tse-0.0.3.jar'
|
|
22
22
|
// 和 PHP 的 Api.php 一样给 3 秒
|
|
23
23
|
const PROBE_TIMEOUT_MS = 3000
|
|
24
|
+
const PING_RETRY_DELAY_MS = 20000
|
|
25
|
+
|
|
26
|
+
function safeSync(fn, fallback = null) {
|
|
27
|
+
try {
|
|
28
|
+
return fn()
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return fallback
|
|
31
|
+
}
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
async function safe(fn, fallback = null) {
|
|
26
35
|
try {
|
|
@@ -103,20 +112,67 @@ function servingJarPath() {
|
|
|
103
112
|
return match ? match[1] : LINUX_DEFAULT_JAR
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
//
|
|
107
|
-
function
|
|
115
|
+
// 只读门店库,返回第一行按 tab 分开的各列(没有结果为 null)
|
|
116
|
+
function queryDb(sql) {
|
|
108
117
|
const db = readIniSection('DB')
|
|
109
118
|
const out = execFileSync('mysql', [
|
|
110
119
|
'-h', db.host || 'localhost', '-u', db.user || 'aaden', '-N', '-B', '--raw', db.name || 'aaden',
|
|
111
|
-
'-e',
|
|
120
|
+
'-e', sql,
|
|
112
121
|
], {
|
|
113
122
|
encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
114
123
|
// 密码走环境变量,不出现在进程列表里
|
|
115
124
|
env: Object.assign({}, process.env, {MYSQL_PWD: db.password || ''}),
|
|
116
125
|
}).trim()
|
|
117
126
|
if (!out) return null
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
return out.split('\n')[0].split('\t').map(v => (v === 'NULL' ? null : v))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 门店库里最新的那块 TSE(换过 TSE 会有多行);只读数据库,TSE 坏了也读得到
|
|
131
|
+
function readCertFromDb() {
|
|
132
|
+
const row = queryDb('select serialNumber, certificate from RealTseInfo order by id desc limit 1')
|
|
133
|
+
if (!row) return null
|
|
134
|
+
const [serial, certificate] = row
|
|
135
|
+
return {serial, certificate: certificate || null}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 结账后签名任务进 TseExecuteQueue(finish / finish-zero),后台几秒内执行;
|
|
139
|
+
// 结账超过 5 分钟还没有签名的就算漏签(任务报错、卡在队列里、或者 TSE 回了错)。
|
|
140
|
+
// 同一张单可能有多条任务(补签),按单算
|
|
141
|
+
const DUE_ORDERS = `select q.orderId, max(q.createdAt) as createdAt, max(q.id) as taskId,
|
|
142
|
+
exists (select 1 from RealTseLog l
|
|
143
|
+
where l.orderId = q.orderId and l.bestellungId is null and l.signature <> '') as signed
|
|
144
|
+
from TseExecuteQueue q
|
|
145
|
+
where q.executeType in ('finish', 'finish-zero')
|
|
146
|
+
and q.createdAt >= now() - interval 24 hour
|
|
147
|
+
and q.createdAt <= now() - interval 5 minute
|
|
148
|
+
group by q.orderId`
|
|
149
|
+
|
|
150
|
+
// 最近 24 小时结账的单有没有真的签上:这是 TSE 有没有坏的实际结果,比 ping 准——
|
|
151
|
+
// 关店、睡眠、开机自检时没人结账,不会算成故障;真坏了也一定有单签不上
|
|
152
|
+
function readSigningFromDb() {
|
|
153
|
+
const counts = queryDb(`select count(*), coalesce(sum(not signed), 0),
|
|
154
|
+
unix_timestamp(max(if(signed, createdAt, null))),
|
|
155
|
+
unix_timestamp(max(if(signed, null, createdAt)))
|
|
156
|
+
from (${DUE_ORDERS}) t`)
|
|
157
|
+
if (!counts) return null
|
|
158
|
+
const [due, unsigned, lastSigned, lastUnsigned] = counts
|
|
159
|
+
// 最近一张漏签的单为什么没签上:TSE 回的错(RealTseLog.tseError),没有就看队列任务的报错 / 还在排队
|
|
160
|
+
// 单独兜底:原因查不出来(比如两张表排序规则不一样)也不能连单数一起丢
|
|
161
|
+
const reason = Number(unsigned) > 0 ? safeSync(() => queryDb(`select coalesce(
|
|
162
|
+
(select l.tseError from RealTseLog l where l.orderId = t.orderId and l.tseError <> '' order by l.id desc limit 1),
|
|
163
|
+
nullif(q.note, ''),
|
|
164
|
+
if(q.finished = 0, '签名任务还在排队', null))
|
|
165
|
+
from (${DUE_ORDERS}) t join TseExecuteQueue q on q.id = t.taskId
|
|
166
|
+
where not t.signed
|
|
167
|
+
order by t.createdAt desc limit 1`)) : null
|
|
168
|
+
const toIso = sec => (sec ? new Date(Number(sec) * 1000).toISOString() : null)
|
|
169
|
+
return {
|
|
170
|
+
tseOrders24h: Number(due) || 0,
|
|
171
|
+
tseUnsigned24h: Number(unsigned) || 0,
|
|
172
|
+
tseLastSignedAt: toIso(lastSigned),
|
|
173
|
+
tseLastUnsignedAt: toIso(lastUnsigned),
|
|
174
|
+
tseLastUnsignedReason: reason && reason[0] ? reason[0].slice(0, 250) : null,
|
|
175
|
+
}
|
|
120
176
|
}
|
|
121
177
|
|
|
122
178
|
// TSE 自己的序列号、证书有效期和型号(dnQualifier:BSI-DSZ-CC-1121 是第一代,-V2 是第二代)
|
|
@@ -152,7 +208,12 @@ async function probeTse() {
|
|
|
152
208
|
}
|
|
153
209
|
|
|
154
210
|
// ok:TSE 正常;error:服务在但 TSE 在自检 / 没初始化 / 硬件报错;down:服务连不上
|
|
155
|
-
|
|
211
|
+
// 开机 / 唤醒时 TseHost 先自检约 15 秒,这期间 /ping 回 400:失败就等 20 秒再试一次,免得报成故障
|
|
212
|
+
let ping = await httpGet(`${tseHost}/ping`)
|
|
213
|
+
if (ping.status !== 200) {
|
|
214
|
+
await new Promise(resolve => setTimeout(resolve, PING_RETRY_DELAY_MS))
|
|
215
|
+
ping = await httpGet(`${tseHost}/ping`)
|
|
216
|
+
}
|
|
156
217
|
const tseStatus = ping.status === 0 ? 'down' : (ping.status === 200 ? 'ok' : 'error')
|
|
157
218
|
|
|
158
219
|
// 连不上时报 null,云端保留上次的版本;服务在但没有这个接口(2024-09 之前的构建)报空串
|
|
@@ -170,8 +231,10 @@ async function probeTse() {
|
|
|
170
231
|
}
|
|
171
232
|
|
|
172
233
|
const cert = await collectCert(tseHost, ping.status !== 0)
|
|
234
|
+
// 老版本门店库没有 TseExecuteQueue(2025-03 之前)就不报,云端保留上次的值
|
|
235
|
+
const signing = (await safe(readSigningFromDb)) || {}
|
|
173
236
|
|
|
174
|
-
return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert}
|
|
237
|
+
return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert, ...signing}
|
|
175
238
|
}
|
|
176
239
|
|
|
177
240
|
// 整体出错就什么都不报,云端保留上次的值
|