koishi-plugin-ets2-tools-tmp 3.4.4 → 3.4.5

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.
@@ -30,10 +30,10 @@ module.exports = async (ctx, session, rankingType, target) => {
30
30
  if (ownerName) tmpIdPicker.cacheAtName(session.platform, ownerKey, ownerName);
31
31
  }
32
32
  // 排行榜只高亮一个人:直接用默认绑定,不弹序号询问
33
+ // 这里只用 tmp_id,高亮名字来自排行榜接口返回的数据 → 不必刷新 tmp_name
33
34
  const bindings = await guildBind.listOf(ctx.database, session.platform, ownerKey);
34
35
  let bindTmpId = null;
35
36
  if (bindings.length > 0) {
36
- await guildBind.ensureNames(ctx, bindings);
37
37
  bindTmpId = String((bindings.find(b => guildBind.isDefault(b)) || bindings[0]).tmp_id);
38
38
  }
39
39
  let playerMileageRanking = null;
@@ -3,12 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VersionCheckService = void 0;
4
4
 
5
5
  const koishi_1 = require("koishi");
6
+ const path_1 = require("path");
7
+ const common_1 = require("../util/common");
8
+
9
+ // TMP 官方文件清单(含每个文件的 MD5),用于比对版本更新前后哪些文件被替换
10
+ const FILES_API = "https://update.ets2mp.com/files.json";
11
+ // 文字通知里最多列出多少个文件,超出只显示数量,避免消息过长
12
+ const MAX_LISTED_FILES = 30;
6
13
 
