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.
- package/lib/command/tmpMileageRanking.js +1 -1
- package/lib/command/tmpVersionCheck.js +278 -12
- package/lib/command/tmpVtcMileageRanking.js +1 -1
- package/lib/database/guildBind.js +16 -9
- package/lib/database/model.js +1 -1
- package/lib/index.js +16 -5
- package/lib/resource/version-update.html +706 -0
- package/lib/util/tmpIdPicker.js +4 -4
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* 刷新玩家名(**总是查接口**,用于玩家改名后的及时同步):
|
|
11
|
+
* - 并发查询传入的所有行(含缺名的旧数据);
|
|
12
|
+
* - 名字确实变了才写库,避免无谓的写操作;
|
|
13
|
+
* - 单个查询失败保留旧名(不把已有名字清空),下次再刷新。
|
|
14
|
+
* 注意:只在**要显示名字**的地方调用(我的绑定 / 序号列表 / 解绑·设为默认文案),
|
|
15
|
+
* 不显示名字的路径(如 pickAll、排行榜)不要调用,省接口。
|
|
12
16
|
* @returns 原数组(行对象已被就地更新)
|
|
13
17
|
*/
|
|
14
|
-
async
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
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(
|
|
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
|
-
|
|
24
|
-
|
|
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
|
},
|
package/lib/database/model.js
CHANGED
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.
|
|
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.
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
590
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
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>
|
package/lib/util/tmpIdPicker.js
CHANGED
|
@@ -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;
|