aaden-cli 1.6.122-rc.0 → 1.6.123

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.
@@ -11,6 +11,14 @@
11
11
  local(localhost/127.x,收银机本机)、lan(局域网 IP,平板等)、none(没有 Referer,一般是直接打开页面)。
12
12
  只统计真实存在的目录,扫描器乱打的路径不算。
13
13
 
14
+ 另外按「客户端类型」统计打到 /PHP/ 的请求,应用名以 "@client/" 开头(原生 App 不带 Referer,上面那套看不到):
15
+ - User-Agent 是 Aaden-<应用>/<版本> 的(约定的统一格式)→ "@client/Aaden-<应用>/<版本>"
16
+ - 否则按 HTTP 库分:okhttp(快消版安卓收银 2.0/3.0、K2)、ktor(点单 / 老板端)、
17
+ electron-<系统>(餐厅版桌面,windows / linux)、browser-<系统>(android 多半是商米机)、
18
+ system(门店主机自己的 cron / PHP / worker、云端经 ngrok 的请求)、other
19
+ - okhttp 另外数几个只有某一代收银才调的接口(MARKERS),用来区分 2.0 和 3.x,比如 "@client/okhttp:DashboardData.php"
20
+ 客户端类的 sources 按客户端 IP 分(没有 Referer):local=127.x/::1(含经 ngrok 进来的),lan=其它。
21
+
14
22
  用法: appCenterUsage.py [日志目录,默认 /var/log/apache2];需要能读 access.log*(root 或 adm 组)。