7
14
  class VersionCheckService {
8
15
  constructor(ctx, config) {
9
16
  this.ctx = ctx;
10
17
  this.cfg = config;
11
18
  this.currentVersion = null;
19
+ // 上一次检查时的文件清单快照(FilePath -> {md5, type}),用于比对文件变化
20
+ this.fileManifest = null;
12
21
  this.timers = [];
13
22
  this.logger = this.initLogger();
14
23
  }
@@ -71,6 +80,7 @@ class VersionCheckService {
71
80
  if (response && response.name) {
72
81
  const versionInfo = {
73
82
  name: response.name,
83
+ stage: response.stage,
74
84
  time: this.convertToUTC8(response.time),
75
85
  supported_game_version: response.supported_game_version,
76
86
  supported_ats_game_version: response.supported_ats_game_version
@@ -80,7 +90,14 @@ class VersionCheckService {
80
90
 
81
91
  if (this.currentVersion && this.currentVersion.name !== versionInfo.name) {
82
92
  this.logger.info(`检测到版本更新: ${this.currentVersion.name} -> ${versionInfo.name}`);
83
- await this.sendVersionUpdateNotification(this.currentVersion, versionInfo);
93
+
94
+ // 比对文件 MD5,找出本次更新实际替换了哪些文件
95
+ const changedFiles = await this.detectChangedFiles();
96
+ if (changedFiles.length > 0) {
97
+ this.logger.info(`本次更新涉及 ${changedFiles.length} 个文件变化`);
98
+ }
99
+
100
+ await this.sendVersionUpdateNotification(this.currentVersion, versionInfo, changedFiles);
84
101
  }
85
102
 
86
103
  this.currentVersion = versionInfo;
@@ -92,6 +109,108 @@ class VersionCheckService {
92
109
  }
93
110
  }
94
111
 
112
+ /**
113
+ * 拉取官方文件清单并转成 { FilePath -> Md5 } 映射。
114
+ * 失败时返回 null(调用方据此区分“没变化”和“查不到”)。
115
+ */
116
+ async fetchFileManifest() {
117
+ try {
118
+ const response = await this.ctx.http.get(FILES_API, { timeout: 15000 });
119
+ const files = response && Array.isArray(response.Files) ? response.Files : null;
120
+ if (!files) {
121
+ this.logger.warn("文件清单格式不正确,跳过文件变化比对");
122
+ return null;
123
+ }
124
+
125
+ const manifest = new Map();
126
+ for (const file of files) {
127
+ if (file && file.FilePath) {
128
+ manifest.set(file.FilePath, {
129
+ md5: String(file.Md5 || "").toLowerCase(),
130
+ type: file.Type || ""
131
+ });
132
+ }
133
+ }
134
+ this.logger.debug(`文件清单已加载,共 ${manifest.size} 个文件`);
135
+ return manifest;
136
+ } catch (error) {
137
+ this.logger.warn(`获取文件清单失败: ${error.message}`);
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 比对「上一次版本的文件清单」与「当前清单」,返回 MD5 变化的文件。
144
+ *
145
+ * 说明:files.json 只提供**当前**版本的文件列表,没有历史版本归档
146
+ * (/0.7.10.0/files.json 等路径均 404)。所以只能在两次检查之间自己留存快照:
147
+ * - 有上一次快照 → 逐文件比对 MD5,新增/删除/改动都算变化;
148
+ * - 没有快照(首次运行或重启)→ 退化为“清单中所有文件都视为本次更新内容”,
149
+ * 并在结果里标注 snapshotMissing,由文案说明这是完整清单而非差异。
150
+ *
151
+ * @returns {Array|null} 变化列表;清单获取失败时返回 null(区别于“没有变化”的空数组)
152
+ */
153
+ async detectChangedFiles() {
154
+ const current = await this.fetchFileManifest();
155
+ if (!current) {
156
+ // 查不到清单 ≠ 没有变化,不能返回 [],否则会误报“未检测到文件变化”
157
+ return null;
158
+ }
159
+
160
+ const previous = this.fileManifest;
161
+ this.fileManifest = current;
162
+
163
+ if (!previous) {
164
+ this.logger.debug("没有上一次的文件清单快照,本次按完整清单输出");
165
+ return this.buildFullFileList(current, true);
166
+ }
167
+
168
+ const changed = [];
169
+ for (const [filePath, info] of current) {
170
+ const old = previous.get(filePath);
171
+ if (!old) {
172
+ changed.push({ path: filePath, type: info.type, kind: 'added' });
173
+ } else if (old.md5 !== info.md5) {
174
+ changed.push({ path: filePath, type: info.type, kind: 'modified' });
175
+ }
176
+ }
177
+ // 上一次有、这次没有 → 被移除
178
+ for (const [filePath, info] of previous) {
179
+ if (!current.has(filePath)) {
180
+ changed.push({ path: filePath, type: info.type, kind: 'removed' });
181
+ }
182
+ }
183
+
184
+ // 稳定排序:先 ets2/ats,再 system;同类按路径
185
+ const typeOrder = { ets2: 0, ats: 1, system: 2 };
186
+ changed.sort((a, b) => {
187
+ const ta = typeOrder[a.type] !== undefined ? typeOrder[a.type] : 9;
188
+ const tb = typeOrder[b.type] !== undefined ? typeOrder[b.type] : 9;
189
+ if (ta !== tb) return ta - tb;
190
+ return a.path.localeCompare(b.path);
191
+ });
192
+
193
+ return changed;
194
+ }
195
+
196
+ /** 没有快照时的退化结果:清单内所有文件 */
197
+ buildFullFileList(manifest, snapshotMissing) {
198
+ const list = [];
199
+ for (const [filePath, info] of manifest) {
200
+ list.push({ path: filePath, type: info.type, kind: 'unknown' });
201
+ }
202
+ const typeOrder = { ets2: 0, ats: 1, system: 2 };
203
+ list.sort((a, b) => {
204
+ const ta = typeOrder[a.type] !== undefined ? typeOrder[a.type] : 9;
205
+ const tb = typeOrder[b.type] !== undefined ? typeOrder[b.type] : 9;
206
+ if (ta !== tb) return ta - tb;
207
+ return a.path.localeCompare(b.path);
208
+ });
209
+ // 用不可枚举标记,避免污染后续 JSON 序列化
210
+ Object.defineProperty(list, 'snapshotMissing', { value: !!snapshotMissing, enumerable: false });
211
+ return list;
212
+ }
213
+
95
214
  convertToUTC8(utcTimeString) {
96
215
  try {
97
216
  const utcDate = new Date(utcTimeString);
@@ -109,28 +228,175 @@ class VersionCheckService {
109
228
  }
110
229
  }
111
230
 
112
- async sendVersionUpdateNotification(oldVersion, newVersion) {
113
- const message = `📢 TMP版本更新通知
231
+ async sendVersionUpdateNotification(oldVersion, newVersion, changedFiles) {
232
+ // null = 清单查询失败;数组 = 比对结果(可能为空)
233
+ const fileCount = changedFiles === null ? '查询失败' : `${changedFiles.length}`;
234
+ this.logger.info(`发送版本更新通知到 ${this.cfg.groups.length} 个群组(变化文件: ${fileCount})`);
114
235
 
115
- 旧版本: ${oldVersion.name}
116
- 新版本: ${newVersion.name}
117
-
118
- 更新时间: ${newVersion.time}(UTC+8)
119
- 欧卡支持版本: ${newVersion.supported_game_version}
120
- 美卡支持版本: ${newVersion.supported_ats_game_version}`;
121
-
122
- this.logger.info(`发送版本更新通知到 ${this.cfg.groups.length} 个群组`);
236
+ // 内容只构建一次(图片渲染较慢,不要每个群各渲染一遍)
237
+ const message = await this.buildUpdateContent(oldVersion, newVersion, changedFiles);
238
+ const isImage = typeof message === "object" && message !== null;
123
239
 
124
240
  for (const groupId of this.cfg.groups) {
125
241
  try {
126
242
  await this.sendToGroup(groupId, message);
127
- this.logger.info(`已发送版本更新通知到群组 ${groupId}`);
243
+ this.logger.info(`已发送版本更新通知到群组 ${groupId}(${isImage ? "图片" : "文字"})`);
128
244
  } catch (error) {
129
245
  this.logger.error(`发送版本更新通知到群组 ${groupId} 失败:`, error.message);
130
246
  }
131
247
  }
132
248
  }
133
249
 
250
+ /**
251
+ * 构建通知内容。notifyType:1=文字(默认),2=图片。
252
+ * 图片渲染失败(如未启用 puppeteer)会**自动回退为文字**,保证通知不丢。
253
+ * @returns 文字字符串,或图片 segment 元素
254
+ */
255
+ async buildUpdateContent(oldVersion, newVersion, changedFiles) {
256
+ const notifyType = Number(this.cfg.notifyType) || 1;
257
+ if (notifyType === 2) {
258
+ const image = await this.renderUpdateImage(oldVersion, newVersion, changedFiles);
259
+ if (image) {
260
+ return image;
261
+ }
262
+ }
263
+ return this.buildUpdateText(oldVersion, newVersion, changedFiles);
264
+ }
265
+
266
+ /**
267
+ * 插件配置里的「更新通知附加文本」,原样追加在通知末尾。
268
+ * 支持 \n 转义换行,方便在配置面板里写多行。
269
+ */
270
+ getExtraText() {
271
+ const raw = this.cfg.extraText;
272
+ if (raw == null) return "";
273
+ const text = String(raw).trim();
274
+ if (!text) return "";
275
+ return text.replace(/\\n/g, "\n");
276
+ }
277
+
278
+ buildUpdateText(oldVersion, newVersion, changedFiles) {
279
+ let message = `📢 TMP版本更新通知
280
+
281
+ 旧版本: ${oldVersion.name}
282
+ 新版本: ${newVersion.name}
283
+
284
+ 更新时间: ${newVersion.time}(UTC+8)
285
+ 欧卡支持版本: ${this.formatGameVersion(oldVersion.supported_game_version, newVersion.supported_game_version)}
286
+ 美卡支持版本: ${this.formatGameVersion(oldVersion.supported_ats_game_version, newVersion.supported_ats_game_version)}`;
287
+
288
+ message += this.formatChangedFiles(changedFiles);
289
+
290
+ const extra = this.getExtraText();
291
+ if (extra) {
292
+ message += `\n\n${extra}`;
293
+ }
294
+ return message;
295
+ }
296
+
297
+ /**
298
+ * 把变化文件列表拼成消息片段。
299
+ * - files 为 null → 清单获取失败,明确说明“查询失败”,不冒充“无变化”
300
+ * - 空数组 → 确实没有文件变化
301
+ * - 超过 MAX_LISTED_FILES 条 → 只列前若干条 + 「等共 N 个」
302
+ */
303
+ formatChangedFiles(files) {
304
+ if (files === null) {
305
+ return `\n\n更新文件: 文件清单获取失败,无法比对变化`;
306
+ }
307
+ if (!files || files.length === 0) {
308
+ return `\n\n更新文件: 未检测到文件变化`;
309
+ }
310
+
311
+ const snapshotMissing = files.snapshotMissing === true;
312
+ const title = snapshotMissing
313
+ ? `更新文件(共 ${files.length} 个,未留存上次清单,以下为完整列表)`
314
+ : `更新文件(共 ${files.length} 个)`;
315
+
316
+ const shown = files.slice(0, MAX_LISTED_FILES);
317
+ const lines = shown.map((file) => {
318
+ const mark = file.kind === 'added' ? '[新增] '
319
+ : file.kind === 'removed' ? '[移除] '
320
+ : file.kind === 'modified' ? '[更新] '
321
+ : '';
322
+ return `· ${mark}${file.path}`;
323
+ });
324
+
325
+ let text = `\n\n${title}\n${lines.join('\n')}`;
326
+ if (files.length > shown.length) {
327
+ text += `\n… 等共 ${files.length} 个文件`;
328
+ }
329
+ return text;
330
+ }
331
+
332
+ /**
333
+ * 游戏支持版本:有变化时显示「旧 → 新」,没变则只显示当前值。
334
+ */
335
+ formatGameVersion(oldValue, newValue) {
336
+ if (oldValue && newValue && String(oldValue) !== String(newValue)) {
337
+ return `${oldValue} → ${newValue}`;
338
+ }
339
+ return newValue || oldValue || "未知";
340
+ }
341
+
342
+ /**
343
+ * 用 puppeteer 渲染版本更新卡片
344
+ * @returns 图片 segment 元素;渲染不可用时返回 null,由调用方回退文字
345
+ */
346
+ async renderUpdateImage(oldVersion, newVersion, changedFiles) {
347
+ if (!this.ctx.puppeteer) {
348
+ this.logger.warn("未启用 puppeteer,版本更新通知回退为文字形式");
349
+ return null;
350
+ }
351
+ let page;
352
+ try {
353
+ // null = 清单查询失败,交给模板显示对应提示
354
+ const files = changedFiles === null ? [] : (Array.isArray(changedFiles) ? changedFiles : []);
355
+ const data = {
356
+ oldName: oldVersion.name,
357
+ newName: newVersion.name,
358
+ time: newVersion.time,
359
+ stage: newVersion.stage,
360
+ // 游戏支持版本也带上旧值,卡片才能显示「旧 → 新」对比
361
+ etsOld: oldVersion.supported_game_version,
362
+ etsNew: newVersion.supported_game_version,
363
+ atsOld: oldVersion.supported_ats_game_version,
364
+ atsNew: newVersion.supported_ats_game_version,
365
+ // 变化文件:路径 + 变化类型,卡片里按组展示
366
+ files: files.map((file) => ({
367
+ path: file.path,
368
+ kind: file.kind || 'unknown',
369
+ type: file.type || ''
370
+ })),
371
+ filesSnapshotMissing: changedFiles !== null && changedFiles.snapshotMissing === true,
372
+ filesUnavailable: changedFiles === null,
373
+ extraText: this.getExtraText()
374
+ };
375
+ const htmlPath = path_1.resolve(__dirname, "../resource/version-update.html");
376
+ this.logger.debug(`渲染版本更新卡片: ${htmlPath}`);
377
+
378
+ page = await this.ctx.puppeteer.page();
379
+ await page.setViewport({ width: 560, height: 900, deviceScaleFactor: 2 });
380
+ await page.goto(`file:///${htmlPath}`);
381
+ await page.evaluate(`setData(${JSON.stringify(data)})`);
382
+ await common_1.sleep(100);
383
+ await page.waitForNetworkIdle();
384
+
385
+ const element = await page.$("#container");
386
+ if (!element) {
387
+ throw new Error("未找到 #container");
388
+ }
389
+ return koishi_1.segment.image(await element.screenshot({ encoding: "binary" }), "image/jpeg");
390
+ } catch (error) {
391
+ this.logger.error(`渲染版本更新卡片失败,回退为文字形式: ${error.message}`);
392
+ return null;
393
+ } finally {
394
+ if (page) {
395
+ try { await page.close(); } catch { }
396
+ }
397
+ }
398
+ }
399
+
134
400
  async sendToGroup(groupId, message) {
135
401
  const onebotBots = this.ctx.bots.filter((bot) => {
136
402
  return bot.platform === "onebot";
@@ -34,10 +34,10 @@ module.exports = async (ctx, cfg, session, rankingType, target) => {
34
34
  if (ownerName) tmpIdPicker.cacheAtName(session.platform, ownerKey, ownerName);
35
35
  }
36
36
  // 排行榜只高亮一个人:直接用默认绑定,不弹序号询问
37
+ // 这里只用 tmp_id,高亮名字来自排行榜接口返回的数据 → 不必刷新 tmp_name
37
38
  const bindings = await guildBind.listOf(ctx.database, session.platform, ownerKey);
38
39
  let bindTmpId = null;
39
40
  if (bindings.length > 0) {
40
- await guildBind.ensureNames(ctx, bindings);
41
41
  bindTmpId = String((bindings.find(b => guildBind.isDefault(b)) || bindings[0]).tmp_id);
42
42
  }
43
43
  let playerMileageRanking = null;
@@ -7,23 +7,30 @@ const evmOpenApi = require('../api/evmOpenApi');
7
7
  */
8
8
  module.exports = {
9
9
  /**
10
- * 补全缺失的玩家名(旧版本绑定的数据没有 tmp_name):查接口并写回数据库。
11
- * 并发补查,单个失败保持未知(下次遇到再补)。
10
+ * 刷新玩家名(**总是查接口**,用于玩家改名后的及时同步):
11
+ * - 并发查询传入的所有行(含缺名的旧数据);
12
+ * - 名字确实变了才写库,避免无谓的写操作;
13
+ * - 单个查询失败保留旧名(不把已有名字清空),下次再刷新。
14
+ * 注意:只在**要显示名字**的地方调用(我的绑定 / 序号列表 / 解绑·设为默认文案),
15
+ * 不显示名字的路径(如 pickAll、排行榜)不要调用,省接口。
12
16
  * @returns 原数组(行对象已被就地更新)
13
17
  */
14
- async ensureNames(ctx, rows) {
15
- const missing = (rows || []).filter(r => !r.tmp_name);
16
- if (missing.length === 0) {
18
+ async refreshNames(ctx, rows) {
19
+ const list = (rows || []).filter(r => r && r.tmp_id != null);
20
+ if (list.length === 0) {
17
21
  return rows;
18
22
  }
19
- await Promise.all(missing.map(async (row) => {
23
+ await Promise.all(list.map(async (row) => {
20
24
  try {
21
25
  const info = await evmOpenApi.playerInfo(ctx.http, row.tmp_id);
22
26
  if (!info.error && info.data && info.data.name) {
23
- row.tmp_name = info.data.name;
24
- await ctx.database.set('tmp_guild_bind', row.id, { tmp_name: info.data.name });
27
+ const name = info.data.name;
28
+ if (row.tmp_name !== name) {
29
+ row.tmp_name = name;
30
+ await ctx.database.set('tmp_guild_bind', row.id, { tmp_name: name });
31
+ }
25
32
  }
26
- } catch { } // 查询失败保持未知,下次遇到再补
33
+ } catch { } // 查询失败保留旧名,下次再刷新
27
34
  }));
28
35
  return rows;
29
36
  },
@@ -31,7 +31,7 @@ const modelArray = {
31
31
  type: 'string',
32
32
  length: 100,
33
33
  nullable: true,
34
- comment: 'TMP玩家名称(绑定时记录,用于多绑定列表展示)'
34
+ comment: 'TMP玩家名称(列表展示用;每次展示绑定时会查接口刷新,改名可及时同步)'
35
35
  },
36
36
  is_default: {
37
37
  type: 'unsigned',
package/lib/index.js CHANGED
@@ -129,7 +129,15 @@ exports.Config = koishi_1.Schema.intersect([
129
129
 
130
130
  tmpVersionCheck: koishi_1.Schema.object({
131
131
  checkInterval: koishi_1.Schema.number().description("版本检查间隔(分钟)").default(30),
132
- groups: koishi_1.Schema.array(koishi_1.Schema.string()).role("table").description("接收版本更新通知的群组ID列表").default([])
132
+ groups: koishi_1.Schema.array(koishi_1.Schema.string()).role("table").description("接收版本更新通知的群组ID列表").default([]),
133
+ notifyType: koishi_1.Schema.union([
134
+ koishi_1.Schema.const(1).description('文字'),
135
+ koishi_1.Schema.const(2).description('图片')
136
+ ]).default(1).description('版本更新通知形式(图片较精美,需启用 puppeteer;渲染失败会自动回退文字)'),
137
+ extraText: koishi_1.Schema.string()
138
+ .role('textarea')
139
+ .description('更新通知末尾附加的文本(留空则不追加)。支持用 \\n 表示换行')
140
+ .default('')
133
141
  }).description("TMP版本更新查询配置"),
134
142
  steamApi: koishi_1.Schema.object({
135
143
  key: koishi_1.Schema.string().description("Steam API Key,用于查询游戏时长")
@@ -270,8 +278,8 @@ function registerBaseCommands(ctx, cfg) {
270
278
  if (bindings.length === 0) {
271
279
  return '还没有绑定编号,使用「绑定 tmpId」添加';
272
280
  }
273
- // 旧绑定缺名字的先补查并写回
274
- await guildBind.ensureNames(ctx, bindings);
281
+ // 展示前刷新名字(玩家改名后可及时同步)
282
+ await guildBind.refreshNames(ctx, bindings);
275
283
  const lines = bindings.map((b, i) => {
276
284
  const mark = guildBind.isDefault(b) ? '★ ' : ' ';
277
285
  return `${mark}${i + 1}. ${b.tmp_name || '未知'} (${b.tmp_id})`;
@@ -289,7 +297,7 @@ function registerBaseCommands(ctx, cfg) {
289
297
  if (!index || !index.trim()) {
290
298
  return '请输入要设为默认的序号(序号见「我的绑定」)';
291
299
  }
292
- await guildBind.ensureNames(ctx, await guildBind.list(ctx.database, session.platform, session.userId));
300
+ await guildBind.refreshNames(ctx, await guildBind.list(ctx.database, session.platform, session.userId));
293
301
  const { row, changed, total } = await guildBind.setDefaultByIndex(
294
302
  ctx, ctx.database, session.platform, session.userId, index.trim()
295
303
  );
@@ -312,7 +320,6 @@ function registerBaseCommands(ctx, cfg) {
312
320
  if (bindings.length === 0) {
313
321
  return '还没有绑定编号';
314
322
  }
315
- await guildBind.ensureNames(ctx, bindings);
316
323
  if (!index) {
317
324
  return '请输入要解绑的序号或 all(序号见「我的绑定」)';
318
325
  }
@@ -324,6 +331,8 @@ function registerBaseCommands(ctx, cfg) {
324
331
  if (isNaN(idx) || idx < 1 || idx > bindings.length) {
325
332
  return `序号无效,可用范围 1-${bindings.length}(序号见「我的绑定」)`;
326
333
  }
334
+ // 到这一步才需要显示名字(成功文案 / 顶替提示)→ 此时刷新
335
+ await guildBind.refreshNames(ctx, bindings);
327
336
  const row = bindings[idx - 1];
328
337
  const { promoted } = await guildBind.remove(ctx.database, session.platform, session.userId, row.id);
329
338
  let msg = `已解绑 ${idx}. ${row.tmp_name || '未知'} (${row.tmp_id})`;
@@ -556,6 +565,8 @@ function apply(ctx, cfg) {
556
565
  const versionCheckConfig = {
557
566
  checkInterval: cfg.tmpVersionCheck.checkInterval,
558
567
  groups: cfg.tmpVersionCheck.groups,
568
+ notifyType: cfg.tmpVersionCheck.notifyType,
569
+ extraText: cfg.tmpVersionCheck.extraText,
559
570
  debugMode: cfg.debugMode,
560
571
  debug: cfg.tmpActivityService?.debug
561
572
  };
@@ -0,0 +1,706 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Version Update</title>
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+
9
+ body {
10
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
11
+ background: #06060b;
12
+ -webkit-font-smoothing: antialiased;
13
+ }
14
+
15
+ #container {
16
+ width: 560px;
17
+ background: #0a0a12;
18
+ position: relative;
19
+ overflow: hidden;
20
+ padding-bottom: 16px;
21
+ }
22
+
23
+ /* ===== 背景:网格 + 光晕 ===== */
24
+ .grid-bg {
25
+ position: absolute;
26
+ inset: 0;
27
+ background-image:
28
+ linear-gradient(rgba(0, 200, 255, 0.03) 1px, transparent 1px),
29
+ linear-gradient(90deg, rgba(0, 200, 255, 0.03) 1px, transparent 1px);
30
+ background-size: 26px 26px;
31
+ pointer-events: none;
32
+ z-index: 0;
33
+ }
34
+
35
+ .glow-top {
36
+ position: absolute;
37
+ top: -180px;
38
+ left: 50%;
39
+ transform: translateX(-50%);
40
+ width: 620px;
41
+ height: 360px;
42
+ background: radial-gradient(ellipse at center, rgba(0, 200, 255, 0.16) 0%, rgba(0, 200, 255, 0.04) 45%, transparent 70%);
43
+ pointer-events: none;
44
+ z-index: 0;
45
+ }
46
+
47
+ .scan-line {
48
+ position: absolute;
49
+ top: 0; left: 0; right: 0;
50
+ height: 2px;
51
+ background: linear-gradient(90deg, transparent, rgba(0, 200, 255, 0.35), transparent);
52
+ pointer-events: none;
53
+ z-index: 3;
54
+ }
55
+
56
+ /* ===== 顶部 ===== */
57
+ .header {
58
+ position: relative;
59
+ z-index: 2;
60
+ padding: 22px 26px 18px;
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 14px;
64
+ border-bottom: 1px solid rgba(0, 200, 255, 0.1);
65
+ background: linear-gradient(180deg, rgba(0, 200, 255, 0.07) 0%, transparent 100%);
66
+ }
67
+
68
+ .header-badge {
69
+ width: 46px;
70
+ height: 46px;
71
+ flex-shrink: 0;
72
+ border-radius: 13px;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ font-size: 23px;
77
+ background: linear-gradient(145deg, rgba(0, 200, 255, 0.22), rgba(0, 200, 255, 0.06));
78
+ border: 1px solid rgba(0, 200, 255, 0.32);
79
+ box-shadow: 0 0 18px rgba(0, 200, 255, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.08);
80
+ }
81
+
82
+ .header-text { flex: 1; min-width: 0; }
83
+
84
+ .header-title {
85
+ font-size: 20px;
86
+ font-weight: 800;
87
+ color: #eaf6ff;
88
+ letter-spacing: 1.5px;
89
+ line-height: 1.25;
90
+ }
91
+
92
+ .header-title em {
93
+ font-style: normal;
94
+ color: #00c8ff;
95
+ text-shadow: 0 0 14px rgba(0, 200, 255, 0.55);
96
+ }
97
+
98
+ .header-sub {
99
+ margin-top: 3px;
100
+ font-size: 10px;
101
+ letter-spacing: 3.2px;
102
+ color: #3d5570;
103
+ text-transform: uppercase;
104
+ }
105
+
106
+ .stage-tag {
107
+ flex-shrink: 0;
108
+ font-size: 11px;
109
+ font-weight: 700;
110
+ letter-spacing: 1px;
111
+ padding: 6px 12px;
112
+ border-radius: 20px;
113
+ color: #ffb545;
114
+ background: rgba(255, 181, 69, 0.1);
115
+ border: 1px solid rgba(255, 181, 69, 0.34);
116
+ box-shadow: 0 0 14px rgba(255, 181, 69, 0.16);
117
+ }
118
+
119
+ .stage-tag:empty { display: none; }
120
+
121
+ /* ===== 版本对比 ===== */
122
+ .hero {
123
+ position: relative;
124
+ z-index: 2;
125
+ margin: 20px 20px 0;
126
+ padding: 22px 20px 20px;
127
+ border-radius: 16px;
128
+ background: linear-gradient(160deg, rgba(0, 200, 255, 0.06) 0%, rgba(0, 200, 255, 0.012) 55%, rgba(0, 255, 136, 0.03) 100%);
129
+ border: 1px solid rgba(0, 200, 255, 0.14);
130
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(0, 200, 255, 0.08);
131
+ }
132
+
133
+ /* 四角装饰 */
134
+ .hero::before,
135
+ .hero::after {
136
+ content: '';
137
+ position: absolute;
138
+ width: 16px;
139
+ height: 16px;
140
+ border: 1.5px solid rgba(0, 200, 255, 0.5);
141
+ }
142
+ .hero::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; border-top-left-radius: 16px; }
143
+ .hero::after { bottom: -1px; right: -1px; border-left: 0; border-top: 0; border-bottom-right-radius: 16px; }
144
+
145
+ .hero-caption {
146
+ text-align: center;
147
+ font-size: 10px;
148
+ letter-spacing: 3px;
149
+ color: #3d5570;
150
+ text-transform: uppercase;
151
+ margin-bottom: 16px;
152
+ }
153
+
154
+ .versus {
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ gap: 16px;
159
+ }
160
+
161
+ .ver {
162
+ flex: 1;
163
+ min-width: 0;
164
+ text-align: center;
165
+ padding: 14px 8px 12px;
166
+ border-radius: 12px;
167
+ }
168
+
169
+ .ver-label {
170
+ font-size: 11px;
171
+ letter-spacing: 1.6px;
172
+ margin-bottom: 9px;
173
+ font-weight: 600;
174
+ }
175
+
176
+ .ver-value {
177
+ font-weight: 800;
178
+ letter-spacing: 0.5px;
179
+ font-variant-numeric: tabular-nums;
180
+ white-space: nowrap;
181
+ }
182
+
183
+ /* 旧版本 */
184
+ .ver.old {
185
+ background: rgba(255, 255, 255, 0.018);
186
+ border: 1px solid rgba(255, 255, 255, 0.06);
187
+ }
188
+ .ver.old .ver-label { color: #5a6878; }
189
+ .ver.old .ver-value {
190
+ font-size: 24px;
191
+ color: #8b98a8;
192
+ text-decoration: line-through;
193
+ text-decoration-color: rgba(255, 92, 92, 0.6);
194
+ text-decoration-thickness: 1.5px;
195
+ }
196
+
197
+ /* 新版本 */
198
+ .ver.new-ver {
199
+ background: linear-gradient(160deg, rgba(0, 200, 255, 0.16) 0%, rgba(0, 255, 136, 0.05) 100%);
200
+ border: 1px solid rgba(0, 200, 255, 0.45);
201
+ box-shadow: 0 0 24px rgba(0, 200, 255, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
202
+ position: relative;
203
+ }
204
+ .ver.new-ver .ver-label { color: #7fe3ff; }
205
+ .ver.new-ver .ver-value {
206
+ font-size: 32px;
207
+ color: #ffffff;
208
+ text-shadow: 0 0 16px rgba(0, 200, 255, 0.7), 0 0 34px rgba(0, 200, 255, 0.35);
209
+ }
210
+
211
+ .new-flag {
212
+ position: absolute;
213
+ top: -9px;
214
+ right: -7px;
215
+ font-size: 9px;
216
+ font-weight: 800;
217
+ letter-spacing: 1.4px;
218
+ padding: 3px 8px;
219
+ border-radius: 9px;
220
+ color: #04121a;
221
+ background: linear-gradient(135deg, #00ff88, #00c8ff);
222
+ box-shadow: 0 0 14px rgba(0, 255, 136, 0.5);
223
+ }
224
+
225
+ /* 中间箭头 */
226
+ .arrow {
227
+ flex-shrink: 0;
228
+ width: 44px;
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: center;
232
+ }
233
+ .arrow svg { display: block; filter: drop-shadow(0 0 7px rgba(0, 200, 255, 0.55)); }
234
+
235
+ /* ===== 信息区 ===== */
236
+ .info {
237
+ position: relative;
238
+ z-index: 2;
239
+ margin: 16px 20px 0;
240
+ display: flex;
241
+ flex-direction: column;
242
+ gap: 8px;
243
+ }
244
+
245
+ .info-row {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 12px;
249
+ padding: 11px 14px;
250
+ border-radius: 11px;
251
+ background: rgba(255, 255, 255, 0.022);
252
+ border: 1px solid rgba(255, 255, 255, 0.055);
253
+ }
254
+
255
+ .info-icon {
256
+ width: 30px;
257
+ height: 30px;
258
+ flex-shrink: 0;
259
+ border-radius: 9px;
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: center;
263
+ font-size: 15px;
264
+ background: rgba(0, 200, 255, 0.1);
265
+ border: 1px solid rgba(0, 200, 255, 0.2);
266
+ }
267
+
268
+ .info-key {
269
+ font-size: 12px;
270
+ color: #5d7089;
271
+ letter-spacing: 0.6px;
272
+ flex-shrink: 0;
273
+ min-width: 96px;
274
+ }
275
+
276
+ .info-val {
277
+ margin-left: auto;
278
+ font-size: 14px;
279
+ font-weight: 700;
280
+ color: #cfe6f7;
281
+ font-variant-numeric: tabular-nums;
282
+ letter-spacing: 0.4px;
283
+ white-space: nowrap;
284
+ display: flex;
285
+ align-items: center;
286
+ gap: 7px;
287
+ }
288
+
289
+ .info-row.hl .info-val { color: #00ff9d; }
290
+
291
+ /* 支持版本「旧 → 新」对比 */
292
+ .v-from {
293
+ font-size: 12.5px;
294
+ font-weight: 600;
295
+ color: #7d8b9c;
296
+ text-decoration: line-through;
297
+ text-decoration-color: rgba(255, 92, 92, 0.5);
298
+ text-decoration-thickness: 1px;
299
+ }
300
+
301
+ .v-to {
302
+ color: #00ff9d;
303
+ text-shadow: 0 0 10px rgba(0, 255, 157, 0.38);
304
+ }
305
+
306
+ .v-same { color: #9fb3c6; font-weight: 600; }
307
+
308
+ .v-none { color: #3d5570; }
309
+
310
+ .mini-arrow { display: block; flex-shrink: 0; }
311
+
312
+ .tag-chg {
313
+ font-size: 10px;
314
+ font-weight: 800;
315
+ letter-spacing: 0.6px;
316
+ padding: 2px 7px;
317
+ border-radius: 8px;
318
+ color: #04121a;
319
+ background: linear-gradient(135deg, #00ff88, #00c8ff);
320
+ box-shadow: 0 0 10px rgba(0, 255, 136, 0.45);
321
+ }
322
+
323
+ .tag-same {
324
+ font-size: 10px;
325
+ letter-spacing: 0.6px;
326
+ padding: 2px 7px;
327
+ border-radius: 8px;
328
+ color: #5d7089;
329
+ background: rgba(255, 255, 255, 0.03);
330
+ border: 1px solid rgba(255, 255, 255, 0.07);
331
+ }
332
+
333
+ /* 支持版本有变化 → 整行高亮 + 左侧强调条 */
334
+ .info-row.changed {
335
+ position: relative;
336
+ border-color: rgba(0, 255, 157, 0.32);
337
+ background: rgba(0, 255, 157, 0.05);
338
+ box-shadow: inset 0 0 20px rgba(0, 255, 157, 0.07);
339
+ }
340
+
341
+ .info-row.changed::before {
342
+ content: '';
343
+ position: absolute;
344
+ left: 0;
345
+ top: 7px;
346
+ bottom: 7px;
347
+ width: 3px;
348
+ border-radius: 0 3px 3px 0;
349
+ background: linear-gradient(180deg, #00ff88, #00c8ff);
350
+ box-shadow: 0 0 10px rgba(0, 255, 136, 0.6);
351
+ }
352
+
353
+ /* ===== 更新文件列表 ===== */
354
+ .files {
355
+ position: relative;
356
+ z-index: 2;
357
+ margin: 14px 22px 0;
358
+ padding: 13px 15px;
359
+ border-radius: 11px;
360
+ background: rgba(255, 255, 255, 0.022);
361
+ border: 1px solid rgba(255, 255, 255, 0.055);
362
+ }
363
+
364
+ .files-head {
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 8px;
368
+ margin-bottom: 9px;
369
+ }
370
+
371
+ .files-icon {
372
+ width: 26px;
373
+ height: 26px;
374
+ flex-shrink: 0;
375
+ border-radius: 8px;
376
+ display: flex;
377
+ align-items: center;
378
+ justify-content: center;
379
+ font-size: 13px;
380
+ background: rgba(0, 200, 255, 0.1);
381
+ border: 1px solid rgba(0, 200, 255, 0.2);
382
+ }
383
+
384
+ .files-title {
385
+ font-size: 12px;
386
+ color: #5d7089;
387
+ letter-spacing: 0.6px;
388
+ }
389
+
390
+ .files-count {
391
+ margin-left: auto;
392
+ font-size: 12px;
393
+ font-weight: 800;
394
+ color: #00c8ff;
395
+ font-variant-numeric: tabular-nums;
396
+ }
397
+
398
+ .files-note {
399
+ margin-bottom: 8px;
400
+ font-size: 10.5px;
401
+ line-height: 1.5;
402
+ color: #7d8b9c;
403
+ }
404
+
405
+ .file-list {
406
+ display: flex;
407
+ flex-direction: column;
408
+ gap: 5px;
409
+ max-height: 190px;
410
+ overflow: hidden;
411
+ }
412
+
413
+ .file-row {
414
+ display: flex;
415
+ align-items: center;
416
+ gap: 8px;
417
+ font-size: 11.5px;
418
+ font-family: 'Cascadia Mono', 'Consolas', monospace;
419
+ color: #9fb3c6;
420
+ line-height: 1.45;
421
+ }
422
+
423
+ .file-kind {
424
+ flex-shrink: 0;
425
+ width: 34px;
426
+ text-align: center;
427
+ font-size: 9px;
428
+ font-weight: 800;
429
+ letter-spacing: 0.4px;
430
+ padding: 1px 0;
431
+ border-radius: 5px;
432
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
433
+ }
434
+
435
+ .file-kind.k-modified { color: #04121a; background: linear-gradient(135deg, #00ff88, #00c8ff); }
436
+ .file-kind.k-added { color: #04121a; background: #00ff9d; }
437
+ .file-kind.k-removed { color: #ffd9d9; background: rgba(255, 92, 92, 0.28); }
438
+ .file-kind.k-unknown { color: #5d7089; background: rgba(255, 255, 255, 0.05); }
439
+
440
+ .file-path {
441
+ overflow: hidden;
442
+ text-overflow: ellipsis;
443
+ white-space: nowrap;
444
+ }
445
+
446
+ .files-more {
447
+ margin-top: 7px;
448
+ font-size: 10.5px;
449
+ color: #5d7089;
450
+ }
451
+
452
+ .files-empty {
453
+ font-size: 12px;
454
+ color: #5d7089;
455
+ }
456
+
457
+ /* ===== 附加文本 ===== */
458
+ .extra {
459
+ position: relative;
460
+ z-index: 2;
461
+ margin: 12px 22px 0;
462
+ padding: 11px 14px;
463
+ border-radius: 11px;
464
+ font-size: 12px;
465
+ line-height: 1.6;
466
+ color: #b9d4e8;
467
+ white-space: pre-wrap;
468
+ word-break: break-word;
469
+ background: rgba(0, 200, 255, 0.05);
470
+ border: 1px solid rgba(0, 200, 255, 0.16);
471
+ }
472
+
473
+ .extra:empty { display: none; }
474
+
475
+ /* ===== 底部 ===== */
476
+ .footer {
477
+ position: relative;
478
+ z-index: 2;
479
+ margin-top: 16px;
480
+ padding: 0 22px;
481
+ text-align: center;
482
+ }
483
+
484
+ .footer-line {
485
+ height: 1px;
486
+ background: linear-gradient(90deg, transparent, rgba(0, 200, 255, 0.2), transparent);
487
+ margin-bottom: 10px;
488
+ }
489
+
490
+ .footer-text {
491
+ font-size: 9.5px;
492
+ letter-spacing: 2.6px;
493
+ color: #22303e;
494
+ text-transform: uppercase;
495
+ }
496
+ </style>
497
+ </head>
498
+ <body>
499
+ <div id="container">
500
+ <div class="grid-bg"></div>
501
+ <div class="glow-top"></div>
502
+ <div class="scan-line"></div>
503
+
504
+ <div class="header">
505
+ <div class="header-badge">📢</div>
506
+ <div class="header-text">
507
+ <div class="header-title">TRUCKERS<em>MP</em> 版本更新</div>
508
+ <div class="header-sub">Version Update Notice</div>
509
+ </div>
510
+ <div class="stage-tag" id="stage-tag"></div>
511
+ </div>
512
+
513
+ <div class="hero">
514
+ <div class="hero-caption">Version Changed</div>
515
+ <div class="versus">
516
+ <div class="ver old">
517
+ <div class="ver-label">旧版本</div>
518
+ <div class="ver-value" id="old-name">-</div>
519
+ </div>
520
+ <div class="arrow">
521
+ <svg width="52" height="26" viewBox="0 0 52 26" xmlns="http://www.w3.org/2000/svg">
522
+ <defs>
523
+ <!-- 注意:水平线的 objectBoundingBox 高度为 0,渐变会塌缩 → 必须用 userSpaceOnUse -->
524
+ <linearGradient id="arw" gradientUnits="userSpaceOnUse" x1="3" y1="13" x2="32" y2="13">
525
+ <stop offset="0%" stop-color="rgba(0,200,255,0.35)"/>
526
+ <stop offset="100%" stop-color="#00c8ff"/>
527
+ </linearGradient>
528
+ </defs>
529
+ <path d="M3 13 H32" stroke="url(#arw)" stroke-width="3" stroke-linecap="round"/>
530
+ <path d="M29 5 L39 13 L29 21" fill="none" stroke="#00ff88" stroke-width="3"
531
+ stroke-linecap="round" stroke-linejoin="round"/>
532
+ </svg>
533
+ </div>
534
+ <div class="ver new-ver">
535
+ <div class="new-flag">NEW</div>
536
+ <div class="ver-label">新版本</div>
537
+ <div class="ver-value" id="new-name">-</div>
538
+ </div>
539
+ </div>
540
+ </div>
541
+
542
+ <div class="info" id="info">
543
+ <div class="info-row">
544
+ <div class="info-icon">🕒</div>
545
+ <div class="info-key">更新时间</div>
546
+ <div class="info-val" id="time">-</div>
547
+ </div>
548
+ <div class="info-row hl" id="ets-row">
549
+ <div class="info-icon">🚛</div>
550
+ <div class="info-key">欧卡支持版本</div>
551
+ <div class="info-val" id="ets-ver"></div>
552
+ </div>
553
+ <div class="info-row hl" id="ats-row">
554
+ <div class="info-icon">🚚</div>
555
+ <div class="info-key">美卡支持版本</div>
556
+ <div class="info-val" id="ats-ver"></div>
557
+ </div>
558
+ </div>
559
+
560
+ <div class="files" id="files-block">
561
+ <div class="files-head">
562
+ <div class="files-icon">📦</div>
563
+ <div class="files-title">更新文件</div>
564
+ <div class="files-count" id="files-count"></div>
565
+ </div>
566
+ <div class="files-note" id="files-note"></div>
567
+ <div class="file-list" id="file-list"></div>
568
+ <div class="files-more" id="files-more"></div>
569
+ </div>
570
+
571
+ <div class="extra" id="extra"></div>
572
+
573
+ <div class="footer">
574
+ <div class="footer-line"></div>
575
+ <div class="footer-text">TruckersMP · Version Monitor</div>
576
+ </div>
577
+ </div>
578
+
579
+ <script>
580
+ /* 「旧 → 新」小箭头(纯实色描边,避开水平线渐变塌缩的坑) */
581
+ var MINI_ARROW =
582
+ '<svg class="mini-arrow" width="19" height="10" viewBox="0 0 19 10" xmlns="http://www.w3.org/2000/svg">' +
583
+ '<path d="M1 5 H12" stroke="#00c8ff" stroke-width="1.6" stroke-linecap="round"/>' +
584
+ '<path d="M10.5 1.5 L15 5 L10.5 8.5" fill="none" stroke="#00ff88" stroke-width="1.6"' +
585
+ ' stroke-linecap="round" stroke-linejoin="round"/></svg>';
586
+
587
+ function esc(s) {
588
+ return String(s == null ? '' : s)
589
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
590
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
591
+ }
592
+
593
+ function setText(id, value) {
594
+ var el = document.getElementById(id);
595
+ if (el) el.textContent = (value === undefined || value === null || value === '') ? '-' : String(value);
596
+ }
597
+
598
+ /**
599
+ * 渲染一条「游戏支持版本」:
600
+ * - 新旧都拿到且不同 → 旧(删除线) → 新(绿) + 「已更新」标签,整行高亮
601
+ * - 相同 / 拿不到旧值 → 只显示当前版本 + 「未变化」标签
602
+ */
603
+ function setGameVersion(valId, rowId, oldV, newV) {
604
+ var el = document.getElementById(valId);
605
+ var row = document.getElementById(rowId);
606
+ if (!el) return;
607
+ var hasOld = oldV !== undefined && oldV !== null && oldV !== '';
608
+ var hasNew = newV !== undefined && newV !== null && newV !== '';
609
+ var changed = hasOld && hasNew && String(oldV) !== String(newV);
610
+
611
+ if (changed) {
612
+ if (row) row.classList.add('changed');
613
+ el.innerHTML =
614
+ '<span class="v-from">' + esc(oldV) + '</span>' +
615
+ MINI_ARROW +
616
+ '<span class="v-to">' + esc(newV) + '</span>' +
617
+ '<span class="tag-chg">已更新</span>';
618
+ return;
619
+ }
620
+
621
+ if (row) row.classList.remove('changed');
622
+ if (!hasNew && !hasOld) {
623
+ el.innerHTML = '<span class="v-none">-</span>';
624
+ return;
625
+ }
626
+ el.innerHTML =
627
+ '<span class="v-same">' + esc(hasNew ? newV : oldV) + '</span>' +
628
+ '<span class="tag-same">未变化</span>';
629
+ }
630
+
631
+ /* 变化类型 → 展示用标签与样式类 */
632
+ var KIND_LABEL = { modified: '更新', added: '新增', removed: '移除', unknown: '文件' };
633
+
634
+ /* 卡片里最多列多少个文件(超出的用「等共 N 个」收尾,避免卡片无限长) */
635
+ var MAX_FILES_IN_CARD = 12;
636
+
637
+ /**
638
+ * 渲染更新文件区块。
639
+ * - filesUnavailable → 清单查询失败(不能显示成「没有变化」)
640
+ * - files 为空 → 确实没有文件变化
641
+ */
642
+ function setChangedFiles(files, snapshotMissing, unavailable) {
643
+ var list = Array.isArray(files) ? files : [];
644
+ var listEl = document.getElementById('file-list');
645
+ var countEl = document.getElementById('files-count');
646
+ var noteEl = document.getElementById('files-note');
647
+ var moreEl = document.getElementById('files-more');
648
+ if (!listEl) return;
649
+
650
+ if (countEl) countEl.textContent = list.length > 0 ? list.length + ' 个' : '';
651
+
652
+ if (unavailable) {
653
+ if (noteEl) noteEl.textContent = '';
654
+ if (moreEl) moreEl.textContent = '';
655
+ listEl.innerHTML = '<div class="files-empty">文件清单获取失败,无法比对变化</div>';
656
+ return;
657
+ }
658
+
659
+ if (list.length === 0) {
660
+ if (noteEl) noteEl.textContent = '';
661
+ if (moreEl) moreEl.textContent = '';
662
+ listEl.innerHTML = '<div class="files-empty">未检测到文件变化</div>';
663
+ return;
664
+ }
665
+
666
+ if (noteEl) {
667
+ noteEl.textContent = snapshotMissing
668
+ ? '未留存上次清单,以下为当前版本完整文件列表'
669
+ : '以下文件相对上次检查发生了 MD5 变化';
670
+ }
671
+
672
+ var shown = list.slice(0, MAX_FILES_IN_CARD);
673
+ listEl.innerHTML = shown.map(function (f) {
674
+ var kind = KIND_LABEL[f.kind] ? f.kind : 'unknown';
675
+ return '<div class="file-row">' +
676
+ '<span class="file-kind k-' + kind + '">' + KIND_LABEL[kind] + '</span>' +
677
+ '<span class="file-path" title="' + esc(f.path) + '">' + esc(f.path) + '</span>' +
678
+ '</div>';
679
+ }).join('');
680
+
681
+ if (moreEl) {
682
+ moreEl.textContent = list.length > shown.length
683
+ ? '… 等共 ' + list.length + ' 个文件'
684
+ : '';
685
+ }
686
+ }
687
+
688
+ function setData(data) {
689
+ data = data || {};
690
+ setText('old-name', data.oldName);
691
+ setText('new-name', data.newName);
692
+ setText('time', data.time);
693
+ setText('stage-tag', data.stage);
694
+ setGameVersion('ets-ver', 'ets-row', data.etsOld, data.etsNew);
695
+ setGameVersion('ats-ver', 'ats-row', data.atsOld, data.atsNew);
696
+ setChangedFiles(data.files, data.filesSnapshotMissing, data.filesUnavailable);
697
+
698
+ var extraEl = document.getElementById('extra');
699
+ if (extraEl) {
700
+ var extra = data.extraText == null ? '' : String(data.extraText).trim();
701
+ extraEl.textContent = extra;
702
+ }
703
+ }
704
+ </script>
705
+ </body>
706
+ </html>
@@ -45,8 +45,7 @@ async function pickAll(ctx, session, bindingRows) {
45
45
  if (list.length === 0) {
46
46
  return { ids: [], total: 0, truncated: false, hint: null };
47
47
  }
48
- // 补全旧数据缺失的玩家名(复用列表展示那套逻辑,保证提示文案准确)
49
- await guildBind.ensureNames(ctx, list);
48
+ // 这里只用到 tmp_id(hint 只报数量,不显示名字)→ 不刷新名字,省接口
50
49
  const { ids, total, truncated } = resolveAll(list);
51
50
  return {
52
51
  ids,
@@ -195,9 +194,10 @@ function register(ctx) {
195
194
  */
196
195
  async function pick(ctx, session, bindings, opts = {}) {
197
196
  if (!bindings || bindings.length === 0) return [];
198
- // 旧绑定的行没有名字:先查接口补全并写回,避免列表显示"未知"
199
- await guildBind.ensureNames(ctx, bindings);
197
+ // 单绑定:不显示列表,直接返回 —— 此时没必要刷新名字(查询本身会显示真实名字)
200
198
  if (bindings.length === 1) return [String(bindings[0].tmp_id)];
199
+ // 要显示序号列表了:查接口刷新名字,保证玩家改名后列表及时同步
200
+ await guildBind.refreshNames(ctx, bindings);
201
201
  const allowMulti = opts.allowMulti !== false;
202
202
  const allowAll = opts.allowAll !== false;
203
203
  const max = allowMulti ? MAX_PICK : 1;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ets2-tools-tmp",
3
3
  "description": "欧卡2 TruckersMP信息查询、车队平台查询及活动提醒",
4
- "version": "3.4.4",
4
+ "version": "3.4.5",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>"
7
7
  ],