koishi-plugin-haulmp-tools 1.0.2 → 1.0.3

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.
@@ -62,6 +62,29 @@ let mapPage = null
62
62
  let mapPageReady = false
63
63
  let mapPageInit = null
64
64
  let mapPageLock = Promise.resolve()
65
+ // 环境不支持 WebGL2 时(无头 Linux 常见)不再反复尝试:命中后冷却一段时间再试
66
+ let mapUnsupportedAt = 0
67
+ const MAP_RETRY_MS = 30 * 60 * 1000
68
+
69
+ // 等页面就绪;期间若出现 WebGL 初始化错误则立即失败(不必干等 30 秒)
70
+ function waitMapReady(page, readErr) {
71
+ return new Promise((resolve, reject) => {
72
+ let done = false
73
+ const finish = (fn, v) => {
74
+ if (done) return
75
+ done = true
76
+ clearInterval(timer)
77
+ fn(v)
78
+ }
79
+ const timer = setInterval(() => {
80
+ const e = readErr()
81
+ if (e) finish(reject, new Error(`WebGL2 不可用(${String(e).slice(0, 60)}),该环境无法渲染地图`))
82
+ }, 250)
83
+ page.waitForFunction(() => window.__ready === true && typeof window.getTileUrl === 'function', { timeout: 30000 })
84
+ .then(() => finish(resolve))
85
+ .catch((e) => finish(reject, e))
86
+ })
87
+ }
65
88
 
66
89
  function withTimeout(promise, ms, onTimeout) {
67
90
  let timer
@@ -151,6 +174,7 @@ async function doGetMapPage(ctx) {
151
174
  puppeteer = ctx.get('puppeteer')
152
175
  } catch (e) { /* ignore */ }
153
176
  if (!puppeteer) return null
177
+ if (mapUnsupportedAt && Date.now() - mapUnsupportedAt < MAP_RETRY_MS) return null
154
178
  const port = await startStaticServer(ctx)
155
179
  if (!port) return null
156
180
  if (mapPage && mapPageReady) {
@@ -174,23 +198,33 @@ async function doGetMapPage(ctx) {
174
198
  const t0 = Date.now()
175
199
  mapPageReady = false
176
200
  mapPage = await puppeteer.page()
201
+ let pageErr = null
202
+ const readErr = () => pageErr
203
+ mapPage.on('pageerror', (err) => {
204
+ const msg = err && err.message ? err.message : String(err)
205
+ if (/webgl|gpuinitialization/i.test(msg)) pageErr = msg
206
+ ctx.logger('haulmp').warn('地图页面脚本错误: %s', msg)
207
+ })
177
208
  try {
178
- mapPage.on('pageerror', (err) => {
179
- ctx.logger('haulmp').warn('地图页面脚本错误: %s', err && err.message ? err.message : String(err))
180
- })
181
209
  await mapPage.setViewport({ width: 720, height: 500, deviceScaleFactor: 2 })
182
210
  await mapPage.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'domcontentloaded', timeout: 30000 })
183
211
  // __ready(底图 load 事件)与 getTileUrl(模块执行完成)都满足才算就绪
184
- await mapPage.waitForFunction(() => window.__ready === true && typeof window.getTileUrl === 'function', { timeout: 30000 })
212
+ await waitMapReady(mapPage, readErr)
185
213
  mapPageReady = true
214
+ mapUnsupportedAt = 0
186
215
  ctx.logger('haulmp').info('地图页面构建完成 %dms', Date.now() - t0)
187
216
  preloadEurope(mapPage).catch(() => {})
188
217
  return mapPage
189
218
  } catch (e) {
190
- ctx.logger('haulmp').warn('地图页面初始化失败: %s', e.message)
191
219
  try { mapPage.close().catch(() => {}) } catch (err) { /* ignore */ }
192
220
  mapPage = null
193
221
  mapPageReady = false
222
+ if (/WebGL2 不可用/.test(e.message)) {
223
+ mapUnsupportedAt = Date.now()
224
+ ctx.logger('haulmp').warn('地图渲染不可用:当前浏览器缺少 WebGL2(无头 Linux 常见)。定位指令将退回文字结果;如需地图请在 puppeteer 服务配置 args 中加入:--use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshader(去掉 --disable-gpu),已暂停 30 分钟内重试')
225
+ } else {
226
+ ctx.logger('haulmp').warn('地图页面初始化失败: %s', e.message)
227
+ }
194
228
  return null
195
229
  }
196
230
  }