15
23
  """
16
24
  import datetime
@@ -49,6 +57,52 @@ def source_of(ref_host):
49
57
  return 'lan'
50
58
 
51
59
 
60
+ # 统一 User-Agent 约定:Aaden-<应用>/<版本> (<平台>; build <提交>; <渠道>)
61
+ AADEN_UA = re.compile(r'^Aaden[- ]([A-Za-z0-9_.-]{1,24})/([A-Za-z0-9_.+-]{1,20})') # 也认老的 "Aaden Buffet/2.16"
62
+ # 门店主机自己(cron 的 wget/curl、PHP 的 Guzzle、node worker 的 axios)和云端经 ngrok 来的请求,不是收银前端
63
+ SYSTEM_UA = ('wget/', 'curl/', 'guzzlehttp', 'axios/', 'reactornetty', 'java/', 'apache-httpclient', 'undici',
64
+ 'python-requests', 'go-http-client', 'node-fetch', 'php/')
65
+ # 只有某一代安卓收银会调的 PHP 接口:DashboardData.php 只在 2.0,QuickOrder.php 只在较新的 3.x
66
+ MARKERS = ('DashboardData.php', 'QuickOrder.php')
67
+ CLIENT = '@client/'
68
+
69
+
70
+ def client_kind(ua):
71
+ """User-Agent → 客户端类型(不含 "@client/" 前缀)"""
72
+ m = AADEN_UA.match(ua)
73
+ if m:
74
+ return f'Aaden-{m.group(1)}/{m.group(2)}'
75
+ low = ua.lower()
76
+ if low in ('', '-') or low.startswith(SYSTEM_UA):
77
+ return 'system'
78
+ if low.startswith('okhttp'):
79
+ return 'okhttp'
80
+ if 'ktor' in low:
81
+ return 'ktor'
82
+ if 'electron' in low:
83
+ return 'electron-' + ua_os(low)
84
+ if low.startswith('mozilla'):
85
+ return 'browser-' + ua_os(low)
86
+ return 'other'
87
+
88
+
89
+ def ua_os(low):
90
+ """餐厅版要分 Windows / Ubuntu 桌面和安卓平板(商米等)"""
91
+ if 'android' in low:
92
+ return 'android'
93
+ if 'windows' in low:
94
+ return 'windows'
95
+ if 'linux' in low or 'x11' in low:
96
+ return 'linux'
97
+ if 'iphone' in low or 'ipad' in low or 'mac os' in low:
98
+ return 'apple'
99
+ return 'other'
100
+
101
+
102
+ def ip_source(ip):
103
+ return 'local' if ip.startswith('127.') or ip == '::1' else 'lan'
104
+
105
+
52
106
  TS = re.compile(r'^(\d{2})/([A-Z][a-z]{2})/(\d{4}):(\d{2}:\d{2}:\d{2})')
53
107
 
54
108
 
@@ -122,6 +176,25 @@ def _app_version(rel):
122
176
  return info
123
177
 
124
178
 
179
+ def count_client(stats, m):
180
+ """一条 /PHP/ 请求按客户端类型记一次;okhttp 碰到标记接口再多记一行"""
181
+ day, at = parse_ts(m['ts'])
182
+ if not day:
183
+ return
184
+ kind = client_kind(m['ua'])
185
+ names = [CLIENT + kind]
186
+ if kind == 'okhttp':
187
+ bare = m['path'].split('?')[0]
188
+ names += [f'{CLIENT}okhttp:{mk}' for mk in MARKERS if bare.endswith('/' + mk)]
189
+ for name in names:
190
+ s = stats[(name, day)]
191
+ s['requests'] += 1
192
+ s['clients'].add((m['ip'], '-', m['ua'][:120]))
193
+ s['sources'][ip_source(m['ip'])] += 1
194
+ if at > s['lastSeen']:
195
+ s['lastSeen'] = at
196
+
197
+
125
198
  def main():
126
199
  installed = []
127
200
  if os.path.isdir(APPCENTER):
@@ -159,11 +232,16 @@ def main():
159
232
  log_from = at
160
233
  if log_to is None or at > log_to:
161
234
  log_to = at
162
- if prefilter is None or not prefilter.search(line):
235
+ php = ' /PHP/' in line
236
+ if not php and (prefilter is None or not prefilter.search(line)):
163
237
  continue
164
238
  m = LINE.match(line)
165
239
  if not m:
166
240
  continue
241
+ if php and m['path'].startswith('/PHP/'):
242
+ count_client(stats, m)
243
+ if prefilter is None or not prefilter.search(line):
244
+ continue
167
245
  path, ref = m['path'], m['ref']
168
246
  pm = APP_IN_PATH.match(path)
169
247
  rm = APP_IN_REF.match(ref) if ref and ref != '-' else None
package/dist/index.js CHANGED
@@ -19352,7 +19352,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
19352
19352
  /***/ ((module) => {
19353
19353
 
19354
19354
  "use strict";
19355
- module.exports = JSON.parse('{"i8":"1.6.122-rc.0"}');
19355
+ module.exports = {"i8":"1.6.123"};
19356
19356
 
19357
19357
  /***/ })
19358
19358
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "aaden-cli",
3
- "version": "1.6.122-rc.0",
3
+ "version": "1.6.123",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
7
- "test": "node test/snap-version.test.js && python3 test/app_version_test.py",
7
+ "test": "node test/snap-version.test.js && python3 test/app_version_test.py && python3 test/client_kind_test.py",
8
8
  "update": "git add . && release-it --ci"
9
9
  },
10
10
  "bin": {
@@ -0,0 +1,61 @@
1
+ # python3 test/client_kind_test.py:appCenterUsage 按客户端类型统计 /PHP/ 请求
2
+ import importlib.util, io, json, os, sys, tempfile, contextlib
3
+ sys.dont_write_bytecode = True
4
+
5
+ logdir = tempfile.mkdtemp()
6
+ root = tempfile.mkdtemp()
7
+ os.makedirs(os.path.join(root, 'App')); open(os.path.join(root, 'App', 'index.html'), 'w').write('<p>')
8
+ os.makedirs(os.path.join(root, 'PHP'))
9
+
10
+ L = '{ip} - - [14/Oct/2026:12:{mm}:00 +0200] "GET {path} HTTP/1.1" 200 12 "{ref}" "{ua}"\n'
11
+ lines = [
12
+ # 2.0 安卓收银:okhttp + 2.0 独有的 DashboardData.php
13
+ L.format(ip='192.168.1.20', mm='01', path='/PHP/Orders.php?op=list', ref='-', ua='okhttp/5.0.0-alpha.14'),
14
+ L.format(ip='192.168.1.20', mm='02', path='/PHP/DashboardData.php?x=1', ref='-', ua='okhttp/5.0.0-alpha.14'),
15
+ # 3.x 安卓收银:QuickOrder.php
16
+ L.format(ip='192.168.1.21', mm='03', path='/PHP/QuickOrder.php?op=config', ref='-', ua='okhttp/5.0.0-alpha.14'),
17
+ # 统一格式的 UA
18
+ L.format(ip='192.168.1.22', mm='04', path='/PHP/Orders.php', ref='-', ua='Aaden-Antelope/3.1.32 (android; build 38a5a79; pgyer)'),
19
+ # 商米机浏览器里的 /App:既算 App(Referer)也算 browser-android
20
+ L.format(ip='127.0.0.1', mm='05', path='/PHP/Orders.php', ref='http://localhost/App/', ua='Mozilla/5.0 (Linux; Android 7.1.2; T2s Build/NHG47K) Chrome/120'),
21
+ L.format(ip='127.0.0.1', mm='06', path='/PHP/Orders.php', ref='-', ua='Mozilla/5.0 (Windows NT 10.0; Win64; x64) aaden-desktop Electron/28.0.0'),
22
+ L.format(ip='192.168.1.30', mm='07', path='/PHP/Dishes.php', ref='-', ua='Ktor client'),
23
+ # 不是 /PHP/ 的不算客户端;乱码时间整行跳过
24
+ L.format(ip='192.168.1.20', mm='08', path='/Resource/dishImg/1.png', ref='-', ua='okhttp/5.0.0-alpha.14'),
25
+ '\x16\x03 - - [\x00garbage] "GET /PHP/x.php HTTP/1.1" 200 1 "-" "okhttp/5"\n',
26
+ ]
27
+ open(os.path.join(logdir, 'access.log'), 'w').writelines(lines)
28
+
29
+ spec = importlib.util.spec_from_file_location('usage', os.path.join(os.path.dirname(__file__), '..', 'AadenDeploy', 'appCenterUsage.py'))
30
+ usage = importlib.util.module_from_spec(spec)
31
+ sys.argv = [sys.argv[0], logdir]
32
+ spec.loader.exec_module(usage)
33
+ usage.DOCROOT = root
34
+ usage.APPCENTER = os.path.join(root, 'AppCenter')
35
+
36
+ buf = io.StringIO()
37
+ with contextlib.redirect_stdout(buf):
38
+ usage.main()
39
+ out = json.loads(buf.getvalue())
40
+ rows = {r['app']: r for r in out['usage']}
41
+
42
+ assert rows['@client/okhttp']['requests'] == 3 and rows['@client/okhttp']['clients'] == 2, rows['@client/okhttp']
43
+ assert rows['@client/okhttp:DashboardData.php']['requests'] == 1
44
+ assert rows['@client/okhttp:QuickOrder.php']['requests'] == 1
45
+ assert rows['@client/Aaden-Antelope/3.1.32']['requests'] == 1
46
+ assert rows['@client/browser-android']['requests'] == 1 and rows['@client/browser-android']['sources']['local'] == 1
47
+ assert rows['@client/electron-windows']['requests'] == 1
48
+ assert rows['@client/ktor']['requests'] == 1 and rows['@client/ktor']['sources']['lan'] == 1
49
+ assert rows['App']['requests'] == 1, rows['App']
50
+ assert not any(a.startswith('@client/') for a in out['installedApps'])
51
+ assert set(rows) == {'App', '@client/okhttp', '@client/okhttp:DashboardData.php', '@client/okhttp:QuickOrder.php',
52
+ '@client/Aaden-Antelope/3.1.32', '@client/browser-android', '@client/electron-windows', '@client/ktor'}, set(rows)
53
+ assert all(len(a) <= 64 for a in rows)
54
+ assert usage.client_kind('Mozilla/5.0 (X11; Linux x86_64) aaden/1.0 Electron/22') == 'electron-linux'
55
+ assert usage.client_kind('Mozilla/5.0 (Windows NT 10.0) Chrome/120') == 'browser-windows'
56
+ assert usage.client_kind('curl/8') == 'system' and usage.client_kind('-') == 'system'
57
+ assert usage.client_kind('GuzzleHttp/7') == 'system' and usage.client_kind('Wget/1.20.3 (linux-gnu)') == 'system'
58
+ assert usage.client_kind('Aaden Buffet/2.16 (iPad; iOS 27.0.1; Scale/2.00)') == 'Aaden-Buffet/2.16'
59
+ assert usage.client_kind('ktor-client') == 'ktor'
60
+ assert usage.client_kind('SomethingNew/1') == 'other'
61
+ print('client_kind_test OK')