koishi-plugin-my-pig-group-friends 1.0.0
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/config.d.ts +21 -0
- package/lib/constants.d.ts +10 -0
- package/lib/database.d.ts +27 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +1426 -0
- package/lib/services/card.d.ts +13 -0
- package/lib/services/location.d.ts +13 -0
- package/lib/services/sunrise.d.ts +6 -0
- package/lib/services/travel.d.ts +17 -0
- package/lib/services/unsplash.d.ts +6 -0
- package/lib/types.d.ts +44 -0
- package/package.json +33 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1426 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name2 in all)
|
|
8
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
Config: () => Config,
|
|
24
|
+
apply: () => apply,
|
|
25
|
+
inject: () => inject,
|
|
26
|
+
name: () => name
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(src_exports);
|
|
29
|
+
var import_koishi2 = require("koishi");
|
|
30
|
+
|
|
31
|
+
// src/database.ts
|
|
32
|
+
function applyDatabase(ctx) {
|
|
33
|
+
ctx.model.extend("pig_user_state", {
|
|
34
|
+
id: "unsigned",
|
|
35
|
+
userId: "string",
|
|
36
|
+
platform: "string",
|
|
37
|
+
lastWakeUp: "timestamp",
|
|
38
|
+
lastSunrise: "timestamp",
|
|
39
|
+
latitude: "float",
|
|
40
|
+
longitude: "float"
|
|
41
|
+
}, { primary: "id", autoInc: true });
|
|
42
|
+
ctx.model.extend("pig_travel_log", {
|
|
43
|
+
id: "unsigned",
|
|
44
|
+
userId: "string",
|
|
45
|
+
platform: "string",
|
|
46
|
+
timestamp: "timestamp",
|
|
47
|
+
country: "string",
|
|
48
|
+
location: "string",
|
|
49
|
+
imagePath: "string",
|
|
50
|
+
isAIGC: "boolean"
|
|
51
|
+
}, { primary: "id", autoInc: true });
|
|
52
|
+
}
|
|
53
|
+
__name(applyDatabase, "applyDatabase");
|
|
54
|
+
|
|
55
|
+
// src/services/sunrise.ts
|
|
56
|
+
async function getSunriseInfo(ctx, lat, lng, date = "today") {
|
|
57
|
+
const url = `https://api.sunrise-sunset.org/json?lat=${lat}&lng=${lng}&formatted=0&date=${date}`;
|
|
58
|
+
const data = await ctx.http.get(url);
|
|
59
|
+
if (data.status !== "OK") {
|
|
60
|
+
throw new Error("Failed to fetch sunrise info");
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
sunrise: new Date(data.results.sunrise),
|
|
64
|
+
sunset: new Date(data.results.sunset)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
__name(getSunriseInfo, "getSunriseInfo");
|
|
68
|
+
|
|
69
|
+
// src/services/card.ts
|
|
70
|
+
var import_promises = require("fs/promises");
|
|
71
|
+
var import_url = require("url");
|
|
72
|
+
|
|
73
|
+
// src/constants.ts
|
|
74
|
+
var LOCATIONS = [
|
|
75
|
+
// 亚洲 (UTC+5 ~ UTC+9)
|
|
76
|
+
{
|
|
77
|
+
country: "Japan",
|
|
78
|
+
countryZh: "日本",
|
|
79
|
+
city: "Kyoto",
|
|
80
|
+
landmark: "Fushimi Inari Shrine",
|
|
81
|
+
landmarkZh: "伏见稻荷大社",
|
|
82
|
+
timezone: "Asia/Tokyo",
|
|
83
|
+
landscapeUrl: "https://images.unsplash.com/photo-1478436127897-769e1b3f0f36"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
country: "Japan",
|
|
87
|
+
countryZh: "日本",
|
|
88
|
+
city: "Osaka",
|
|
89
|
+
landmark: "Dotonbori",
|
|
90
|
+
landmarkZh: "道顿堀",
|
|
91
|
+
timezone: "Asia/Tokyo",
|
|
92
|
+
landscapeUrl: "https://images.unsplash.com/photo-1590559899731-a382839e5549"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
country: "South Korea",
|
|
96
|
+
countryZh: "韩国",
|
|
97
|
+
city: "Seoul",
|
|
98
|
+
landmark: "Gyeongbokgung Palace",
|
|
99
|
+
landmarkZh: "景福宫",
|
|
100
|
+
timezone: "Asia/Seoul",
|
|
101
|
+
landscapeUrl: "https://images.unsplash.com/photo-1534274988757-a28bf1a57c17"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
country: "China",
|
|
105
|
+
countryZh: "中国",
|
|
106
|
+
city: "Shanghai",
|
|
107
|
+
landmark: "The Bund",
|
|
108
|
+
landmarkZh: "外滩",
|
|
109
|
+
timezone: "Asia/Shanghai",
|
|
110
|
+
landscapeUrl: "https://images.unsplash.com/photo-1537531383496-f4749b918caa"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
country: "China",
|
|
114
|
+
countryZh: "中国",
|
|
115
|
+
city: "Guilin",
|
|
116
|
+
landmark: "Li River",
|
|
117
|
+
landmarkZh: "漓江",
|
|
118
|
+
timezone: "Asia/Shanghai",
|
|
119
|
+
landscapeUrl: "https://images.unsplash.com/photo-1529921879218-f99546d03a50"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
country: "Thailand",
|
|
123
|
+
countryZh: "泰国",
|
|
124
|
+
city: "Bangkok",
|
|
125
|
+
landmark: "Grand Palace",
|
|
126
|
+
landmarkZh: "大皇宫",
|
|
127
|
+
timezone: "Asia/Bangkok",
|
|
128
|
+
landscapeUrl: "https://images.unsplash.com/photo-1563492065599-3520f775eeed"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
country: "Vietnam",
|
|
132
|
+
countryZh: "越南",
|
|
133
|
+
city: "Ha Long Bay",
|
|
134
|
+
landmark: "Ha Long Bay",
|
|
135
|
+
landmarkZh: "下龙湾",
|
|
136
|
+
timezone: "Asia/Ho_Chi_Minh",
|
|
137
|
+
landscapeUrl: "https://images.unsplash.com/photo-1528127269322-539801943592"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
country: "India",
|
|
141
|
+
countryZh: "印度",
|
|
142
|
+
city: "Agra",
|
|
143
|
+
landmark: "Taj Mahal",
|
|
144
|
+
landmarkZh: "泰姬陵",
|
|
145
|
+
timezone: "Asia/Kolkata",
|
|
146
|
+
landscapeUrl: "https://images.unsplash.com/photo-1564507592333-c60657eea523"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
country: "Singapore",
|
|
150
|
+
countryZh: "新加坡",
|
|
151
|
+
city: "Singapore",
|
|
152
|
+
landmark: "Marina Bay Sands",
|
|
153
|
+
landmarkZh: "滨海湾金沙",
|
|
154
|
+
timezone: "Asia/Singapore",
|
|
155
|
+
landscapeUrl: "https://images.unsplash.com/photo-1525625293386-3f8f99389edd"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
country: "Indonesia",
|
|
159
|
+
countryZh: "印度尼西亚",
|
|
160
|
+
city: "Bali",
|
|
161
|
+
landmark: "Tanah Lot Temple",
|
|
162
|
+
landmarkZh: "海神庙",
|
|
163
|
+
timezone: "Asia/Jakarta",
|
|
164
|
+
landscapeUrl: "https://images.unsplash.com/photo-1537996194471-e657df975ab4"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
country: "Nepal",
|
|
168
|
+
countryZh: "尼泊尔",
|
|
169
|
+
city: "Kathmandu",
|
|
170
|
+
landmark: "Himalaya Mountains",
|
|
171
|
+
landmarkZh: "喜马拉雅山",
|
|
172
|
+
timezone: "Asia/Kathmandu",
|
|
173
|
+
landscapeUrl: "https://images.unsplash.com/photo-1544735716-392fe2489ffa"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
country: "UAE",
|
|
177
|
+
countryZh: "阿联酋",
|
|
178
|
+
city: "Dubai",
|
|
179
|
+
landmark: "Burj Khalifa",
|
|
180
|
+
landmarkZh: "哈利法塔",
|
|
181
|
+
timezone: "Asia/Dubai",
|
|
182
|
+
landscapeUrl: "https://images.unsplash.com/photo-1512453979798-5ea266f8880c"
|
|
183
|
+
},
|
|
184
|
+
// 欧洲 (UTC+0 ~ UTC+3)
|
|
185
|
+
{
|
|
186
|
+
country: "UK",
|
|
187
|
+
countryZh: "英国",
|
|
188
|
+
city: "London",
|
|
189
|
+
landmark: "Big Ben",
|
|
190
|
+
landmarkZh: "大本钟",
|
|
191
|
+
timezone: "Europe/London",
|
|
192
|
+
landscapeUrl: "https://images.unsplash.com/photo-1513635269975-59663e0ac1ad"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
country: "France",
|
|
196
|
+
countryZh: "法国",
|
|
197
|
+
city: "Provence",
|
|
198
|
+
landmark: "Lavender Fields",
|
|
199
|
+
landmarkZh: "薰衣草田",
|
|
200
|
+
timezone: "Europe/Paris",
|
|
201
|
+
landscapeUrl: "https://images.unsplash.com/photo-1499002238440-d264edd596ec"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
country: "Italy",
|
|
205
|
+
countryZh: "意大利",
|
|
206
|
+
city: "Venice",
|
|
207
|
+
landmark: "Grand Canal",
|
|
208
|
+
landmarkZh: "大运河",
|
|
209
|
+
timezone: "Europe/Rome",
|
|
210
|
+
landscapeUrl: "https://images.unsplash.com/photo-1514890547357-a9ee288728e0"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
country: "Italy",
|
|
214
|
+
countryZh: "意大利",
|
|
215
|
+
city: "Rome",
|
|
216
|
+
landmark: "Colosseum",
|
|
217
|
+
landmarkZh: "罗马斗兽场",
|
|
218
|
+
timezone: "Europe/Rome",
|
|
219
|
+
landscapeUrl: "https://images.unsplash.com/photo-1552832230-c0197dd311b5"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
country: "Spain",
|
|
223
|
+
countryZh: "西班牙",
|
|
224
|
+
city: "Barcelona",
|
|
225
|
+
landmark: "Sagrada Familia",
|
|
226
|
+
landmarkZh: "圣家堂",
|
|
227
|
+
timezone: "Europe/Madrid",
|
|
228
|
+
landscapeUrl: "https://images.unsplash.com/photo-1583422409516-2895a77efded"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
country: "Greece",
|
|
232
|
+
countryZh: "希腊",
|
|
233
|
+
city: "Santorini",
|
|
234
|
+
landmark: "Oia Village",
|
|
235
|
+
landmarkZh: "伊亚小镇",
|
|
236
|
+
timezone: "Europe/Athens",
|
|
237
|
+
landscapeUrl: "https://images.unsplash.com/photo-1613395877344-13d4a8e0d49e"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
country: "Netherlands",
|
|
241
|
+
countryZh: "荷兰",
|
|
242
|
+
city: "Amsterdam",
|
|
243
|
+
landmark: "Canal Ring",
|
|
244
|
+
landmarkZh: "运河环",
|
|
245
|
+
timezone: "Europe/Amsterdam",
|
|
246
|
+
landscapeUrl: "https://images.unsplash.com/photo-1534351590666-13e3e96b5017"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
country: "Switzerland",
|
|
250
|
+
countryZh: "瑞士",
|
|
251
|
+
city: "Interlaken",
|
|
252
|
+
landmark: "Swiss Alps",
|
|
253
|
+
landmarkZh: "阿尔卑斯山",
|
|
254
|
+
timezone: "Europe/Zurich",
|
|
255
|
+
landscapeUrl: "https://images.unsplash.com/photo-1531366936337-7c912a4589a7"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
country: "Norway",
|
|
259
|
+
countryZh: "挪威",
|
|
260
|
+
city: "Tromsø",
|
|
261
|
+
landmark: "Northern Lights",
|
|
262
|
+
landmarkZh: "北极光",
|
|
263
|
+
timezone: "Europe/Oslo",
|
|
264
|
+
landscapeUrl: "https://images.unsplash.com/photo-1483347756197-71ef80e95f73"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
country: "Iceland",
|
|
268
|
+
countryZh: "冰岛",
|
|
269
|
+
city: "Reykjavik",
|
|
270
|
+
landmark: "Blue Lagoon",
|
|
271
|
+
landmarkZh: "蓝湖温泉",
|
|
272
|
+
timezone: "Atlantic/Reykjavik",
|
|
273
|
+
landscapeUrl: "https://images.unsplash.com/photo-1504829857797-ddff29c27927"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
country: "Turkey",
|
|
277
|
+
countryZh: "土耳其",
|
|
278
|
+
city: "Cappadocia",
|
|
279
|
+
landmark: "Hot Air Balloons",
|
|
280
|
+
landmarkZh: "热气球",
|
|
281
|
+
timezone: "Europe/Istanbul",
|
|
282
|
+
landscapeUrl: "https://images.unsplash.com/photo-1641128324972-af3212f0f6bd"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
country: "Russia",
|
|
286
|
+
countryZh: "俄罗斯",
|
|
287
|
+
city: "Moscow",
|
|
288
|
+
landmark: "Red Square",
|
|
289
|
+
landmarkZh: "红场",
|
|
290
|
+
timezone: "Europe/Moscow",
|
|
291
|
+
landscapeUrl: "https://images.unsplash.com/photo-1513326738677-b964603b136d"
|
|
292
|
+
},
|
|
293
|
+
// 非洲 (UTC+0 ~ UTC+3)
|
|
294
|
+
{
|
|
295
|
+
country: "Egypt",
|
|
296
|
+
countryZh: "埃及",
|
|
297
|
+
city: "Giza",
|
|
298
|
+
landmark: "Pyramids of Giza",
|
|
299
|
+
landmarkZh: "吉萨金字塔",
|
|
300
|
+
timezone: "Africa/Cairo",
|
|
301
|
+
landscapeUrl: "https://images.unsplash.com/photo-1503177119275-0aa32b3a9368"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
country: "Morocco",
|
|
305
|
+
countryZh: "摩洛哥",
|
|
306
|
+
city: "Marrakech",
|
|
307
|
+
landmark: "Jardin Majorelle",
|
|
308
|
+
landmarkZh: "马约尔花园",
|
|
309
|
+
timezone: "Africa/Casablanca",
|
|
310
|
+
landscapeUrl: "https://images.unsplash.com/photo-1489749798305-4fea3ae63d43"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
country: "South Africa",
|
|
314
|
+
countryZh: "南非",
|
|
315
|
+
city: "Cape Town",
|
|
316
|
+
landmark: "Table Mountain",
|
|
317
|
+
landmarkZh: "桌山",
|
|
318
|
+
timezone: "Africa/Johannesburg",
|
|
319
|
+
landscapeUrl: "https://images.unsplash.com/photo-1580060839134-75a5edca2e99"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
country: "Kenya",
|
|
323
|
+
countryZh: "肯尼亚",
|
|
324
|
+
city: "Masai Mara",
|
|
325
|
+
landmark: "Safari Savanna",
|
|
326
|
+
landmarkZh: "马赛马拉草原",
|
|
327
|
+
timezone: "Africa/Nairobi",
|
|
328
|
+
landscapeUrl: "https://images.unsplash.com/photo-1547970810-dc1eac37d174"
|
|
329
|
+
},
|
|
330
|
+
// 北美洲 (UTC-5 ~ UTC-8)
|
|
331
|
+
{
|
|
332
|
+
country: "USA",
|
|
333
|
+
countryZh: "美国",
|
|
334
|
+
city: "New York",
|
|
335
|
+
landmark: "Times Square",
|
|
336
|
+
landmarkZh: "时代广场",
|
|
337
|
+
timezone: "America/New_York",
|
|
338
|
+
landscapeUrl: "https://images.unsplash.com/photo-1534430480872-3498386e7856"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
country: "USA",
|
|
342
|
+
countryZh: "美国",
|
|
343
|
+
city: "San Francisco",
|
|
344
|
+
landmark: "Golden Gate Bridge",
|
|
345
|
+
landmarkZh: "金门大桥",
|
|
346
|
+
timezone: "America/Los_Angeles",
|
|
347
|
+
landscapeUrl: "https://images.unsplash.com/photo-1449034446853-66c86144b0ad"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
country: "USA",
|
|
351
|
+
countryZh: "美国",
|
|
352
|
+
city: "Las Vegas",
|
|
353
|
+
landmark: "The Strip",
|
|
354
|
+
landmarkZh: "拉斯维加斯大道",
|
|
355
|
+
timezone: "America/Los_Angeles",
|
|
356
|
+
landscapeUrl: "https://images.unsplash.com/photo-1605833556294-ea5c7a74f57d"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
country: "USA",
|
|
360
|
+
countryZh: "美国",
|
|
361
|
+
city: "Arizona",
|
|
362
|
+
landmark: "Grand Canyon",
|
|
363
|
+
landmarkZh: "大峡谷",
|
|
364
|
+
timezone: "America/Phoenix",
|
|
365
|
+
landscapeUrl: "https://images.unsplash.com/photo-1474044159687-1ee9f3a51722"
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
country: "Canada",
|
|
369
|
+
countryZh: "加拿大",
|
|
370
|
+
city: "Banff",
|
|
371
|
+
landmark: "Lake Louise",
|
|
372
|
+
landmarkZh: "路易斯湖",
|
|
373
|
+
timezone: "America/Edmonton",
|
|
374
|
+
landscapeUrl: "https://images.unsplash.com/photo-1502085671122-2d218cd434e6"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
country: "Canada",
|
|
378
|
+
countryZh: "加拿大",
|
|
379
|
+
city: "Niagara Falls",
|
|
380
|
+
landmark: "Niagara Falls",
|
|
381
|
+
landmarkZh: "尼亚加拉瀑布",
|
|
382
|
+
timezone: "America/Toronto",
|
|
383
|
+
landscapeUrl: "https://images.unsplash.com/photo-1489447068241-b3490214e879"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
country: "Mexico",
|
|
387
|
+
countryZh: "墨西哥",
|
|
388
|
+
city: "Cancun",
|
|
389
|
+
landmark: "Chichen Itza",
|
|
390
|
+
landmarkZh: "奇琴伊察",
|
|
391
|
+
timezone: "America/Cancun",
|
|
392
|
+
landscapeUrl: "https://images.unsplash.com/photo-1518638150340-f706e86654de"
|
|
393
|
+
},
|
|
394
|
+
// 南美洲 (UTC-3 ~ UTC-5)
|
|
395
|
+
{
|
|
396
|
+
country: "Brazil",
|
|
397
|
+
countryZh: "巴西",
|
|
398
|
+
city: "Rio de Janeiro",
|
|
399
|
+
landmark: "Christ the Redeemer",
|
|
400
|
+
landmarkZh: "基督救世主像",
|
|
401
|
+
timezone: "America/Sao_Paulo",
|
|
402
|
+
landscapeUrl: "https://images.unsplash.com/photo-1483729558449-99ef09a8c325"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
country: "Peru",
|
|
406
|
+
countryZh: "秘鲁",
|
|
407
|
+
city: "Cusco",
|
|
408
|
+
landmark: "Machu Picchu",
|
|
409
|
+
landmarkZh: "马丘比丘",
|
|
410
|
+
timezone: "America/Lima",
|
|
411
|
+
landscapeUrl: "https://images.unsplash.com/photo-1526392060635-9d6019884377"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
country: "Argentina",
|
|
415
|
+
countryZh: "阿根廷",
|
|
416
|
+
city: "Patagonia",
|
|
417
|
+
landmark: "Perito Moreno Glacier",
|
|
418
|
+
landmarkZh: "莫雷诺冰川",
|
|
419
|
+
timezone: "America/Argentina/Buenos_Aires",
|
|
420
|
+
landscapeUrl: "https://images.unsplash.com/photo-1551279880-03041531948f"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
country: "Chile",
|
|
424
|
+
countryZh: "智利",
|
|
425
|
+
city: "Atacama",
|
|
426
|
+
landmark: "Atacama Desert",
|
|
427
|
+
landmarkZh: "阿塔卡马沙漠",
|
|
428
|
+
timezone: "America/Santiago",
|
|
429
|
+
landscapeUrl: "https://images.unsplash.com/photo-1489392191049-fc10c97e64b6"
|
|
430
|
+
},
|
|
431
|
+
// 大洋洲 (UTC+10 ~ UTC+12)
|
|
432
|
+
{
|
|
433
|
+
country: "Australia",
|
|
434
|
+
countryZh: "澳大利亚",
|
|
435
|
+
city: "Sydney",
|
|
436
|
+
landmark: "Sydney Opera House",
|
|
437
|
+
landmarkZh: "悉尼歌剧院",
|
|
438
|
+
timezone: "Australia/Sydney",
|
|
439
|
+
landscapeUrl: "https://images.unsplash.com/photo-1506973035872-a4ec16b8e8d9"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
country: "Australia",
|
|
443
|
+
countryZh: "澳大利亚",
|
|
444
|
+
city: "Queensland",
|
|
445
|
+
landmark: "Great Barrier Reef",
|
|
446
|
+
landmarkZh: "大堡礁",
|
|
447
|
+
timezone: "Australia/Brisbane",
|
|
448
|
+
landscapeUrl: "https://images.unsplash.com/photo-1559128010-7c1ad6e1b6a5"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
country: "New Zealand",
|
|
452
|
+
countryZh: "新西兰",
|
|
453
|
+
city: "Queenstown",
|
|
454
|
+
landmark: "Milford Sound",
|
|
455
|
+
landmarkZh: "米尔福德峡湾",
|
|
456
|
+
timezone: "Pacific/Auckland",
|
|
457
|
+
landscapeUrl: "https://images.unsplash.com/photo-1507699622108-4be3abd695ad"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
country: "New Zealand",
|
|
461
|
+
countryZh: "新西兰",
|
|
462
|
+
city: "Matamata",
|
|
463
|
+
landmark: "Hobbiton",
|
|
464
|
+
landmarkZh: "霍比屯",
|
|
465
|
+
timezone: "Pacific/Auckland",
|
|
466
|
+
landscapeUrl: "https://images.unsplash.com/photo-1507097634215-e82e6b552cae"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
country: "Fiji",
|
|
470
|
+
countryZh: "斐济",
|
|
471
|
+
city: "Nadi",
|
|
472
|
+
landmark: "Fiji Islands",
|
|
473
|
+
landmarkZh: "斐济群岛",
|
|
474
|
+
timezone: "Pacific/Fiji",
|
|
475
|
+
landscapeUrl: "https://images.unsplash.com/photo-1589179899031-d86b7e3fcd61"
|
|
476
|
+
}
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
// src/services/card.ts
|
|
480
|
+
async function generateFootprintCard(ctx, config, data, userInfo, platform, backgroundUrl) {
|
|
481
|
+
const username = userInfo.username || userInfo.userId;
|
|
482
|
+
let avatarUrl = userInfo.avatarUrl || "";
|
|
483
|
+
if (!avatarUrl) {
|
|
484
|
+
avatarUrl = `https://ui-avatars.com/api/?name=${encodeURIComponent(username)}&background=random&size=200`;
|
|
485
|
+
}
|
|
486
|
+
let bgImage = backgroundUrl || data.location.landscapeUrl;
|
|
487
|
+
if (config.debug) ctx.logger("pig").debug(`Initial background URL: ${bgImage || "none"}`);
|
|
488
|
+
const normalizeImageUrl = /* @__PURE__ */ __name((url) => {
|
|
489
|
+
const trimmed = url.trim();
|
|
490
|
+
if (trimmed.startsWith("data:")) return trimmed;
|
|
491
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
492
|
+
try {
|
|
493
|
+
return encodeURI(trimmed);
|
|
494
|
+
} catch {
|
|
495
|
+
return trimmed;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return trimmed;
|
|
499
|
+
}, "normalizeImageUrl");
|
|
500
|
+
const sniffMime = /* @__PURE__ */ __name((buffer) => {
|
|
501
|
+
if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) return "image/png";
|
|
502
|
+
if (buffer[0] === 255 && buffer[1] === 216) return "image/jpeg";
|
|
503
|
+
if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70) return "image/webp";
|
|
504
|
+
return "image/jpeg";
|
|
505
|
+
}, "sniffMime");
|
|
506
|
+
const inlineMaxBytes = 900 * 1024;
|
|
507
|
+
const fetchToDataUrl = /* @__PURE__ */ __name(async (url) => {
|
|
508
|
+
const normalized = normalizeImageUrl(url);
|
|
509
|
+
if (config.debug) ctx.logger("pig").debug(`Normalized background URL: ${normalized}`);
|
|
510
|
+
if (normalized.startsWith("data:")) return normalized;
|
|
511
|
+
if (normalized.startsWith("file://")) {
|
|
512
|
+
try {
|
|
513
|
+
const filePath = (0, import_url.fileURLToPath)(normalized);
|
|
514
|
+
const buffer = await (0, import_promises.readFile)(filePath);
|
|
515
|
+
const mime = sniffMime(buffer);
|
|
516
|
+
if (buffer.length > inlineMaxBytes) {
|
|
517
|
+
if (config.debug) ctx.logger("pig").warn(`Background too large for data URL (${buffer.length} bytes), using file URL`);
|
|
518
|
+
return normalized;
|
|
519
|
+
}
|
|
520
|
+
if (config.debug) ctx.logger("pig").debug(`Loaded local background file: ${filePath} (${mime}, ${buffer.length} bytes)`);
|
|
521
|
+
return `data:${mime};base64,${buffer.toString("base64")}`;
|
|
522
|
+
} catch (e) {
|
|
523
|
+
if (config.debug) ctx.logger("pig").warn(`Failed to read local background file: ${e}`);
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (/^https?:\/\//i.test(normalized)) {
|
|
528
|
+
try {
|
|
529
|
+
if (config.debug) ctx.logger("pig").debug(`Server-side fetching background: ${normalized}`);
|
|
530
|
+
const response = await ctx.http(normalized, {
|
|
531
|
+
responseType: "arraybuffer",
|
|
532
|
+
timeout: 15e3,
|
|
533
|
+
headers: {
|
|
534
|
+
"User-Agent": "Mozilla/5.0",
|
|
535
|
+
"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"
|
|
536
|
+
},
|
|
537
|
+
redirect: "follow"
|
|
538
|
+
});
|
|
539
|
+
const contentType = response.headers.get("content-type") || "";
|
|
540
|
+
const contentLength = response.headers.get("content-length") || "unknown";
|
|
541
|
+
if (config.debug) {
|
|
542
|
+
ctx.logger("pig").debug(
|
|
543
|
+
`Background fetch response: status=${response.status} url=${response.url} content-type=${contentType || "unknown"} content-length=${contentLength}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (!contentType.startsWith("image/")) {
|
|
547
|
+
if (config.debug) ctx.logger("pig").warn(`Background fetch returned non-image content-type: ${contentType || "unknown"}`);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
const buffer = Buffer.from(response.data);
|
|
551
|
+
const mime = sniffMime(buffer);
|
|
552
|
+
if (buffer.length > inlineMaxBytes) {
|
|
553
|
+
if (config.debug) ctx.logger("pig").warn(`Background too large for data URL (${buffer.length} bytes), using remote URL`);
|
|
554
|
+
return normalized;
|
|
555
|
+
}
|
|
556
|
+
if (config.debug) ctx.logger("pig").debug(`Background fetched and decoded: ${mime}, ${buffer.length} bytes`);
|
|
557
|
+
return `data:${mime};base64,${buffer.toString("base64")}`;
|
|
558
|
+
} catch (e) {
|
|
559
|
+
if (config.debug) ctx.logger("pig").warn(`Failed to fetch background server-side: ${e}`);
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return normalized;
|
|
564
|
+
}, "fetchToDataUrl");
|
|
565
|
+
if (bgImage) {
|
|
566
|
+
bgImage = normalizeImageUrl(bgImage);
|
|
567
|
+
const fetched = await fetchToDataUrl(bgImage);
|
|
568
|
+
if (fetched) {
|
|
569
|
+
bgImage = fetched;
|
|
570
|
+
if (config.debug && fetched.startsWith("data:")) {
|
|
571
|
+
ctx.logger("pig").debug(`Successfully converted background to base64 (length=${fetched.length})`);
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
if (config.debug) ctx.logger("pig").warn("Background fetch failed, trying static fallback");
|
|
575
|
+
const fallbackLoc = LOCATIONS.find(
|
|
576
|
+
(l) => l.country === data.location.country || l.timezone === data.location.timezone
|
|
577
|
+
);
|
|
578
|
+
if (fallbackLoc && fallbackLoc.landscapeUrl !== bgImage) {
|
|
579
|
+
if (config.debug) ctx.logger("pig").info(`Found fallback static location: ${fallbackLoc.landmark} (${fallbackLoc.landscapeUrl})`);
|
|
580
|
+
const fallback = await fetchToDataUrl(fallbackLoc.landscapeUrl);
|
|
581
|
+
if (fallback) {
|
|
582
|
+
bgImage = fallback;
|
|
583
|
+
if (config.debug) ctx.logger("pig").info("Successfully used fallback static image");
|
|
584
|
+
} else if (config.debug) {
|
|
585
|
+
ctx.logger("pig").warn("Fallback static image also failed");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (config.debug) {
|
|
590
|
+
ctx.logger("pig").debug(`Final background value: ${bgImage ? bgImage.startsWith("data:") ? `data-url(${bgImage.length})` : bgImage : "none"}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const now = /* @__PURE__ */ new Date();
|
|
594
|
+
const dateStr = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`;
|
|
595
|
+
const html = `
|
|
596
|
+
<!DOCTYPE html>
|
|
597
|
+
<html>
|
|
598
|
+
<head>
|
|
599
|
+
<meta charset="UTF-8">
|
|
600
|
+
<style>
|
|
601
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&display=swap');
|
|
602
|
+
|
|
603
|
+
* {
|
|
604
|
+
margin: 0;
|
|
605
|
+
padding: 0;
|
|
606
|
+
box-sizing: border-box;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
body {
|
|
610
|
+
width: 1080px;
|
|
611
|
+
height: 1920px;
|
|
612
|
+
overflow: hidden;
|
|
613
|
+
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
|
614
|
+
background: #f0f0f2;
|
|
615
|
+
--bg-image: url('${bgImage}');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.wrapper {
|
|
619
|
+
position: relative;
|
|
620
|
+
width: 100%;
|
|
621
|
+
height: 100%;
|
|
622
|
+
overflow: hidden;
|
|
623
|
+
background: #f0f0f2;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* Dynamic Background Image */
|
|
627
|
+
.bg-image {
|
|
628
|
+
position: absolute;
|
|
629
|
+
inset: 0;
|
|
630
|
+
width: 100%;
|
|
631
|
+
height: 100%;
|
|
632
|
+
background-color: #d1d1d6;
|
|
633
|
+
background-image: var(--bg-image);
|
|
634
|
+
background-size: cover;
|
|
635
|
+
background-position: center;
|
|
636
|
+
z-index: 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/* Gradient Overlay for Depth */
|
|
640
|
+
.bg-overlay {
|
|
641
|
+
position: absolute;
|
|
642
|
+
inset: 0;
|
|
643
|
+
width: 100%;
|
|
644
|
+
height: 100%;
|
|
645
|
+
background: linear-gradient(
|
|
646
|
+
to bottom,
|
|
647
|
+
rgba(0,0,0,0) 0%,
|
|
648
|
+
rgba(0,0,0,0.1) 60%,
|
|
649
|
+
rgba(0,0,0,0.4) 100%
|
|
650
|
+
);
|
|
651
|
+
z-index: 1;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.card-container {
|
|
655
|
+
position: absolute;
|
|
656
|
+
bottom: 0;
|
|
657
|
+
left: 0;
|
|
658
|
+
width: 100%;
|
|
659
|
+
padding: 72px;
|
|
660
|
+
padding-bottom: 72px;
|
|
661
|
+
z-index: 10;
|
|
662
|
+
display: flex;
|
|
663
|
+
flex-direction: column;
|
|
664
|
+
justify-content: flex-end;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Advanced Liquid Glass Card (Based on user request) */
|
|
668
|
+
.glass-card {
|
|
669
|
+
position: relative;
|
|
670
|
+
border-radius: 56px;
|
|
671
|
+
padding: 56px 60px 60px;
|
|
672
|
+
overflow: hidden;
|
|
673
|
+
|
|
674
|
+
/* 基础半透明底色,保证文字可读性 */
|
|
675
|
+
background: rgba(255, 255, 255, 0.45);
|
|
676
|
+
|
|
677
|
+
/* 细腻的边框 */
|
|
678
|
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
679
|
+
border-top: 1px solid rgba(255, 255, 255, 0.8);
|
|
680
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
|
681
|
+
|
|
682
|
+
box-shadow:
|
|
683
|
+
0 40px 80px -20px rgba(0,0,0,0.3),
|
|
684
|
+
inset 0 0 0 1px rgba(255,255,255,0.3);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/* 模拟厚度与折射 (Refraction Layer) */
|
|
688
|
+
.glass-card::after {
|
|
689
|
+
content: "";
|
|
690
|
+
position: absolute;
|
|
691
|
+
inset: 0;
|
|
692
|
+
/* 关键:使用 fixed attachment 模拟透视背景 */
|
|
693
|
+
background-image: var(--bg-image);
|
|
694
|
+
background-attachment: fixed;
|
|
695
|
+
background-size: cover;
|
|
696
|
+
background-position: center;
|
|
697
|
+
|
|
698
|
+
/* 模糊处理,模拟毛玻璃内部的散射 */
|
|
699
|
+
filter: blur(25px) brightness(1.1);
|
|
700
|
+
opacity: 0.7;
|
|
701
|
+
z-index: -1;
|
|
702
|
+
|
|
703
|
+
/* 稍微缩小一点范围,制造边缘的玻璃厚度感 */
|
|
704
|
+
margin: 4px;
|
|
705
|
+
border-radius: 52px;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* 强力滤镜层 (Filter Layer) */
|
|
709
|
+
.glass-card::before {
|
|
710
|
+
content: "";
|
|
711
|
+
position: absolute;
|
|
712
|
+
inset: -20%;
|
|
713
|
+
width: 140%;
|
|
714
|
+
height: 140%;
|
|
715
|
+
|
|
716
|
+
/* 高级滤镜组合:模糊 + 饱和度提升 + 对比度微调 */
|
|
717
|
+
backdrop-filter: blur(40px) saturate(180%) contrast(110%);
|
|
718
|
+
-webkit-backdrop-filter: blur(40px) saturate(180%) contrast(110%);
|
|
719
|
+
|
|
720
|
+
z-index: -2;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* Noise Texture - Moved to .card-content::before */
|
|
724
|
+
.card-content::before {
|
|
725
|
+
content: "";
|
|
726
|
+
position: absolute;
|
|
727
|
+
inset: 0;
|
|
728
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.05'/%3E%3C/svg%3E");
|
|
729
|
+
opacity: 0.1;
|
|
730
|
+
mix-blend-mode: overlay;
|
|
731
|
+
pointer-events: none;
|
|
732
|
+
z-index: -1;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* Content Wrapper */
|
|
736
|
+
.card-content {
|
|
737
|
+
position: relative;
|
|
738
|
+
z-index: 2;
|
|
739
|
+
display: flex;
|
|
740
|
+
flex-direction: column;
|
|
741
|
+
gap: 36px;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.header {
|
|
745
|
+
display: flex;
|
|
746
|
+
align-items: center;
|
|
747
|
+
gap: 24px;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.avatar-ring {
|
|
751
|
+
padding: 6px;
|
|
752
|
+
background: white;
|
|
753
|
+
border-radius: 50%;
|
|
754
|
+
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.avatar {
|
|
758
|
+
display: block;
|
|
759
|
+
width: 108px;
|
|
760
|
+
height: 108px;
|
|
761
|
+
border-radius: 50%;
|
|
762
|
+
object-fit: cover;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.user-meta {
|
|
766
|
+
display: flex;
|
|
767
|
+
flex-direction: column;
|
|
768
|
+
gap: 8px;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.username {
|
|
772
|
+
font-size: 44px;
|
|
773
|
+
font-weight: 800;
|
|
774
|
+
color: #1d1d1f;
|
|
775
|
+
letter-spacing: -0.02em;
|
|
776
|
+
line-height: 1.2;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.date {
|
|
780
|
+
font-size: 24px;
|
|
781
|
+
font-weight: 500;
|
|
782
|
+
color: rgba(0, 0, 0, 0.5);
|
|
783
|
+
letter-spacing: 0.02em;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.message-body {
|
|
787
|
+
padding: 10px 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.message-text {
|
|
791
|
+
font-size: 56px;
|
|
792
|
+
line-height: 1.25;
|
|
793
|
+
font-weight: 800;
|
|
794
|
+
color: #1d1d1f;
|
|
795
|
+
letter-spacing: -0.03em;
|
|
796
|
+
word-break: break-word;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.highlight {
|
|
800
|
+
color: #007AFF;
|
|
801
|
+
background: linear-gradient(120deg, rgba(0,122,255,0.08) 0%, rgba(0,122,255,0.15) 100%);
|
|
802
|
+
padding: 2px 12px;
|
|
803
|
+
border-radius: 14px;
|
|
804
|
+
box-decoration-break: clone;
|
|
805
|
+
-webkit-box-decoration-break: clone;
|
|
806
|
+
display: inline-block;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.divider {
|
|
810
|
+
height: 2px;
|
|
811
|
+
background: rgba(0,0,0,0.08);
|
|
812
|
+
border-radius: 2px;
|
|
813
|
+
width: 100%;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.footer {
|
|
817
|
+
display: flex;
|
|
818
|
+
justify-content: space-between;
|
|
819
|
+
align-items: flex-end;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.location-group {
|
|
823
|
+
display: flex;
|
|
824
|
+
flex-direction: column;
|
|
825
|
+
gap: 16px;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.location-pill {
|
|
829
|
+
display: inline-flex;
|
|
830
|
+
align-items: center;
|
|
831
|
+
gap: 12px;
|
|
832
|
+
background: #007AFF;
|
|
833
|
+
padding: 12px 28px;
|
|
834
|
+
border-radius: 100px;
|
|
835
|
+
box-shadow: 0 8px 20px rgba(0,122,255,0.25);
|
|
836
|
+
width: fit-content;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.location-pill span {
|
|
840
|
+
font-size: 26px;
|
|
841
|
+
font-weight: 700;
|
|
842
|
+
color: #ffffff;
|
|
843
|
+
letter-spacing: 0.02em;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.landmark-name {
|
|
847
|
+
font-size: 38px;
|
|
848
|
+
font-weight: 800;
|
|
849
|
+
color: #1d1d1f;
|
|
850
|
+
letter-spacing: -0.01em;
|
|
851
|
+
padding-left: 6px;
|
|
852
|
+
opacity: 0.8;
|
|
853
|
+
line-height: 1.2;
|
|
854
|
+
word-break: break-word;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.brand-tag {
|
|
858
|
+
display: flex;
|
|
859
|
+
flex-direction: column;
|
|
860
|
+
align-items: flex-end;
|
|
861
|
+
opacity: 0.4;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.brand-icon {
|
|
865
|
+
font-size: 50px;
|
|
866
|
+
margin-bottom: 4px;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.brand-name {
|
|
870
|
+
font-size: 20px;
|
|
871
|
+
font-weight: 800;
|
|
872
|
+
text-transform: uppercase;
|
|
873
|
+
letter-spacing: 0.2em;
|
|
874
|
+
color: #1d1d1f;
|
|
875
|
+
}
|
|
876
|
+
</style>
|
|
877
|
+
</head>
|
|
878
|
+
<body>
|
|
879
|
+
<div class="wrapper">
|
|
880
|
+
<div class="bg-image"></div>
|
|
881
|
+
<div class="bg-overlay"></div>
|
|
882
|
+
|
|
883
|
+
<div class="card-container">
|
|
884
|
+
<div class="glass-card">
|
|
885
|
+
<div class="card-content">
|
|
886
|
+
|
|
887
|
+
<div class="header">
|
|
888
|
+
<div class="avatar-ring">
|
|
889
|
+
<img class="avatar" src="${avatarUrl}" onerror="this.src='https://ui-avatars.com/api/?name=U&background=random'" />
|
|
890
|
+
</div>
|
|
891
|
+
<div class="user-meta">
|
|
892
|
+
<div class="username">${escapeHtml(username)}</div>
|
|
893
|
+
<div class="date">${dateStr}</div>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
<div class="message-body">
|
|
898
|
+
<div class="message-text">
|
|
899
|
+
今天 🐷猪醒在<br/>
|
|
900
|
+
<span class="highlight">${data.location.landmarkZh || data.location.landmark}</span>
|
|
901
|
+
</div>
|
|
902
|
+
</div>
|
|
903
|
+
|
|
904
|
+
<div class="divider"></div>
|
|
905
|
+
|
|
906
|
+
<div class="footer">
|
|
907
|
+
<div class="location-group">
|
|
908
|
+
<div class="location-pill">
|
|
909
|
+
<span>📍 ${data.location.countryZh || data.location.country} · ${data.location.city}</span>
|
|
910
|
+
</div>
|
|
911
|
+
<div class="landmark-name">${data.location.landmark}</div>
|
|
912
|
+
</div>
|
|
913
|
+
|
|
914
|
+
<div class="brand-tag">
|
|
915
|
+
<div class="brand-icon">🐷</div>
|
|
916
|
+
<div class="brand-name">Pig Travel</div>
|
|
917
|
+
</div>
|
|
918
|
+
</div>
|
|
919
|
+
|
|
920
|
+
</div>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
924
|
+
|
|
925
|
+
<script>
|
|
926
|
+
async function waitForImages() {
|
|
927
|
+
console.log('Starting waitForImages...');
|
|
928
|
+
// 1. Wait for standard img tags
|
|
929
|
+
const images = Array.from(document.images);
|
|
930
|
+
console.log('Found ' + images.length + ' standard images');
|
|
931
|
+
|
|
932
|
+
// 2. Identify and wait for background images
|
|
933
|
+
const bgImages = [];
|
|
934
|
+
const allElements = document.getElementsByTagName('*');
|
|
935
|
+
for (let i = 0; i < allElements.length; i++) {
|
|
936
|
+
const bg = window.getComputedStyle(allElements[i]).backgroundImage;
|
|
937
|
+
if (bg && bg !== 'none' && bg.includes('url(')) {
|
|
938
|
+
const urlMatch = bg.match(/url(["']?(.+?)["']?)/);
|
|
939
|
+
if (urlMatch) {
|
|
940
|
+
console.log('Found background image: ' + urlMatch[1]);
|
|
941
|
+
const img = new Image();
|
|
942
|
+
img.src = urlMatch[1];
|
|
943
|
+
bgImages.push(img);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const allToLoad = [...images, ...bgImages];
|
|
949
|
+
console.log('Total images to load: ' + allToLoad.length);
|
|
950
|
+
const promises = allToLoad.map((img, index) => {
|
|
951
|
+
if (img.complete) {
|
|
952
|
+
console.log('Image ' + index + ' already complete: ' + img.src);
|
|
953
|
+
return Promise.resolve();
|
|
954
|
+
}
|
|
955
|
+
return new Promise(resolve => {
|
|
956
|
+
img.onload = () => {
|
|
957
|
+
console.log('Image ' + index + ' loaded successfully: ' + img.src);
|
|
958
|
+
resolve();
|
|
959
|
+
};
|
|
960
|
+
img.onerror = () => {
|
|
961
|
+
console.warn('Image ' + index + ' failed to load: ' + img.src);
|
|
962
|
+
resolve();
|
|
963
|
+
};
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
await Promise.all([
|
|
968
|
+
...promises,
|
|
969
|
+
document.fonts.ready
|
|
970
|
+
]);
|
|
971
|
+
|
|
972
|
+
console.log('All images and fonts loaded');
|
|
973
|
+
// Extra safety delay for rendering
|
|
974
|
+
await new Promise(r => setTimeout(r, 400));
|
|
975
|
+
}
|
|
976
|
+
window.renderReady = waitForImages();
|
|
977
|
+
</script>
|
|
978
|
+
</body>
|
|
979
|
+
</html>
|
|
980
|
+
`;
|
|
981
|
+
try {
|
|
982
|
+
const page = await ctx.puppeteer.page();
|
|
983
|
+
if (config.debug) {
|
|
984
|
+
page.on("console", (msg) => ctx.logger("pig").debug(`[Browser] ${msg.text()}`));
|
|
985
|
+
page.on("requestfailed", (request) => {
|
|
986
|
+
ctx.logger("pig").warn(`[Browser] Request failed: ${request.url()} - ${request.failure()?.errorText}`);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
await page.setViewport({ width: 1080, height: 1920, deviceScaleFactor: 1 });
|
|
990
|
+
await page.setContent(html, { waitUntil: "networkidle0" });
|
|
991
|
+
await page.evaluate(() => window["renderReady"]);
|
|
992
|
+
const buffer = await page.screenshot({ type: "png", fullPage: true });
|
|
993
|
+
await page.close();
|
|
994
|
+
const filename = `pig_${userInfo.userId}_${now.getTime()}.png`;
|
|
995
|
+
ctx.logger("pig").info(`足迹卡片已生成: ${filename}`);
|
|
996
|
+
return { buffer, filename };
|
|
997
|
+
} catch (e) {
|
|
998
|
+
ctx.logger("pig").error("Failed to generate card", e);
|
|
999
|
+
throw e;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
__name(generateFootprintCard, "generateFootprintCard");
|
|
1003
|
+
function escapeHtml(text) {
|
|
1004
|
+
const map = {
|
|
1005
|
+
"&": "&",
|
|
1006
|
+
"<": "<",
|
|
1007
|
+
">": ">",
|
|
1008
|
+
'"': """,
|
|
1009
|
+
"'": "'"
|
|
1010
|
+
};
|
|
1011
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
1012
|
+
}
|
|
1013
|
+
__name(escapeHtml, "escapeHtml");
|
|
1014
|
+
|
|
1015
|
+
// src/services/location.ts
|
|
1016
|
+
var import_messages = require("@langchain/core/messages");
|
|
1017
|
+
|
|
1018
|
+
// src/services/unsplash.ts
|
|
1019
|
+
var UNSPLASH_API_BASE = "https://api.unsplash.com";
|
|
1020
|
+
async function searchUnsplashPhoto(ctx, accessKey, query, debug = false) {
|
|
1021
|
+
if (!accessKey) {
|
|
1022
|
+
if (debug) ctx.logger("pig").debug("Unsplash: No access key provided");
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
if (debug) ctx.logger("pig").debug(`Unsplash: Searching for "${query}"`);
|
|
1027
|
+
const response = await ctx.http.get(
|
|
1028
|
+
`${UNSPLASH_API_BASE}/search/photos`,
|
|
1029
|
+
{
|
|
1030
|
+
params: {
|
|
1031
|
+
query,
|
|
1032
|
+
per_page: 1,
|
|
1033
|
+
orientation: "landscape"
|
|
1034
|
+
},
|
|
1035
|
+
headers: {
|
|
1036
|
+
Authorization: `Client-ID ${accessKey}`,
|
|
1037
|
+
"Accept-Version": "v1"
|
|
1038
|
+
},
|
|
1039
|
+
timeout: 1e4
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
if (response.results && response.results.length > 0) {
|
|
1043
|
+
const photoUrl = response.results[0].urls.full;
|
|
1044
|
+
if (debug) ctx.logger("pig").debug(`Unsplash: Found photo URL: ${photoUrl}`);
|
|
1045
|
+
return photoUrl;
|
|
1046
|
+
}
|
|
1047
|
+
if (debug) ctx.logger("pig").debug("Unsplash: No photos found for query");
|
|
1048
|
+
return null;
|
|
1049
|
+
} catch (e) {
|
|
1050
|
+
ctx.logger("pig").warn(`Unsplash API error: ${e}`);
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
__name(searchUnsplashPhoto, "searchUnsplashPhoto");
|
|
1055
|
+
|
|
1056
|
+
// src/services/location.ts
|
|
1057
|
+
var LOCATION_CATEGORIES = [
|
|
1058
|
+
"自然奇观(峡谷、瀑布、火山、冰川、沙漠绿洲等)",
|
|
1059
|
+
"历史遗迹(古城、废墟、考古遗址、古老寺庙等)",
|
|
1060
|
+
"现代建筑奇观(摩天大楼、桥梁、博物馆、体育场等)",
|
|
1061
|
+
"小众秘境(冷门国家的隐藏景点、当地人才知道的地方)",
|
|
1062
|
+
"极地与边境地区(南极科考站、北极圈小镇、国境线上的奇特地点)",
|
|
1063
|
+
"海岛与海滨(珊瑚礁、海崖、灯塔、渔村等)",
|
|
1064
|
+
"高山与高原(山峰、高原湖泊、山间村落、登山营地等)",
|
|
1065
|
+
"文化地标(宗教圣地、传统村落、民俗景点等)",
|
|
1066
|
+
"都市风情(特色街区、夜景地标、城市公园等)",
|
|
1067
|
+
"神秘与奇特之地(地质奇观、UFO小镇、怪异地貌等)"
|
|
1068
|
+
];
|
|
1069
|
+
var CONTINENTS = ["亚洲", "欧洲", "非洲", "北美洲", "南美洲", "大洋洲", "南极洲"];
|
|
1070
|
+
function getRandomPromptHints() {
|
|
1071
|
+
const category = LOCATION_CATEGORIES[Math.floor(Math.random() * LOCATION_CATEGORIES.length)];
|
|
1072
|
+
const continent = CONTINENTS[Math.floor(Math.random() * CONTINENTS.length)];
|
|
1073
|
+
const hotCountries = ["法国", "日本", "意大利", "美国", "英国", "中国", "西班牙", "泰国", "澳大利亚"];
|
|
1074
|
+
const shuffled = hotCountries.sort(() => Math.random() - 0.5);
|
|
1075
|
+
const avoidCountries = shuffled.slice(0, 3 + Math.floor(Math.random() * 4)).join("、");
|
|
1076
|
+
return { category, continent, avoidCountries };
|
|
1077
|
+
}
|
|
1078
|
+
__name(getRandomPromptHints, "getRandomPromptHints");
|
|
1079
|
+
var LOCATION_GENERATION_PROMPT = `你是一位资深旅行探险家,专门发掘世界各地的独特目的地。
|
|
1080
|
+
|
|
1081
|
+
你的任务是生成一个真实存在的旅游目的地。要求:
|
|
1082
|
+
1. 地点必须真实存在,可以是著名景点,也可以是小众秘境
|
|
1083
|
+
2. 提供准确的地理信息
|
|
1084
|
+
3. 尽量选择有趣、独特、不常见的地点
|
|
1085
|
+
4. 避免总是选择最热门的旅游景点
|
|
1086
|
+
|
|
1087
|
+
请严格按照以下JSON格式输出(不要包含任何其他文字):
|
|
1088
|
+
{
|
|
1089
|
+
"country": "国家英文名",
|
|
1090
|
+
"countryZh": "国家中文名",
|
|
1091
|
+
"city": "城市/地区名",
|
|
1092
|
+
"landmark": "地标英文名",
|
|
1093
|
+
"landmarkZh": "地标中文名",
|
|
1094
|
+
"timezone": "IANA时区字符串",
|
|
1095
|
+
"landscapeUrl": "https://images.unsplash.com/featured/?地标英文名"
|
|
1096
|
+
}`;
|
|
1097
|
+
async function generateLocationWithLLM(ctx, config) {
|
|
1098
|
+
if (!config.llmLocationEnabled || !config.llmLocationModel || !ctx.chatluna) {
|
|
1099
|
+
ctx.logger("pig").debug("LLM location generation not enabled or not available, using static locations");
|
|
1100
|
+
return getRandomStaticLocation();
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
ctx.logger("pig").info(`Using LLM to generate location with model: ${config.llmLocationModel}`);
|
|
1104
|
+
const modelRef = await ctx.chatluna.createChatModel(config.llmLocationModel);
|
|
1105
|
+
const model = modelRef.value;
|
|
1106
|
+
if (!model) {
|
|
1107
|
+
ctx.logger("pig").warn(`Failed to create model: ${config.llmLocationModel}`);
|
|
1108
|
+
return getRandomStaticLocation();
|
|
1109
|
+
}
|
|
1110
|
+
const hints = getRandomPromptHints();
|
|
1111
|
+
const userPrompt = `请生成一个位于【${hints.continent}】的【${hints.category}】类型的旅游目的地。
|
|
1112
|
+
|
|
1113
|
+
特别要求:这次请避开 ${hints.avoidCountries} 这些热门国家,选择一个更独特、更少人知道的地方。
|
|
1114
|
+
|
|
1115
|
+
直接输出JSON,不要有任何其他文字。`;
|
|
1116
|
+
if (config.debug) ctx.logger("pig").debug(`Location prompt hints: ${hints.continent}, ${hints.category}`);
|
|
1117
|
+
const messages = [
|
|
1118
|
+
new import_messages.SystemMessage(LOCATION_GENERATION_PROMPT),
|
|
1119
|
+
new import_messages.HumanMessage(userPrompt)
|
|
1120
|
+
];
|
|
1121
|
+
const response = await model.invoke(messages, { temperature: 1 });
|
|
1122
|
+
const content = typeof response.content === "string" ? response.content : JSON.stringify(response.content);
|
|
1123
|
+
if (config.debug) ctx.logger("pig").debug(`LLM response: ${content}`);
|
|
1124
|
+
const location = parseLocationResponse(content);
|
|
1125
|
+
if (location) {
|
|
1126
|
+
ctx.logger("pig").info(`LLM generated location: ${location.landmarkZh} (${location.landmark}), ${location.countryZh}`);
|
|
1127
|
+
if (config.unsplashAccessKey) {
|
|
1128
|
+
const searchQueries = [
|
|
1129
|
+
`${location.landmark} ${location.country}`,
|
|
1130
|
+
// Most specific: landmark + country
|
|
1131
|
+
location.city ? `${location.city} ${location.country}` : null,
|
|
1132
|
+
// City + country
|
|
1133
|
+
`${location.country} landscape`,
|
|
1134
|
+
// Country landscape
|
|
1135
|
+
location.country
|
|
1136
|
+
// Just country name
|
|
1137
|
+
].filter(Boolean);
|
|
1138
|
+
let photoUrl = null;
|
|
1139
|
+
for (const query of searchQueries) {
|
|
1140
|
+
if (config.debug) ctx.logger("pig").debug(`Searching Unsplash for: ${query}`);
|
|
1141
|
+
photoUrl = await searchUnsplashPhoto(ctx, config.unsplashAccessKey, query, config.debug);
|
|
1142
|
+
if (photoUrl) {
|
|
1143
|
+
if (config.debug) ctx.logger("pig").info(`Using Unsplash photo: ${photoUrl}`);
|
|
1144
|
+
location.landscapeUrl = photoUrl;
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (!photoUrl && config.debug) {
|
|
1149
|
+
ctx.logger("pig").debug("All Unsplash searches returned no results, using LLM-generated URL");
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return location;
|
|
1153
|
+
}
|
|
1154
|
+
ctx.logger("pig").warn("Failed to parse LLM response, falling back to static locations");
|
|
1155
|
+
return getRandomStaticLocation();
|
|
1156
|
+
} catch (e) {
|
|
1157
|
+
ctx.logger("pig").error(`LLM location generation failed: ${e}`);
|
|
1158
|
+
return getRandomStaticLocation();
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
__name(generateLocationWithLLM, "generateLocationWithLLM");
|
|
1162
|
+
function parseLocationResponse(content) {
|
|
1163
|
+
try {
|
|
1164
|
+
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1165
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : content;
|
|
1166
|
+
const data = JSON.parse(jsonStr.trim());
|
|
1167
|
+
if (!data.country || !data.landmark || !data.landscapeUrl) {
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
const location = {
|
|
1171
|
+
country: data.country,
|
|
1172
|
+
countryZh: data.countryZh || data.country,
|
|
1173
|
+
city: data.city || "",
|
|
1174
|
+
landmark: data.landmark,
|
|
1175
|
+
landmarkZh: data.landmarkZh || data.landmark,
|
|
1176
|
+
timezone: data.timezone || "UTC",
|
|
1177
|
+
landscapeUrl: data.landscapeUrl
|
|
1178
|
+
};
|
|
1179
|
+
if (!location.landscapeUrl.startsWith("http")) {
|
|
1180
|
+
location.landscapeUrl = `https://images.unsplash.com/featured/?${encodeURIComponent(location.landmark)},${encodeURIComponent(location.country)}`;
|
|
1181
|
+
}
|
|
1182
|
+
return location;
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
try {
|
|
1185
|
+
const objectMatch = content.match(/\{[\s\S]*"country"[\s\S]*"landmark"[\s\S]*\}/);
|
|
1186
|
+
if (objectMatch) {
|
|
1187
|
+
return parseLocationResponse(objectMatch[0]);
|
|
1188
|
+
}
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
__name(parseLocationResponse, "parseLocationResponse");
|
|
1195
|
+
function getRandomStaticLocation() {
|
|
1196
|
+
return LOCATIONS[Math.floor(Math.random() * LOCATIONS.length)];
|
|
1197
|
+
}
|
|
1198
|
+
__name(getRandomStaticLocation, "getRandomStaticLocation");
|
|
1199
|
+
|
|
1200
|
+
// src/services/travel.ts
|
|
1201
|
+
async function triggerTravelSequence(ctx, config, userInfo, platform) {
|
|
1202
|
+
let location;
|
|
1203
|
+
if (config.llmLocationEnabled && ctx.chatluna) {
|
|
1204
|
+
location = await generateLocationWithLLM(ctx, config);
|
|
1205
|
+
} else {
|
|
1206
|
+
location = getRandomStaticLocation();
|
|
1207
|
+
}
|
|
1208
|
+
let imageBuffer = null;
|
|
1209
|
+
let imageUrl = null;
|
|
1210
|
+
let isAIGC = false;
|
|
1211
|
+
const msg = config.travelMessageTemplate.replace("{landmark}", location.landmarkZh || location.landmark).replace("{country}", location.countryZh || location.country);
|
|
1212
|
+
if (config.outputMode === "image") {
|
|
1213
|
+
let backgroundUrl = null;
|
|
1214
|
+
if (config.aigcEnabled && config.aigcChannel && ctx.mediaLuna) {
|
|
1215
|
+
isAIGC = true;
|
|
1216
|
+
ctx.logger("pig").info(`使用 media-luna 生成 AIGC 图片: ${userInfo.userId} @ ${location.landmark}`);
|
|
1217
|
+
const prompt = config.aigcPrompt.replace("{landmark}", location.landmark).replace("{country}", location.country);
|
|
1218
|
+
try {
|
|
1219
|
+
const result = await ctx.mediaLuna.generateByName({
|
|
1220
|
+
channelName: config.aigcChannel,
|
|
1221
|
+
prompt,
|
|
1222
|
+
session: null
|
|
1223
|
+
});
|
|
1224
|
+
if (result.success && result.output && result.output.length > 0) {
|
|
1225
|
+
const output = result.output[0];
|
|
1226
|
+
if (output.url) {
|
|
1227
|
+
backgroundUrl = output.url;
|
|
1228
|
+
ctx.logger("pig").info(`AIGC 背景图已生成: ${backgroundUrl}`);
|
|
1229
|
+
}
|
|
1230
|
+
} else {
|
|
1231
|
+
ctx.logger("pig").warn(`AIGC 生成失败: ${result.error}`);
|
|
1232
|
+
isAIGC = false;
|
|
1233
|
+
}
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
ctx.logger("pig").error(`AIGC 调用失败: ${e}`);
|
|
1236
|
+
isAIGC = false;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (!backgroundUrl) {
|
|
1240
|
+
backgroundUrl = location.landscapeUrl;
|
|
1241
|
+
ctx.logger("pig").info(`使用预设风景图: ${location.landmark} (${backgroundUrl})`);
|
|
1242
|
+
}
|
|
1243
|
+
if (config.debug) {
|
|
1244
|
+
ctx.logger("pig").debug(`最终确定的背景图 URL: ${backgroundUrl}`);
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
const cardData = { location, msg };
|
|
1248
|
+
const cardResult = await generateFootprintCard(ctx, config, cardData, userInfo, platform, backgroundUrl);
|
|
1249
|
+
imageBuffer = cardResult.buffer;
|
|
1250
|
+
if (config.useStorageService && ctx.chatluna_storage) {
|
|
1251
|
+
try {
|
|
1252
|
+
const fileInfo = await ctx.chatluna_storage.createTempFile(
|
|
1253
|
+
cardResult.buffer,
|
|
1254
|
+
cardResult.filename,
|
|
1255
|
+
config.storageCacheHours
|
|
1256
|
+
);
|
|
1257
|
+
imageUrl = fileInfo.url;
|
|
1258
|
+
ctx.logger("pig").info(`卡片已上传到存储服务: ${imageUrl}`);
|
|
1259
|
+
} catch (e) {
|
|
1260
|
+
ctx.logger("pig").warn(`上传到存储服务失败: ${e}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
} catch (e) {
|
|
1264
|
+
ctx.logger("pig").error(`生成足迹卡片失败: ${e}`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const now = /* @__PURE__ */ new Date();
|
|
1268
|
+
await ctx.database.create("pig_travel_log", {
|
|
1269
|
+
userId: userInfo.userId,
|
|
1270
|
+
platform,
|
|
1271
|
+
timestamp: now,
|
|
1272
|
+
country: location.country,
|
|
1273
|
+
location: location.landmark,
|
|
1274
|
+
imagePath: imageUrl || "",
|
|
1275
|
+
// 存储 URL 或空
|
|
1276
|
+
isAIGC
|
|
1277
|
+
});
|
|
1278
|
+
return {
|
|
1279
|
+
location,
|
|
1280
|
+
imageBuffer,
|
|
1281
|
+
imageUrl,
|
|
1282
|
+
isAIGC,
|
|
1283
|
+
msg
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
__name(triggerTravelSequence, "triggerTravelSequence");
|
|
1287
|
+
|
|
1288
|
+
// src/config.ts
|
|
1289
|
+
var import_koishi = require("koishi");
|
|
1290
|
+
var Config = import_koishi.Schema.object({
|
|
1291
|
+
sunriseApi: import_koishi.Schema.string().default("https://api.sunrise-sunset.org/json").description("日出日落 API 地址"),
|
|
1292
|
+
defaultLat: import_koishi.Schema.number().default(30).description("默认纬度(北纬为正,南纬为负,范围 -90 到 90)"),
|
|
1293
|
+
defaultLng: import_koishi.Schema.number().default(120).description("默认经度(东经为正,西经为负,范围 -180 到 180)"),
|
|
1294
|
+
abnormalThreshold: import_koishi.Schema.number().default(3).description("异常作息阈值(小时)"),
|
|
1295
|
+
outputMode: import_koishi.Schema.union(["text", "image"]).default("text").description("输出模式"),
|
|
1296
|
+
useStorageService: import_koishi.Schema.boolean().default(true).description("使用 chatluna-storage-service 管理文件缓存(推荐,需安装该插件)。关闭时使用 base64 直接发送。"),
|
|
1297
|
+
storageCacheHours: import_koishi.Schema.number().default(24).description("存储服务缓存时间(小时)"),
|
|
1298
|
+
travelMessageTemplate: import_koishi.Schema.string().default("去了 {landmark},{country}!📸").description("旅行消息模板(可用变量:{landmark} 地标名, {country} 国家名)"),
|
|
1299
|
+
aigcEnabled: import_koishi.Schema.boolean().default(false).description("启用 AI 生成图片(需要 media-luna 插件)"),
|
|
1300
|
+
aigcChannel: import_koishi.Schema.string().default("").description("AI 生图渠道名称(media-luna 渠道)"),
|
|
1301
|
+
aigcPrompt: import_koishi.Schema.string().default("一个可爱的卡通小猪正在 {country} 的 {landmark} 前面自拍,阳光明媚,旅游照片风格").description("AI 生图提示词模板"),
|
|
1302
|
+
logPath: import_koishi.Schema.string().default("./data/pig/logs").description("旅行日志存储路径(仅在不使用存储服务时生效)"),
|
|
1303
|
+
// LLM location generation
|
|
1304
|
+
llmLocationEnabled: import_koishi.Schema.boolean().default(false).description("启用 LLM 动态生成旅行地点(需要 chatluna 插件)"),
|
|
1305
|
+
llmLocationModel: import_koishi.Schema.dynamic("model").description("用于生成地点的 LLM 模型(需要支持中文输出)"),
|
|
1306
|
+
// Unsplash API
|
|
1307
|
+
unsplashAccessKey: import_koishi.Schema.string().default("").description("Unsplash API Access Key(从 unsplash.com/developers 获取,用于获取高质量背景图)"),
|
|
1308
|
+
// Travel log retention
|
|
1309
|
+
logRetentionDays: import_koishi.Schema.number().default(45).description("旅行日志保留天数(默认45天,约1.5个月)"),
|
|
1310
|
+
// Debug
|
|
1311
|
+
debug: import_koishi.Schema.boolean().default(false).description("启用调试模式(输出详细日志)")
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
// src/index.ts
|
|
1315
|
+
var name = "my-pig-group-friends";
|
|
1316
|
+
var inject = {
|
|
1317
|
+
required: ["database", "cron", "puppeteer"],
|
|
1318
|
+
optional: ["mediaLuna", "chatluna_storage", "chatluna"]
|
|
1319
|
+
};
|
|
1320
|
+
function formatTravelMessage(result, userId, config) {
|
|
1321
|
+
if (config.outputMode === "text") {
|
|
1322
|
+
return `${import_koishi2.segment.at(userId)} ${result.msg}`;
|
|
1323
|
+
}
|
|
1324
|
+
if (result.imageUrl) {
|
|
1325
|
+
return `${import_koishi2.segment.at(userId)} ${result.msg}
|
|
1326
|
+
${import_koishi2.segment.image(result.imageUrl)}`;
|
|
1327
|
+
}
|
|
1328
|
+
if (result.imageBuffer) {
|
|
1329
|
+
const base64 = result.imageBuffer.toString("base64");
|
|
1330
|
+
return `${import_koishi2.segment.at(userId)} ${result.msg}
|
|
1331
|
+
${import_koishi2.segment.image(`base64://${base64}`)}`;
|
|
1332
|
+
}
|
|
1333
|
+
return `${import_koishi2.segment.at(userId)} ${result.msg}`;
|
|
1334
|
+
}
|
|
1335
|
+
__name(formatTravelMessage, "formatTravelMessage");
|
|
1336
|
+
function apply(ctx, config) {
|
|
1337
|
+
ctx.logger("pig").info("my-pig-group-friends plugin is loading...");
|
|
1338
|
+
applyDatabase(ctx);
|
|
1339
|
+
if (config.useStorageService && !ctx.chatluna_storage) {
|
|
1340
|
+
ctx.logger("pig").warn("useStorageService 已启用但 chatluna_storage 服务不可用,将回退到 base64 模式");
|
|
1341
|
+
}
|
|
1342
|
+
ctx.command("pig <user:user>", "虚拟旅行").action(async ({ session }, user) => {
|
|
1343
|
+
if (!user) return "请指定一个用户";
|
|
1344
|
+
const [platform, userId] = user.split(":");
|
|
1345
|
+
let userInfo;
|
|
1346
|
+
if (session?.userId === userId) {
|
|
1347
|
+
userInfo = {
|
|
1348
|
+
userId,
|
|
1349
|
+
username: session.author?.nickname || session.author?.name || session.username || userId,
|
|
1350
|
+
avatarUrl: session.author?.avatar || ""
|
|
1351
|
+
};
|
|
1352
|
+
} else {
|
|
1353
|
+
userInfo = {
|
|
1354
|
+
userId,
|
|
1355
|
+
username: userId,
|
|
1356
|
+
avatarUrl: ""
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
const result = await triggerTravelSequence(ctx, config, userInfo, platform);
|
|
1360
|
+
return formatTravelMessage(result, userId, config);
|
|
1361
|
+
});
|
|
1362
|
+
ctx.middleware(async (session, next) => {
|
|
1363
|
+
if (!session.userId || !session.content) return next();
|
|
1364
|
+
const now = /* @__PURE__ */ new Date();
|
|
1365
|
+
const [userState] = await ctx.database.get("pig_user_state", {
|
|
1366
|
+
userId: session.userId,
|
|
1367
|
+
platform: session.platform
|
|
1368
|
+
});
|
|
1369
|
+
const lat = userState?.latitude ?? config.defaultLat;
|
|
1370
|
+
const lng = userState?.longitude ?? config.defaultLng;
|
|
1371
|
+
try {
|
|
1372
|
+
const sunriseInfo = await getSunriseInfo(ctx, lat, lng);
|
|
1373
|
+
const dayStart = new Date(sunriseInfo.sunrise.getTime() - 2 * 60 * 60 * 1e3);
|
|
1374
|
+
if (now >= dayStart && (!userState?.lastWakeUp || userState.lastWakeUp < dayStart)) {
|
|
1375
|
+
if (userState?.lastWakeUp) {
|
|
1376
|
+
const diffHours = Math.abs((now.getTime() - userState.lastWakeUp.getTime() - 24 * 60 * 60 * 1e3) / (1e3 * 60 * 60));
|
|
1377
|
+
if (diffHours > config.abnormalThreshold) {
|
|
1378
|
+
await session.send(`检测到 ${import_koishi2.segment.at(session.userId)} 作息异常(差异: ${diffHours.toFixed(1)}小时),准备虚拟旅行...`);
|
|
1379
|
+
const userInfo = {
|
|
1380
|
+
userId: session.userId,
|
|
1381
|
+
username: session.author?.nickname || session.author?.name || session.username || session.userId,
|
|
1382
|
+
avatarUrl: session.author?.avatar || ""
|
|
1383
|
+
};
|
|
1384
|
+
const result = await triggerTravelSequence(ctx, config, userInfo, session.platform);
|
|
1385
|
+
await session.send(formatTravelMessage(result, session.userId, config));
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
await ctx.database.upsert("pig_user_state", [{
|
|
1389
|
+
userId: session.userId,
|
|
1390
|
+
platform: session.platform,
|
|
1391
|
+
lastWakeUp: now,
|
|
1392
|
+
lastSunrise: sunriseInfo.sunrise
|
|
1393
|
+
}]);
|
|
1394
|
+
}
|
|
1395
|
+
} catch (e) {
|
|
1396
|
+
ctx.logger("pig").error("Failed to process wake-up detection:", e);
|
|
1397
|
+
}
|
|
1398
|
+
return next();
|
|
1399
|
+
});
|
|
1400
|
+
ctx.cron("0 0 1 * *", async () => {
|
|
1401
|
+
const now = /* @__PURE__ */ new Date();
|
|
1402
|
+
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
1403
|
+
ctx.logger("pig").info("Generating monthly travel handbook...");
|
|
1404
|
+
});
|
|
1405
|
+
ctx.cron("0 3 * * *", async () => {
|
|
1406
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
1407
|
+
cutoffDate.setDate(cutoffDate.getDate() - config.logRetentionDays);
|
|
1408
|
+
ctx.logger("pig").info(`Cleaning up travel logs older than ${config.logRetentionDays} days (before ${cutoffDate.toISOString()})...`);
|
|
1409
|
+
try {
|
|
1410
|
+
const result = await ctx.database.remove("pig_travel_log", {
|
|
1411
|
+
timestamp: { $lt: cutoffDate }
|
|
1412
|
+
});
|
|
1413
|
+
ctx.logger("pig").info(`Cleaned up old travel logs`);
|
|
1414
|
+
} catch (e) {
|
|
1415
|
+
ctx.logger("pig").error("Failed to cleanup old travel logs:", e);
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
__name(apply, "apply");
|
|
1420
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1421
|
+
0 && (module.exports = {
|
|
1422
|
+
Config,
|
|
1423
|
+
apply,
|
|
1424
|
+
inject,
|
|
1425
|
+
name
|
|
1426
|
+
});
|