@@ -217,10 +251,104 @@ async function mapShot(ctx, info, p, nearby) {
217
251
  return run
218
252
  }
219
253
 
254
+ // ---- Canvas 2D 兜底页(不依赖 WebGL,任何无头环境都能出图)----
255
+ let fbPage = null
256
+ let fbReady = false
257
+ let fbLock = Promise.resolve()
258
+
259
+ async function getFallbackPage(ctx) {
260
+ const run = fbLock.then(() => doGetFallbackPage(ctx), () => doGetFallbackPage(ctx))
261
+ fbLock = run.then(() => {}, () => {})
262
+ return run
263
+ }
264
+
265
+ async function doGetFallbackPage(ctx) {
266
+ let puppeteer = null
267
+ try {
268
+ puppeteer = ctx.get('puppeteer')
269
+ } catch (e) { /* ignore */ }
270
+ if (!puppeteer) return null
271
+ const port = await startStaticServer(ctx)
272
+ if (!port) return null
273
+ if (fbPage && fbReady) {
274
+ try {
275
+ if (!fbPage.isClosed()) {
276
+ ctx.logger('haulmp').info('使用已就绪兜底页面')
277
+ return fbPage
278
+ }
279
+ } catch (e) { /* ignore */ }
280
+ fbPage = null
281
+ fbReady = false
282
+ }
283
+ if (fbPage) {
284
+ try { fbPage.close().catch(() => {}) } catch (e) { /* ignore */ }
285
+ fbPage = null
286
+ fbReady = false
287
+ }
288
+ ctx.logger('haulmp').info('重建兜底页面…')
289
+ const t0 = Date.now()
290
+ fbPage = await puppeteer.page()
291
+ try {
292
+ fbPage.on('pageerror', (err) => {
293
+ ctx.logger('haulmp').warn('兜底页面脚本错误: %s', err && err.message ? err.message : String(err))
294
+ })
295
+ await fbPage.setViewport({ width: 760, height: 560, deviceScaleFactor: 2 })
296
+ await fbPage.goto(`http://127.0.0.1:${port}/fallback.html`, { waitUntil: 'domcontentloaded', timeout: 20000 })
297
+ await fbPage.waitForFunction(() => window.__ready === true && typeof window.drawView === 'function', { timeout: 15000 })
298
+ fbReady = true
299
+ ctx.logger('haulmp').info('兜底页面构建完成 %dms', Date.now() - t0)
300
+ return fbPage
301
+ } catch (e) {
302
+ ctx.logger('haulmp').warn('兜底页面初始化失败: %s', e.message)
303
+ try { fbPage.close().catch(() => {}) } catch (err) { /* ignore */ }
304
+ fbPage = null
305
+ fbReady = false
306
+ return null
307
+ }
308
+ }
309
+
310
+ async function fallbackShot(ctx, info, nearby) {
311
+ try {
312
+ const page = await getFallbackPage(ctx)
313
+ if (!page) return null
314
+ const reset = () => {
315
+ try { page.close().catch(() => {}) } catch (e) { /* ignore */ }
316
+ fbPage = null
317
+ fbReady = false
318
+ }
319
+ await withTimeout(
320
+ page.evaluate((data) => window.drawView(data), {
321
+ info,
322
+ nearby: nearby.map((n) => ({ lng: n.lon, lat: n.lat, name: n.name, km: n.km })),
323
+ self: { lng: info.lng, lat: info.lat, bearing: info.bearing },
324
+ }),
325
+ 10000,
326
+ reset,
327
+ )
328
+ const el = await page.$('#shot')
329
+ const buf = await withTimeout(
330
+ el ? el.screenshot({ encoding: 'binary', type: 'png' }) : page.screenshot({ encoding: 'binary', type: 'png' }),
331
+ 10000,
332
+ reset,
333
+ )
334
+ ctx.logger('haulmp').info('兜底方位图渲染完成')
335
+ return buf
336
+ } catch (e) {
337
+ ctx.logger('haulmp').warn('兜底方位图渲染失败: %s', e.message)
338
+ if (fbPage) {
339
+ try { fbPage.close().catch(() => {}) } catch (err) { /* ignore */ }
340
+ fbPage = null
341
+ fbReady = false
342
+ }
343
+ return null
344
+ }
345
+ }
346
+
220
347
  async function doMapShot(ctx, info, p, nearby) {
221
348
  try {
222
349
  const page = await getMapPage(ctx)
223
- if (!page) return null
350
+ // 该环境不支持 WebGL2(无 GPU 的无头服务器)→ 退回 Canvas 方位图,仍能出图
351
+ if (!page) return await fallbackShot(ctx, info, nearby)
224
352
  const [lng, lat] = gameToLngLat(p.x, p.z)
225
353
  const resetPage = () => {
226
354
  try { page.close().catch(() => {}) } catch (e) { /* ignore */ }
@@ -371,6 +499,13 @@ function apply(ctx, config) {
371
499
  city: cityDisplay,
372
500
  dist: near.km.toFixed(0),
373
501
  id: p.id,
502
+ // 供 Canvas 兜底页使用的数值字段
503
+ lng,
504
+ lat,
505
+ bearing: bearingDeg,
506
+ bearingText,
507
+ speed: p.speed,
508
+ model,
374
509
  }
375
510
 
376
511
  let card = null
@@ -393,4 +528,4 @@ function apply(ctx, config) {
393
528
  })
394
529
  }
395
530
 
396
- module.exports = { apply, gameToLngLat, nearestCity, nearbyPlayers, loadPlaces, matchPlayer, headingBearing, mapShot, syncTileVersion }
531
+ module.exports = { apply, gameToLngLat, nearestCity, nearbyPlayers, loadPlaces, matchPlayer, headingBearing, mapShot, fallbackShot, syncTileVersion }
@@ -53,7 +53,7 @@ function menuCard() {
53
53
  </style></head><body><div id="card">${CARD_DECO}
54
54
  <div class="head"><span class="title">HaulMP 功能菜单</span><span class="brand">MENU</span></div>
55
55
  <div class="mgrid">${sections}</div>
56
- <div class="foot"><span>HaulMP · 指令末尾 H / h 通用</span><span>发送「菜单」查看本菜单</span></div>
56
+ <div class="foot"><span>HaulMP · 指令末尾 H / h 通用</span><span>发送「菜单H」查看本菜单</span></div>
57
57
  </div></body></html>`
58
58
  }
59
59
 
@@ -64,13 +64,14 @@ function menuText() {
64
64
  for (const [cmd, desc] of items) lines.push(` · ${cmd} — ${desc}`)
65
65
  }
66
66
  lines.push('━━━━━━━━━━━━')
67
- lines.push('(指令末尾 H/h 通用,菜单无需加 H)')
67
+ lines.push('(指令末尾 H/h 通用;看本菜单请发送「菜单H」)')
68
68
  return lines.join('\n')
69
69
  }
70
70
 
71
71
  function apply(ctx, config) {
72
- ctx.command('菜单h', 'HaulMP 功能菜单')
73
- .alias('menuh')
72
+ // 只注册带 H 的写法(菜单H / 菜单h / menuh),不注册裸「菜单」——避免与其它插件的同名指令冲突
73
+ ctx.command('菜单H', 'HaulMP 功能菜单')
74
+ .alias('menuh', '菜单h')
74
75
  .action(async ({ session }) => {
75
76
  if (!session) return
76
77
  if (!isText(config, 'menu')) {
package/lib/index.js CHANGED
@@ -48,7 +48,7 @@ const Config = Schema.object({
48
48
  status: cardMode('服务器状态'),
49
49
  companyBind: cardMode('车队绑定'),
50
50
  companyBoard: cardMode('今日里程'),
51
- menu: cardMode('菜单'),
51
+ menu: cardMode('菜单H'),
52
52
  changelog: cardMode('客户端更新推送'),
53
53
  announce: cardMode('车队公告推送'),
54
54
  locate: cardMode('实时定位'),
@@ -0,0 +1,241 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>HaulMP 定位(Canvas 兜底)</title>
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+ body {
9
+ background-color: #eef2fc;
10
+ background-image:
11
+ radial-gradient(48% 42% at 8% 2%, rgba(129,140,248,.62), transparent 64%),
12
+ radial-gradient(44% 40% at 94% 6%, rgba(56,189,248,.52), transparent 62%),
13
+ radial-gradient(50% 46% at 90% 94%, rgba(52,211,153,.48), transparent 64%),
14
+ radial-gradient(46% 40% at 6% 98%, rgba(251,191,36,.44), transparent 62%),
15
+ radial-gradient(40% 36% at 50% 50%, rgba(244,114,182,.30), transparent 66%);
16
+ font-family: "Segoe UI","Droid Sans Fallback","Noto Sans CJK SC","Microsoft YaHei",sans-serif;
17
+ }
18
+ #shot {
19
+ --t1: #16203a; --t2: #48587a; --t3: #7b8aa5;
20
+ --glass: rgba(255,255,255,.62); --glass-2: rgba(255,255,255,.52);
21
+ --stroke: rgba(255,255,255,.95); --edge: rgba(124,144,196,.22);
22
+ --shadow: 0 12px 30px -16px rgba(58,76,140,.36), inset 0 1px 0 rgba(255,255,255,.95);
23
+ width: 720px; padding: 22px; color: var(--t2);
24
+ }
25
+ .card {
26
+ border-radius: 26px; padding: 20px;
27
+ background: linear-gradient(150deg, rgba(255,255,255,.60), rgba(248,251,255,.38) 48%, rgba(255,255,255,.54));
28
+ border: 1px solid var(--stroke);
29
+ box-shadow: 0 26px 64px -24px rgba(58,76,140,.50), inset 0 1px 0 rgba(255,255,255,.98);
30
+ backdrop-filter: saturate(190%) blur(26px); -webkit-backdrop-filter: saturate(190%) blur(26px);
31
+ position: relative; overflow: hidden;
32
+ }
33
+ .glow { position: absolute; top: 0; left: 0; right: 0; height: 4px;
34
+ background: linear-gradient(90deg, transparent, #4f46e5 26%, #0ea5e9 74%, transparent); opacity: .9; }
35
+ .head { display: flex; align-items: center; gap: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--edge); }
36
+ .ava { width: 58px; height: 58px; border-radius: 18px; flex: none; display: flex; align-items: center; justify-content: center;
37
+ font-size: 24px; font-weight: 700; color: #3730a3; border: 1px solid var(--stroke);
38
+ background: linear-gradient(150deg, rgba(99,102,241,.30), rgba(14,165,233,.24)); box-shadow: var(--shadow); }
39
+ .who { flex: 1; min-width: 0; }
40
+ .name { font-size: 21px; font-weight: 700; color: var(--t1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
41
+ .sub { font-size: 13px; color: var(--t3); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
42
+ .loc { text-align: right; flex: none; }
43
+ .city { font-size: 17px; font-weight: 700; color: #3730a3; }
44
+ .dist { font-size: 12px; color: var(--t3); margin-top: 3px; }
45
+ .chips { display: flex; gap: 8px; margin-top: 14px; }
46
+ .chip { font-size: 12.5px; color: #3730a3; background: var(--glass-2); border: 1px solid var(--stroke);
47
+ border-radius: 999px; padding: 4px 12px; box-shadow: var(--shadow); white-space: nowrap; }
48
+ .chip b { color: var(--t1); }
49
+ .plotwrap { margin-top: 14px; border-radius: 20px; border: 1px solid var(--stroke); background: var(--glass-2);
50
+ box-shadow: var(--shadow); overflow: hidden; }
51
+ canvas { display: block; }
52
+ .foot { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--edge);
53
+ display: flex; justify-content: space-between; font-size: 11.5px; color: var(--t3); }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <div id="shot">
58
+ <div class="card">
59
+ <div class="glow"></div>
60
+ <div class="head">
61
+ <div class="ava" id="ava">?</div>
62
+ <div class="who">
63
+ <div class="name" id="name">-</div>
64
+ <div class="sub" id="sub"></div>
65
+ </div>
66
+ <div class="loc">
67
+ <div class="city" id="city">-</div>
68
+ <div class="dist" id="dist"></div>
69
+ </div>
70
+ </div>
71
+ <div class="chips" id="chips"></div>
72
+ <div class="plotwrap"><canvas id="plot" width="1336" height="600" style="width:668px;height:300px"></canvas></div>
73
+ <div class="foot"><span id="near">周边 0 人</span><span id="fid">HaulMP</span></div>
74
+ </div>
75
+ </div>
76
+ <script>
77
+ window.__ready = true
78
+
79
+ var RANGE = 30 // 图幅半径(km)
80
+ var PAD = 34 // 画布内边距
81
+ var W = 668, H = 300
82
+
83
+ function drawRadar(data) {
84
+ var cv = document.getElementById('plot')
85
+ var ctx = cv.getContext('2d')
86
+ var S = 2
87
+ ctx.setTransform(S, 0, 0, S, 0, 0)
88
+ ctx.clearRect(0, 0, W, H)
89
+
90
+ var cx = W / 2, cy = H / 2
91
+ var R = Math.min(W, H) / 2 - PAD
92
+ var scale = R / RANGE
93
+
94
+ // 量程环
95
+ ctx.save()
96
+ for (var i = 1; i <= 3; i++) {
97
+ var r = R * i / 3
98
+ ctx.beginPath()
99
+ ctx.arc(cx, cy, r, 0, Math.PI * 2)
100
+ ctx.setLineDash(i === 3 ? [] : [4, 5])
101
+ ctx.lineWidth = 1
102
+ ctx.strokeStyle = i === 3 ? 'rgba(124,144,196,.45)' : 'rgba(124,144,196,.28)'
103
+ ctx.stroke()
104
+ ctx.setLineDash([])
105
+ ctx.font = '11px "Microsoft YaHei",sans-serif'
106
+ ctx.fillStyle = 'rgba(123,138,165,.95)'
107
+ ctx.textAlign = 'left'
108
+ ctx.fillText((RANGE * i / 3) + ' km', cx + r + 5, cy - 4)
109
+ }
110
+ // 十字轴
111
+ ctx.beginPath()
112
+ ctx.moveTo(cx - R, cy); ctx.lineTo(cx + R, cy)
113
+ ctx.moveTo(cx, cy - R); ctx.lineTo(cx, cy + R)
114
+ ctx.strokeStyle = 'rgba(124,144,196,.30)'
115
+ ctx.lineWidth = 1
116
+ ctx.stroke()
117
+ // 方位
118
+ ctx.font = '11px "Microsoft YaHei",sans-serif'
119
+ ctx.fillStyle = 'rgba(72,88,122,.9)'
120
+ ctx.textAlign = 'center'
121
+ ctx.fillText('北', cx, cy - R - 10)
122
+ ctx.fillText('南', cx, cy + R + 15)
123
+ ctx.textAlign = 'right'; ctx.fillText('西', cx - R - 5, cy + 4)
124
+ ctx.textAlign = 'left'; ctx.fillText('东', cx + R + 5, cy + 4)
125
+ ctx.restore()
126
+
127
+ // 相对坐标(等距圆柱近似,km)
128
+ var sx = data.self.lng != null ? data.self.lng : data.self.lon
129
+ var sy = data.self.lat
130
+ var kx = 111.32 * Math.cos(sy * Math.PI / 180)
131
+ var ky = 111.32
132
+ function pt(lon, lat) {
133
+ return [cx + (lon - sx) * kx * scale, cy - (lat - sy) * ky * scale]
134
+ }
135
+
136
+ // 附近玩家
137
+ var nearby = data.nearby || []
138
+ for (var j = 0; j < nearby.length; j++) {
139
+ var n = nearby[j]
140
+ var q = pt(n.lng, n.lat)
141
+ ctx.beginPath()
142
+ ctx.arc(q[0], q[1], 5, 0, Math.PI * 2)
143
+ ctx.fillStyle = '#0ea5e9'
144
+ ctx.fill()
145
+ ctx.lineWidth = 2
146
+ ctx.strokeStyle = 'rgba(255,255,255,.95)'
147
+ ctx.stroke()
148
+
149
+ var label = n.name + (n.km != null ? ' · ' + Number(n.km).toFixed(1) + ' km' : '')
150
+ ctx.font = '11px "Microsoft YaHei",sans-serif'
151
+ var tw = ctx.measureText(label).width
152
+ var lx = q[0] + 9, ly = q[1] - 20
153
+ if (lx + tw + 12 > W - 6) lx = q[0] - tw - 21
154
+ if (ly < 4) ly = q[1] + 8
155
+ ctx.fillStyle = 'rgba(255,255,255,.88)'
156
+ ctx.strokeStyle = 'rgba(124,144,196,.30)'
157
+ ctx.lineWidth = 1
158
+ roundRect(ctx, lx, ly, tw + 12, 19, 9)
159
+ ctx.fill(); ctx.stroke()
160
+ ctx.fillStyle = '#48587a'
161
+ ctx.textAlign = 'left'
162
+ ctx.fillText(label, lx + 6, ly + 13.5)
163
+ }
164
+
165
+ // 自己(中心标记 + 朝向箭头)
166
+ ctx.save()
167
+ ctx.translate(cx, cy)
168
+ var bearing = Number(data.self.bearing)
169
+ if (isFinite(bearing)) {
170
+ ctx.save()
171
+ ctx.rotate(bearing * Math.PI / 180)
172
+ ctx.beginPath()
173
+ ctx.moveTo(0, -30)
174
+ ctx.lineTo(-9, -11)
175
+ ctx.lineTo(9, -11)
176
+ ctx.closePath()
177
+ ctx.fillStyle = 'rgba(79,70,229,.85)'
178
+ ctx.fill()
179
+ ctx.restore()
180
+ }
181
+ ctx.beginPath()
182
+ ctx.arc(0, 0, 26, 0, Math.PI * 2)
183
+ ctx.fillStyle = 'rgba(79,70,229,.14)'
184
+ ctx.fill()
185
+ ctx.beginPath()
186
+ ctx.arc(0, 0, 8, 0, Math.PI * 2)
187
+ ctx.fillStyle = '#4f46e5'
188
+ ctx.fill()
189
+ ctx.lineWidth = 2.5
190
+ ctx.strokeStyle = 'rgba(255,255,255,.95)'
191
+ ctx.stroke()
192
+ ctx.restore()
193
+
194
+ if (!nearby.length) {
195
+ ctx.font = '13px "Microsoft YaHei",sans-serif'
196
+ ctx.fillStyle = 'rgba(123,138,165,.95)'
197
+ ctx.textAlign = 'center'
198
+ ctx.fillText('30 km 内暂时没有其他玩家', cx, cy + R - 14)
199
+ }
200
+ }
201
+
202
+ function roundRect(ctx, x, y, w, h, r) {
203
+ ctx.beginPath()
204
+ ctx.moveTo(x + r, y)
205
+ ctx.lineTo(x + w - r, y)
206
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r)
207
+ ctx.lineTo(x + w, y + h - r)
208
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h)
209
+ ctx.lineTo(x + r, y + h)
210
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r)
211
+ ctx.lineTo(x, y + r)
212
+ ctx.quadraticCurveTo(x, y, x + r, y)
213
+ ctx.closePath()
214
+ }
215
+
216
+ window.drawView = function (data) {
217
+ var info = data.info || {}
218
+ document.getElementById('ava').textContent = info.initial || '?'
219
+ document.getElementById('name').textContent = info.name || '-'
220
+ document.getElementById('sub').textContent = info.sub || ''
221
+ document.getElementById('city').textContent = info.city || '-'
222
+ document.getElementById('dist').textContent = '距市中心 ' + (info.dist != null ? info.dist : '—') + ' km'
223
+
224
+ var chips = []
225
+ if (info.speed != null) chips.push('速度 <b>' + info.speed + ' km/h</b>')
226
+ if (info.bearingText) chips.push('朝向 <b>' + info.bearingText + '</b>')
227
+ chips.push('车型 <b>' + (info.model || '未知') + '</b>')
228
+ document.getElementById('chips').innerHTML = chips.map(function (c) {
229
+ return '<span class="chip">' + c + '</span>'
230
+ }).join('')
231
+
232
+ var n = (data.nearby || []).length
233
+ document.getElementById('near').textContent = '周边 ' + n + ' 人(' + RANGE + ' km 内)'
234
+ document.getElementById('fid').textContent = 'ID ' + (info.id != null ? info.id : '—') + ' · HaulMP · 方位图模式'
235
+
236
+ drawRadar(data)
237
+ return true
238
+ }
239
+ </script>
240
+ </body>
241
+ </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-haulmp-tools",
3
3
  "description": "欧卡HaulMP联机平台信息查询",
4
- "version": "1.0.2",
4
+ "version": "1.0.3",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>"
7
7
  ],
package/readme.md CHANGED
@@ -25,7 +25,7 @@
25
25
  | 车队 | `绑定车队H <车队ID或链接>` | 绑定本群车队(仅队长可绑定) |
26
26
  | 车队 | `解绑车队H` | 解除本群车队绑定(绑定人或队长) |
27
27
  | 车队 | `今日里程H` | 本群车队当天里程排行(每日自动建立基准) |
28
- | 其他 | `菜单` | 功能菜单 |
28
+ | 其他 | `菜单H` | 功能菜单(只注册带 H 的写法,避免与其它插件的「菜单」指令冲突) |
29
29
 
30
30
  自动推送(无需指令):
31
31
 
@@ -78,6 +78,42 @@ src/
78
78
  - 数据库服务(`database`)
79
79
  - `puppeteer` 服务(卡片图片渲染;未启用时自动退回文字输出)
80
80
 
81
+ ## 部署注意:定位地图需要 WebGL2
82
+
83
+ `定位H` 的地图由 maplibre 渲染,**需要浏览器支持 WebGL2**。无头 Linux 服务器默认没有 GPU,
84
+ 浏览器会以「WebGL2 is required to display this map / GPUInitializationError」失败,
85
+ 此时定位指令会自动退回文字结果(地图页面 30 分钟内不再重试)。
86
+
87
+ 需要地图出图,请在 **puppeteer 服务的配置**里加上软件渲染参数:
88
+
89
+ ```yaml
90
+ puppeteer:
91
+ args:
92
+ - --no-sandbox
93
+ - --disable-setuid-sandbox
94
+ - --disable-dev-shm-usage
95
+ - --use-gl=angle
96
+ - --use-angle=swiftshader
97
+ - --enable-unsafe-swiftshader
98
+ - --ignore-gpu-blocklist
99
+ ```
100
+
101
+ - 新版 Chrome 已不接受 `--use-gl=swiftshader`,必须用 `--use-gl=angle --use-angle=swiftshader`。
102
+ - `--enable-unsafe-swiftshader` 不能省:Chrome 默认禁止用 SwiftShader 提供 WebGL,缺了它拿到的仍是空 canvas。
103
+ - **不要**加 `--disable-gpu`,那会彻底关掉 WebGL。
104
+ - `headless` 需保持 `true`(新版无头模式);远程浏览器模式(`remote: true`)要把参数加在**远端浏览器的启动命令**上。
105
+
106
+ ### 没有 WebGL 也能出图:Canvas 方位图模式
107
+
108
+ 若服务器确实无法开启软件渲染(WebGL/WebGL2 都拿不到),`定位H` **会自动退回纯 Canvas 2D 的方位图**
109
+ (不依赖任何 GL,任何无头环境都能画):
110
+
111
+ - 中心是玩家本人(带朝向箭头),周围 30 km 内的其他玩家按真实相对方位与距离落点并标注名字/距离;
112
+ - 环形量程 10 / 20 / 30 km、十字方位轴、玻璃质感信息栏(速度 / 朝向 / 车型 / 所在城市 / ID);
113
+ - 与真实地图的差别:**没有路网**,只有相对方位关系(页面右下角标注「方位图模式」)。
114
+
115
+ 一旦 WebGL 可用(例如按上面配好启动参数),会自动改回真实地图,无需改任何配置。
116
+
81
117
  ## License
82
118
 
83
119
  [MIT](./LICENSE)