koishi-plugin-my-pig-group-friends 1.1.1 → 1.1.4

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/index.js CHANGED
@@ -45,7 +45,10 @@ function applyDatabase(ctx) {
45
45
  platform: "string",
46
46
  timestamp: "timestamp",
47
47
  country: "string",
48
+ countryZh: "string",
48
49
  location: "string",
50
+ locationZh: "string",
51
+ timezone: "string",
49
52
  imagePath: "string",
50
53
  isAIGC: "boolean"
51
54
  }, { primary: "id", autoInc: true });
@@ -101,6 +104,13 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
101
104
  return "image/jpeg";
102
105
  }, "sniffMime");
103
106
  const inlineMaxBytes = 900 * 1024;
107
+ const fetchTimeoutMs = config.backgroundFetchTimeoutMs ?? 8e3;
108
+ const shouldServerFetch = /* @__PURE__ */ __name((url) => {
109
+ const mode = config.backgroundFetchMode || "auto";
110
+ if (mode === "never") return false;
111
+ if (mode === "always") return true;
112
+ return !/^https?:\/\/(images|source)\.unsplash\.com\//i.test(url);
113
+ }, "shouldServerFetch");
104
114
  const fetchToDataUrl = /* @__PURE__ */ __name(async (url) => {
105
115
  const normalized = normalizeImageUrl(url);
106
116
  if (config.debug) ctx.logger("pig").debug(`Normalized background URL: ${normalized}`);
@@ -122,20 +132,25 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
122
132
  }
123
133
  }
124
134
  if (/^https?:\/\//i.test(normalized)) {
135
+ if (!shouldServerFetch(normalized)) {
136
+ if (config.debug) {
137
+ ctx.logger("pig").debug(`Skip server-side fetch for background: ${normalized}`);
138
+ }
139
+ return normalized;
140
+ }
125
141
  try {
126
142
  if (config.debug) ctx.logger("pig").debug(`Server-side fetching background: ${normalized}`);
127
143
  const response = await ctx.http(normalized, {
128
144
  responseType: "arraybuffer",
129
- timeout: 8e3,
130
- // Reduced from 15s to 8s for better responsiveness
145
+ timeout: fetchTimeoutMs,
131
146
  headers: {
132
147
  "User-Agent": "Mozilla/5.0",
133
148
  "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"
134
149
  },
135
150
  redirect: "follow"
136
151
  });
137
- const contentType = response.headers.get("content-type") || "";
138
- const contentLength = response.headers.get("content-length") || "unknown";
152
+ const contentType = response.headers?.["content-type"] || "";
153
+ const contentLength = response.headers?.["content-length"] || "unknown";
139
154
  if (config.debug) {
140
155
  ctx.logger("pig").debug(
141
156
  `Background fetch response: status=${response.status} url=${response.url} content-type=${contentType || "unknown"} content-length=${contentLength}`
@@ -177,7 +192,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
177
192
  }
178
193
  const now = /* @__PURE__ */ new Date();
179
194
  const dateStr = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`;
180
- const emojiFont = config.emojiFont === "System" ? "" : `"${config.emojiFont}", `;
195
+ const bgCssValue = bgImage ? `url("${bgImage}")` : "none";
181
196
  const html = `
182
197
  <!DOCTYPE html>
183
198
  <html>
@@ -195,12 +210,18 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
195
210
  width: 1080px;
196
211
  height: 1920px;
197
212
  overflow: hidden;
198
- /* Prioritize system fonts with wide Unicode coverage for emoji and CJK support */
199
- font-family: ${emojiFont}"Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei", "WenQuanYi Micro Hei", "Droid Sans Fallback", "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
213
+ /* Default emoji font set to Noto Color Emoji to maintain layout consistency */
214
+ font-family: "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei", "WenQuanYi Micro Hei", "Droid Sans Fallback", "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif, "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji";
200
215
  background: #f0f0f2;
201
- --bg-image: url('${bgImage}');
216
+ --bg-image: ${bgCssValue};
202
217
  }
203
218
 
219
+ /* Specific class for the pig emoji to use Twemoji */
220
+ .twemoji {
221
+ font-family: "Twemoji", "Noto Color Emoji", sans-serif;
222
+ }
223
+
224
+
204
225
  .wrapper {
205
226
  position: relative;
206
227
  width: 100%;
@@ -215,10 +236,9 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
215
236
  inset: 0;
216
237
  width: 100%;
217
238
  height: 100%;
239
+ display: block;
240
+ object-fit: cover;
218
241
  background-color: #d1d1d6;
219
- background-image: var(--bg-image);
220
- background-size: cover;
221
- background-position: center;
222
242
  z-index: 0;
223
243
  }
224
244
 
@@ -376,7 +396,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
376
396
  .message-text {
377
397
  font-size: 56px;
378
398
  line-height: 1.25;
379
- font-weight: 800;
399
+ font-weight: 900;
380
400
  color: #1d1d1f;
381
401
  letter-spacing: -0.03em;
382
402
  word-break: break-word;
@@ -390,8 +410,11 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
390
410
  box-decoration-break: clone;
391
411
  -webkit-box-decoration-break: clone;
392
412
  display: inline-block;
413
+ font-weight: 900;
414
+ letter-spacing: -0.01em;
393
415
  }
394
416
 
417
+
395
418
  .divider {
396
419
  height: 2px;
397
420
  background: rgba(0,0,0,0.08);
@@ -424,14 +447,14 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
424
447
 
425
448
  .location-pill span {
426
449
  font-size: 26px;
427
- font-weight: 700;
450
+ font-weight: 800;
428
451
  color: #ffffff;
429
452
  letter-spacing: 0.02em;
430
453
  }
431
454
 
432
455
  .landmark-name {
433
456
  font-size: 38px;
434
- font-weight: 800;
457
+ font-weight: 900;
435
458
  color: #1d1d1f;
436
459
  letter-spacing: -0.01em;
437
460
  padding-left: 6px;
@@ -463,7 +486,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
463
486
  </head>
464
487
  <body>
465
488
  <div class="wrapper">
466
- <div class="bg-image"></div>
489
+ <img class="bg-image" src="${bgImage || ""}" alt="" />
467
490
  <div class="bg-overlay"></div>
468
491
 
469
492
  <div class="card-container">
@@ -482,7 +505,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
482
505
 
483
506
  <div class="message-body">
484
507
  <div class="message-text">
485
- 今天 🐷猪醒在<br/>
508
+ 今天 <span class="twemoji">🐷</span>猪醒在<br/>
486
509
  <span class="highlight">${data.location.landmarkZh || data.location.landmark}</span>
487
510
  </div>
488
511
  </div>
@@ -498,7 +521,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
498
521
  </div>
499
522
 
500
523
  <div class="brand-tag">
501
- <div class="brand-icon">🐷</div>
524
+ <div class="brand-icon"><span class="twemoji">🐷</span></div>
502
525
  <div class="brand-name">Pig Travel</div>
503
526
  </div>
504
527
  </div>
@@ -1037,7 +1060,8 @@ async function searchUnsplashPhoto(ctx, accessKey, query, debug = false) {
1037
1060
  {
1038
1061
  params: {
1039
1062
  query,
1040
- per_page: 1,
1063
+ per_page: 10,
1064
+ // Fetch more results for variety
1041
1065
  orientation: "landscape"
1042
1066
  },
1043
1067
  headers: {
@@ -1048,13 +1072,15 @@ async function searchUnsplashPhoto(ctx, accessKey, query, debug = false) {
1048
1072
  }
1049
1073
  );
1050
1074
  if (response.results && response.results.length > 0) {
1051
- let photoUrl = response.results[0].urls.regular;
1075
+ const randomIndex = Math.floor(Math.random() * response.results.length);
1076
+ const selectedPhoto = response.results[randomIndex];
1077
+ let photoUrl = selectedPhoto.urls.regular;
1052
1078
  if (photoUrl.includes("?")) {
1053
1079
  photoUrl += "&fm=webp&q=85";
1054
1080
  } else {
1055
1081
  photoUrl += "?fm=webp&q=85";
1056
1082
  }
1057
- if (debug) ctx.logger("pig").debug(`Unsplash: Found photo URL: ${photoUrl}`);
1083
+ if (debug) ctx.logger("pig").debug(`Unsplash: Selected photo ${randomIndex + 1}/${response.results.length}, URL: ${photoUrl}`);
1058
1084
  return photoUrl;
1059
1085
  }
1060
1086
  if (debug) ctx.logger("pig").debug("Unsplash: No photos found for query");
@@ -1066,6 +1092,53 @@ async function searchUnsplashPhoto(ctx, accessKey, query, debug = false) {
1066
1092
  }
1067
1093
  __name(searchUnsplashPhoto, "searchUnsplashPhoto");
1068
1094
 
1095
+ // src/services/pexels.ts
1096
+ var PEXELS_API_BASE = "https://api.pexels.com/v1";
1097
+ async function searchPexelsPhoto(ctx, apiKey, query, debug = false) {
1098
+ if (!apiKey) {
1099
+ if (debug) ctx.logger("pig").debug("Pexels: No API key provided");
1100
+ return null;
1101
+ }
1102
+ try {
1103
+ if (debug) ctx.logger("pig").debug(`Pexels: Searching for "${query}"`);
1104
+ const response = await ctx.http.get(
1105
+ `${PEXELS_API_BASE}/search`,
1106
+ {
1107
+ params: {
1108
+ query,
1109
+ per_page: 10,
1110
+ // Fetch more results for variety
1111
+ orientation: "landscape"
1112
+ },
1113
+ headers: {
1114
+ Authorization: apiKey
1115
+ },
1116
+ timeout: 1e4
1117
+ }
1118
+ );
1119
+ if (response.photos && response.photos.length > 0) {
1120
+ const randomIndex = Math.floor(Math.random() * response.photos.length);
1121
+ const selectedPhoto = response.photos[randomIndex];
1122
+ let photoUrl = selectedPhoto.src.large2x || selectedPhoto.src.large;
1123
+ if (photoUrl.includes("w=")) {
1124
+ photoUrl = photoUrl.replace(/w=\d+/, "w=1080");
1125
+ } else if (photoUrl.includes("?")) {
1126
+ photoUrl += "&w=1080";
1127
+ } else {
1128
+ photoUrl += "?w=1080";
1129
+ }
1130
+ if (debug) ctx.logger("pig").debug(`Pexels: Selected photo ${randomIndex + 1}/${response.photos.length}, URL: ${photoUrl}`);
1131
+ return photoUrl;
1132
+ }
1133
+ if (debug) ctx.logger("pig").debug("Pexels: No photos found for query");
1134
+ return null;
1135
+ } catch (e) {
1136
+ ctx.logger("pig").warn(`Pexels API error: ${e}`);
1137
+ return null;
1138
+ }
1139
+ }
1140
+ __name(searchPexelsPhoto, "searchPexelsPhoto");
1141
+
1069
1142
  // src/services/location.ts
1070
1143
  var LOCATION_CATEGORIES = [
1071
1144
  "自然奇观(峡谷、瀑布、火山、冰川、沙漠绿洲等)",
@@ -1080,13 +1153,43 @@ var LOCATION_CATEGORIES = [
1080
1153
  "神秘与奇特之地(地质奇观、UFO小镇、怪异地貌等)"
1081
1154
  ];
1082
1155
  var CONTINENTS = ["亚洲", "欧洲", "非洲", "北美洲", "南美洲", "大洋洲", "南极洲"];
1156
+ function getSunriseTimezoneHint() {
1157
+ const now = /* @__PURE__ */ new Date();
1158
+ const utcHour = now.getUTCHours();
1159
+ const targetLocalHour = 6;
1160
+ const idealOffset = targetLocalHour - utcHour;
1161
+ const normalizeOffset = /* @__PURE__ */ __name((offset) => {
1162
+ if (offset < -12) return offset + 24;
1163
+ if (offset > 14) return offset - 24;
1164
+ return offset;
1165
+ }, "normalizeOffset");
1166
+ const minOffset = normalizeOffset(idealOffset - 1);
1167
+ const maxOffset = normalizeOffset(idealOffset + 1);
1168
+ const getRegionByOffset = /* @__PURE__ */ __name((offset) => {
1169
+ if (offset >= 9 && offset <= 12) return "东亚、澳大利亚东部、太平洋岛屿";
1170
+ if (offset >= 5 && offset <= 8) return "南亚、东南亚、中亚";
1171
+ if (offset >= 2 && offset <= 4) return "中东、东欧、东非";
1172
+ if (offset >= -1 && offset <= 1) return "西欧、西非";
1173
+ if (offset >= -5 && offset <= -2) return "南美洲东部、大西洋";
1174
+ if (offset >= -8 && offset <= -6) return "北美洲西部、太平洋东部";
1175
+ if (offset >= -12 && offset <= -9) return "太平洋中部、夏威夷、阿拉斯加";
1176
+ return "全球各地";
1177
+ }, "getRegionByOffset");
1178
+ const formatOffset = /* @__PURE__ */ __name((offset) => offset >= 0 ? `UTC+${offset}` : `UTC${offset}`, "formatOffset");
1179
+ return {
1180
+ utcOffsetRange: `${formatOffset(minOffset)} 到 ${formatOffset(maxOffset)}`,
1181
+ regionHint: getRegionByOffset(idealOffset)
1182
+ };
1183
+ }
1184
+ __name(getSunriseTimezoneHint, "getSunriseTimezoneHint");
1083
1185
  function getRandomPromptHints() {
1084
1186
  const category = LOCATION_CATEGORIES[Math.floor(Math.random() * LOCATION_CATEGORIES.length)];
1085
1187
  const continent = CONTINENTS[Math.floor(Math.random() * CONTINENTS.length)];
1086
1188
  const hotCountries = ["法国", "日本", "意大利", "美国", "英国", "中国", "西班牙", "泰国", "澳大利亚"];
1087
1189
  const shuffled = hotCountries.sort(() => Math.random() - 0.5);
1088
1190
  const avoidCountries = shuffled.slice(0, 3 + Math.floor(Math.random() * 4)).join("、");
1089
- return { category, continent, avoidCountries };
1191
+ const sunriseHint = getSunriseTimezoneHint();
1192
+ return { category, continent, avoidCountries, sunriseHint };
1090
1193
  }
1091
1194
  __name(getRandomPromptHints, "getRandomPromptHints");
1092
1195
  var LOCATION_GENERATION_PROMPT = `你是一位资深旅行探险家,专门发掘世界各地的独特目的地。
@@ -1121,12 +1224,23 @@ async function generateLocationWithLLM(ctx, config) {
1121
1224
  return getRandomStaticLocation();
1122
1225
  }
1123
1226
  const hints = getRandomPromptHints();
1124
- const userPrompt = `请生成一个位于【${hints.continent}】的【${hints.category}】类型的旅游目的地。
1227
+ let userPrompt = `请生成一个【${hints.category}】类型的旅游目的地。
1228
+
1229
+ 🌅 时区要求(重要):当前 UTC 时间是 ${(/* @__PURE__ */ new Date()).getUTCHours()}:${String((/* @__PURE__ */ new Date()).getUTCMinutes()).padStart(2, "0")},请选择一个正处于日出时段(当地时间约 5:00-7:00)的地点。
1230
+ 符合条件的时区范围大约是 ${hints.sunriseHint.utcOffsetRange},对应地区包括:${hints.sunriseHint.regionHint}。
1125
1231
 
1126
- 特别要求:这次请避开 ${hints.avoidCountries} 这些热门国家,选择一个更独特、更少人知道的地方。
1232
+ 如果上述地区没有合适的【${hints.category}】类型目的地,可以适当放宽到邻近时区,但优先选择正在迎接日出的地方。
1233
+
1234
+ 特别要求:请避开 ${hints.avoidCountries} 这些热门国家,选择一个更独特、更少人知道的地方。要求这个地方在地理位置或文化上具有独特性。`;
1235
+ if (config.llmLocationCustomContext) {
1236
+ userPrompt += `
1237
+
1238
+ 此外,请参考以下用户提供的偏好或上下文:${config.llmLocationCustomContext}`;
1239
+ }
1240
+ userPrompt += `
1127
1241
 
1128
1242
  直接输出JSON,不要有任何其他文字。`;
1129
- if (config.debug) ctx.logger("pig").debug(`Location prompt hints: ${hints.continent}, ${hints.category}`);
1243
+ if (config.debug) ctx.logger("pig").debug(`Location prompt hints: sunrise=${hints.sunriseHint.regionHint}, category=${hints.category}`);
1130
1244
  const messages = [
1131
1245
  new import_messages.SystemMessage(LOCATION_GENERATION_PROMPT),
1132
1246
  new import_messages.HumanMessage(userPrompt)
@@ -1137,29 +1251,37 @@ async function generateLocationWithLLM(ctx, config) {
1137
1251
  const location = parseLocationResponse(content);
1138
1252
  if (location) {
1139
1253
  ctx.logger("pig").info(`LLM generated location: ${location.landmarkZh} (${location.landmark}), ${location.countryZh}`);
1140
- if (config.unsplashAccessKey) {
1254
+ if (config.unsplashAccessKey || config.pexelsApiKey) {
1255
+ const template = config.imageSearchPrompt || "{landmark} {country} landscape";
1256
+ const formatQuery = /* @__PURE__ */ __name((tmpl) => tmpl.replace("{landmark}", location.landmark).replace("{country}", location.country).replace("{city}", location.city || "").trim(), "formatQuery");
1141
1257
  const searchQueries = [
1258
+ formatQuery(template),
1259
+ // Primary: User template
1142
1260
  `${location.landmark} ${location.country}`,
1143
- // Most specific: landmark + country
1261
+ // Fallback 1: specific
1144
1262
  location.city ? `${location.city} ${location.country}` : null,
1145
- // City + country
1146
- `${location.country} landscape`,
1147
- // Country landscape
1263
+ // Fallback 2: city
1148
1264
  location.country
1149
- // Just country name
1265
+ // Fallback 3: country
1150
1266
  ].filter(Boolean);
1151
1267
  let photoUrl = null;
1152
1268
  for (const query of searchQueries) {
1153
- if (config.debug) ctx.logger("pig").debug(`Searching Unsplash for: ${query}`);
1154
- photoUrl = await searchUnsplashPhoto(ctx, config.unsplashAccessKey, query, config.debug);
1269
+ if (config.unsplashAccessKey) {
1270
+ if (config.debug) ctx.logger("pig").debug(`Searching Unsplash for: ${query}`);
1271
+ photoUrl = await searchUnsplashPhoto(ctx, config.unsplashAccessKey, query, config.debug);
1272
+ }
1273
+ if (!photoUrl && config.pexelsApiKey) {
1274
+ if (config.debug) ctx.logger("pig").debug(`Searching Pexels for: ${query}`);
1275
+ photoUrl = await searchPexelsPhoto(ctx, config.pexelsApiKey, query, config.debug);
1276
+ }
1155
1277
  if (photoUrl) {
1156
- if (config.debug) ctx.logger("pig").info(`Using Unsplash photo: ${photoUrl}`);
1278
+ ctx.logger("pig").info(`Found photo URL: ${photoUrl}`);
1157
1279
  location.landscapeUrl = photoUrl;
1158
1280
  break;
1159
1281
  }
1160
1282
  }
1161
1283
  if (!photoUrl && config.debug) {
1162
- ctx.logger("pig").debug("All Unsplash searches returned no results, using LLM-generated URL");
1284
+ ctx.logger("pig").debug("All image searches returned no results, using LLM-generated URL fallback");
1163
1285
  }
1164
1286
  }
1165
1287
  return location;
@@ -1190,7 +1312,8 @@ function parseLocationResponse(content) {
1190
1312
  landscapeUrl: data.landscapeUrl
1191
1313
  };
1192
1314
  if (!location.landscapeUrl.startsWith("http")) {
1193
- location.landscapeUrl = `https://images.unsplash.com/featured/?${encodeURIComponent(location.landmark)},${encodeURIComponent(location.country)}`;
1315
+ const query = `${encodeURIComponent(location.landmark)},${encodeURIComponent(location.country)}`;
1316
+ location.landscapeUrl = `https://images.unsplash.com/featured/?${query}`;
1194
1317
  }
1195
1318
  return location;
1196
1319
  } catch (e) {
@@ -1283,7 +1406,10 @@ async function triggerTravelSequence(ctx, config, userInfo, platform) {
1283
1406
  platform,
1284
1407
  timestamp: now,
1285
1408
  country: location.country,
1409
+ countryZh: location.countryZh || location.country,
1286
1410
  location: location.landmark,
1411
+ locationZh: location.landmarkZh || location.landmark,
1412
+ timezone: location.timezone || "UTC",
1287
1413
  imagePath: imageUrl || "",
1288
1414
  // 存储 URL 或空
1289
1415
  isAIGC
@@ -1298,22 +1424,650 @@ async function triggerTravelSequence(ctx, config, userInfo, platform) {
1298
1424
  }
1299
1425
  __name(triggerTravelSequence, "triggerTravelSequence");
1300
1426
 
1427
+ // src/services/summary.ts
1428
+ async function getMonthlyLogs(ctx, userId, platform, year, month) {
1429
+ const startDate = new Date(year, month - 1, 1);
1430
+ const endDate = new Date(year, month, 1);
1431
+ const logs = await ctx.database.get("pig_travel_log", {
1432
+ userId,
1433
+ platform,
1434
+ timestamp: {
1435
+ $gte: startDate,
1436
+ $lt: endDate
1437
+ }
1438
+ });
1439
+ logs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1440
+ return logs;
1441
+ }
1442
+ __name(getMonthlyLogs, "getMonthlyLogs");
1443
+ async function getUsersWithLogsInMonth(ctx, year, month) {
1444
+ const startDate = new Date(year, month - 1, 1);
1445
+ const endDate = new Date(year, month, 1);
1446
+ const logs = await ctx.database.get("pig_travel_log", {
1447
+ timestamp: {
1448
+ $gte: startDate,
1449
+ $lt: endDate
1450
+ }
1451
+ });
1452
+ const userMap = /* @__PURE__ */ new Map();
1453
+ for (const log of logs) {
1454
+ const key = `${log.platform}:${log.userId}`;
1455
+ if (!userMap.has(key)) {
1456
+ userMap.set(key, { userId: log.userId, platform: log.platform });
1457
+ }
1458
+ }
1459
+ return Array.from(userMap.values());
1460
+ }
1461
+ __name(getUsersWithLogsInMonth, "getUsersWithLogsInMonth");
1462
+ function formatTimezone(timezone) {
1463
+ if (!timezone || timezone === "UTC") return "UTC";
1464
+ try {
1465
+ const now = /* @__PURE__ */ new Date();
1466
+ const formatter = new Intl.DateTimeFormat("en-US", {
1467
+ timeZone: timezone,
1468
+ timeZoneName: "shortOffset"
1469
+ });
1470
+ const parts = formatter.formatToParts(now);
1471
+ const tzPart = parts.find((p) => p.type === "timeZoneName");
1472
+ if (tzPart) {
1473
+ return tzPart.value.replace("GMT", "UTC");
1474
+ }
1475
+ } catch {
1476
+ }
1477
+ if (timezone.startsWith("UTC")) return timezone;
1478
+ return "UTC";
1479
+ }
1480
+ __name(formatTimezone, "formatTimezone");
1481
+ async function generateMonthlySummaryCard(ctx, config, data) {
1482
+ const { year, month, logs, username, totalTrips, countriesVisited, locationsVisited } = data;
1483
+ let { avatarUrl } = data;
1484
+ if (avatarUrl && avatarUrl.startsWith("http")) {
1485
+ try {
1486
+ if (config.debug) ctx.logger("pig").debug(`Fetching avatar: ${avatarUrl}`);
1487
+ const response = await ctx.http(avatarUrl, {
1488
+ responseType: "arraybuffer",
1489
+ timeout: 5e3,
1490
+ headers: {
1491
+ "User-Agent": "Mozilla/5.0",
1492
+ "Accept": "image/*"
1493
+ }
1494
+ });
1495
+ const buffer = Buffer.from(response.data);
1496
+ const contentType = response.headers?.["content-type"] || "image/jpeg";
1497
+ avatarUrl = `data:${contentType};base64,${buffer.toString("base64")}`;
1498
+ if (config.debug) ctx.logger("pig").debug(`Avatar fetched successfully, size: ${buffer.length}`);
1499
+ } catch (e) {
1500
+ if (config.debug) ctx.logger("pig").warn(`Failed to fetch avatar: ${e}`);
1501
+ avatarUrl = "";
1502
+ }
1503
+ }
1504
+ const monthNames = ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"];
1505
+ const monthName = monthNames[month - 1];
1506
+ const tripsHtml = logs.slice(0, 12).map((log, index) => {
1507
+ const date = new Date(log.timestamp);
1508
+ const dayStr = `${date.getMonth() + 1}/${date.getDate()}`;
1509
+ const tz = formatTimezone(log.timezone);
1510
+ return `
1511
+ <div class="trip-item">
1512
+ <div class="trip-index">${index + 1}</div>
1513
+ <div class="trip-info">
1514
+ <div class="trip-location">${escapeHtml2(log.locationZh || log.location)}</div>
1515
+ <div class="trip-country">${escapeHtml2(log.countryZh || log.country)} · ${dayStr}</div>
1516
+ <div class="trip-location-en">${escapeHtml2(log.location)}</div>
1517
+ </div>
1518
+ <div class="trip-tz">${tz}</div>
1519
+ </div>
1520
+ `;
1521
+ }).join("");
1522
+ const moreTripsHtml = logs.length > 12 ? `<div class="more-trips">... 还有 ${logs.length - 12} 次旅行</div>` : "";
1523
+ const statsHtml = `
1524
+ <div class="stats-grid">
1525
+ <div class="stat-item bg-yellow">
1526
+ <div class="stat-value">${totalTrips}</div>
1527
+ <div class="stat-label">次旅行</div>
1528
+ <div class="deco-dot"></div>
1529
+ </div>
1530
+ <div class="stat-item bg-pink">
1531
+ <div class="stat-value">${countriesVisited.length}</div>
1532
+ <div class="stat-label">个国家</div>
1533
+ <div class="deco-line"></div>
1534
+ </div>
1535
+ <div class="stat-item bg-cyan">
1536
+ <div class="stat-value">${locationsVisited.length}</div>
1537
+ <div class="stat-label">个地点</div>
1538
+ <div class="deco-triangle"></div>
1539
+ </div>
1540
+ </div>
1541
+ `;
1542
+ const html = `
1543
+ <!DOCTYPE html>
1544
+ <html>
1545
+ <head>
1546
+ <meta charset="UTF-8">
1547
+ <style>
1548
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700;900&display=swap');
1549
+
1550
+ * {
1551
+ margin: 0;
1552
+ padding: 0;
1553
+ box-sizing: border-box;
1554
+ }
1555
+
1556
+ body {
1557
+ width: 1080px;
1558
+ min-height: 1920px;
1559
+ font-family: "Noto Sans SC", sans-serif, "Noto Color Emoji";
1560
+ background-color: #F0F0F0;
1561
+ background-image:
1562
+ radial-gradient(#000 10%, transparent 11%),
1563
+ radial-gradient(#000 10%, transparent 11%);
1564
+ background-size: 30px 30px;
1565
+ background-position: 0 0, 15px 15px;
1566
+ background-color: #FFDEE9;
1567
+ padding: 60px;
1568
+ }
1569
+
1570
+ .twemoji {
1571
+ font-family: "Twemoji", "Noto Color Emoji", sans-serif;
1572
+ }
1573
+
1574
+ .container {
1575
+ background: #fff;
1576
+ border: 4px solid #000;
1577
+ box-shadow: 20px 20px 0 #000;
1578
+ padding: 60px;
1579
+ position: relative;
1580
+ overflow: hidden;
1581
+ }
1582
+
1583
+ /* Memphis decorative elements */
1584
+ .deco-shape-1 {
1585
+ position: absolute;
1586
+ top: -20px;
1587
+ right: -20px;
1588
+ width: 150px;
1589
+ height: 150px;
1590
+ background: #FFD700;
1591
+ border: 4px solid #000;
1592
+ border-radius: 50%;
1593
+ z-index: 0;
1594
+ }
1595
+
1596
+ .deco-shape-2 {
1597
+ position: absolute;
1598
+ bottom: 40px;
1599
+ left: -30px;
1600
+ width: 100px;
1601
+ height: 100px;
1602
+ background: #00CED1;
1603
+ border: 4px solid #000;
1604
+ transform: rotate(45deg);
1605
+ z-index: 0;
1606
+ }
1607
+
1608
+ .header {
1609
+ display: flex;
1610
+ align-items: center;
1611
+ gap: 32px;
1612
+ margin-bottom: 48px;
1613
+ position: relative;
1614
+ z-index: 1;
1615
+ background: #fff;
1616
+ border: 4px solid #000;
1617
+ padding: 24px;
1618
+ box-shadow: 8px 8px 0 #000;
1619
+ }
1620
+
1621
+ .avatar-ring {
1622
+ padding: 0;
1623
+ border: 4px solid #000;
1624
+ border-radius: 50%;
1625
+ overflow: hidden;
1626
+ background: #FF69B4;
1627
+ }
1628
+
1629
+ .avatar {
1630
+ width: 120px;
1631
+ height: 120px;
1632
+ object-fit: cover;
1633
+ display: block;
1634
+ }
1635
+
1636
+ .user-info {
1637
+ flex: 1;
1638
+ }
1639
+
1640
+ .username {
1641
+ font-size: 48px;
1642
+ font-weight: 900;
1643
+ color: #000;
1644
+ margin-bottom: 8px;
1645
+ text-transform: uppercase;
1646
+ letter-spacing: -1px;
1647
+ }
1648
+
1649
+ .period {
1650
+ font-size: 28px;
1651
+ color: #000;
1652
+ font-weight: 700;
1653
+ background: #00CED1;
1654
+ display: inline-block;
1655
+ padding: 4px 12px;
1656
+ border: 3px solid #000;
1657
+ box-shadow: 4px 4px 0 #000;
1658
+ }
1659
+
1660
+ .title-section {
1661
+ text-align: center;
1662
+ margin-bottom: 48px;
1663
+ position: relative;
1664
+ z-index: 1;
1665
+ border-bottom: 4px solid #000;
1666
+ padding-bottom: 24px;
1667
+ }
1668
+
1669
+ .title {
1670
+ font-size: 80px;
1671
+ font-weight: 900;
1672
+ color: #000;
1673
+ margin-bottom: 16px;
1674
+ text-shadow: 6px 6px 0 #FF69B4;
1675
+ letter-spacing: 4px;
1676
+ }
1677
+
1678
+ .subtitle {
1679
+ font-size: 32px;
1680
+ color: #000;
1681
+ font-weight: 700;
1682
+ background: #FFD700;
1683
+ display: inline-block;
1684
+ padding: 8px 24px;
1685
+ border: 3px solid #000;
1686
+ box-shadow: 6px 6px 0 #000;
1687
+ transform: rotate(-2deg);
1688
+ }
1689
+
1690
+ .stats-grid {
1691
+ display: flex;
1692
+ justify-content: space-between;
1693
+ margin-bottom: 48px;
1694
+ gap: 24px;
1695
+ position: relative;
1696
+ z-index: 1;
1697
+ }
1698
+
1699
+ .stat-item {
1700
+ text-align: center;
1701
+ flex: 1;
1702
+ padding: 32px 16px;
1703
+ border: 4px solid #000;
1704
+ box-shadow: 12px 12px 0 #000;
1705
+ position: relative;
1706
+ overflow: hidden;
1707
+ }
1708
+
1709
+ .bg-yellow { background: #FFD700; }
1710
+ .bg-pink { background: #FF69B4; }
1711
+ .bg-cyan { background: #00CED1; }
1712
+
1713
+ .stat-value {
1714
+ font-size: 72px;
1715
+ font-weight: 900;
1716
+ color: #000;
1717
+ line-height: 1;
1718
+ position: relative;
1719
+ z-index: 2;
1720
+ }
1721
+
1722
+ .stat-label {
1723
+ font-size: 24px;
1724
+ color: #000;
1725
+ margin-top: 8px;
1726
+ font-weight: 700;
1727
+ text-transform: uppercase;
1728
+ position: relative;
1729
+ z-index: 2;
1730
+ }
1731
+
1732
+ .trips-section {
1733
+ margin-bottom: 32px;
1734
+ position: relative;
1735
+ z-index: 1;
1736
+ }
1737
+
1738
+ .section-title {
1739
+ font-size: 36px;
1740
+ font-weight: 900;
1741
+ color: #000;
1742
+ margin-bottom: 24px;
1743
+ padding: 12px 24px;
1744
+ border: 4px solid #000;
1745
+ display: inline-block;
1746
+ background: #fff;
1747
+ box-shadow: 8px 8px 0 #000;
1748
+ }
1749
+
1750
+ .trips-list {
1751
+ display: flex;
1752
+ flex-direction: column;
1753
+ gap: 20px;
1754
+ }
1755
+
1756
+ .trip-item {
1757
+ display: flex;
1758
+ align-items: center;
1759
+ gap: 20px;
1760
+ padding: 20px 24px;
1761
+ background: #fff;
1762
+ border: 3px solid #000;
1763
+ box-shadow: 8px 8px 0 #000;
1764
+ transition: transform 0.2s;
1765
+ position: relative;
1766
+ overflow: hidden;
1767
+ }
1768
+
1769
+ .trip-item:nth-child(odd) {
1770
+ transform: rotate(0.5deg);
1771
+ }
1772
+
1773
+ .trip-item:nth-child(even) {
1774
+ transform: rotate(-0.5deg);
1775
+ }
1776
+
1777
+ .trip-index {
1778
+ width: 48px;
1779
+ height: 48px;
1780
+ background: #000;
1781
+ color: #fff;
1782
+ border-radius: 0;
1783
+ display: flex;
1784
+ align-items: center;
1785
+ justify-content: center;
1786
+ font-size: 24px;
1787
+ font-weight: 700;
1788
+ flex-shrink: 0;
1789
+ box-shadow: 4px 4px 0 #FF69B4;
1790
+ position: relative;
1791
+ z-index: 2;
1792
+ }
1793
+
1794
+ .trip-info {
1795
+ flex: 1;
1796
+ position: relative;
1797
+ z-index: 2;
1798
+ }
1799
+
1800
+ .trip-location {
1801
+ font-size: 28px;
1802
+ font-weight: 800;
1803
+ color: #000;
1804
+ margin-bottom: 4px;
1805
+ }
1806
+
1807
+ .trip-location-en {
1808
+ position: absolute;
1809
+ right: 120px;
1810
+ bottom: -10px;
1811
+ font-size: 16px;
1812
+ font-weight: 900;
1813
+ color: rgba(0,0,0,0.08);
1814
+ text-transform: uppercase;
1815
+ letter-spacing: 1px;
1816
+ pointer-events: none;
1817
+ white-space: nowrap;
1818
+ font-style: italic;
1819
+ z-index: 1;
1820
+ }
1821
+
1822
+ .trip-country {
1823
+ font-size: 22px;
1824
+ color: #000;
1825
+ font-weight: 500;
1826
+ background: #eee;
1827
+ display: inline-block;
1828
+ padding: 2px 8px;
1829
+ border: 2px solid #000;
1830
+ }
1831
+
1832
+ .trip-tz {
1833
+ font-size: 18px;
1834
+ font-weight: 700;
1835
+ color: #000;
1836
+ background: #FFD700;
1837
+ padding: 6px 12px;
1838
+ border: 2px solid #000;
1839
+ box-shadow: 3px 3px 0 #000;
1840
+ flex-shrink: 0;
1841
+ }
1842
+
1843
+ .more-trips {
1844
+ text-align: center;
1845
+ padding: 20px;
1846
+ color: #000;
1847
+ font-size: 24px;
1848
+ font-weight: 700;
1849
+ background: #fff;
1850
+ border: 3px dashed #000;
1851
+ margin-top: 10px;
1852
+ }
1853
+
1854
+ .footer {
1855
+ display: flex;
1856
+ justify-content: space-between;
1857
+ align-items: center;
1858
+ margin-top: 60px;
1859
+ padding-top: 32px;
1860
+ border-top: 6px solid #000;
1861
+ position: relative;
1862
+ z-index: 1;
1863
+ }
1864
+
1865
+ .brand {
1866
+ display: flex;
1867
+ align-items: center;
1868
+ gap: 12px;
1869
+ }
1870
+
1871
+ .brand-icon {
1872
+ font-size: 40px;
1873
+ background: #FF69B4;
1874
+ border: 3px solid #000;
1875
+ padding: 4px;
1876
+ line-height: 1;
1877
+ box-shadow: 4px 4px 0 #000;
1878
+ }
1879
+
1880
+ .brand-name {
1881
+ font-size: 32px;
1882
+ font-weight: 900;
1883
+ text-transform: uppercase;
1884
+ color: #000;
1885
+ font-style: italic;
1886
+ }
1887
+
1888
+ .generated-at {
1889
+ font-size: 20px;
1890
+ color: #000;
1891
+ font-weight: 600;
1892
+ background: #fff;
1893
+ padding: 4px 12px;
1894
+ border: 2px solid #000;
1895
+ }
1896
+
1897
+ .empty-state {
1898
+ text-align: center;
1899
+ padding: 80px 40px;
1900
+ border: 4px dashed #000;
1901
+ background: #fff;
1902
+ margin: 40px 0;
1903
+ }
1904
+
1905
+ .empty-icon {
1906
+ font-size: 80px;
1907
+ margin-bottom: 24px;
1908
+ }
1909
+
1910
+ .empty-text {
1911
+ font-size: 32px;
1912
+ font-weight: 700;
1913
+ color: #000;
1914
+ }
1915
+ </style>
1916
+ </head>
1917
+ <body>
1918
+ <div class="deco-shape-1"></div>
1919
+ <div class="deco-shape-2"></div>
1920
+
1921
+ <div class="container">
1922
+ <div class="header">
1923
+ <div class="avatar-ring">
1924
+ <img class="avatar" src="${avatarUrl || "https://ui-avatars.com/api/?name=U&background=667eea&color=fff"}" onerror="this.src='https://ui-avatars.com/api/?name=U&background=667eea&color=fff'" />
1925
+ </div>
1926
+ <div class="user-info">
1927
+ <div class="username">${escapeHtml2(username)}</div>
1928
+ <div class="period">${year}年${monthName} 总结</div>
1929
+ </div>
1930
+ </div>
1931
+
1932
+ <div class="title-section">
1933
+ <div class="title">猪猪日报</div>
1934
+ <div class="subtitle">PIG DAILY SUMMARY</div>
1935
+ </div>
1936
+
1937
+ ${totalTrips > 0 ? `
1938
+ ${statsHtml}
1939
+
1940
+ <div class="trips-section">
1941
+ <div class="section-title">本月足迹 TRACKS</div>
1942
+ <div class="trips-list">
1943
+ ${tripsHtml}
1944
+ ${moreTripsHtml}
1945
+ </div>
1946
+ </div>
1947
+ ` : `
1948
+ <div class="empty-state">
1949
+ <div class="empty-icon"><span class="twemoji">🐷</span></div>
1950
+ <div class="empty-text">这个月还没有旅行记录哦~</div>
1951
+ </div>
1952
+ `}
1953
+
1954
+ <div class="footer">
1955
+ <div class="brand">
1956
+ <div class="brand-icon"><span class="twemoji">🐷</span></div>
1957
+ <div class="brand-name">Pig Travel</div>
1958
+ </div>
1959
+ <div class="generated-at">${(/* @__PURE__ */ new Date()).toLocaleDateString("zh-CN")}</div>
1960
+ </div>
1961
+ </div>
1962
+
1963
+ <script>
1964
+ async function waitForImages() {
1965
+ const images = Array.from(document.images);
1966
+ const promises = images.map(img => {
1967
+ if (img.complete) return Promise.resolve();
1968
+ return new Promise(resolve => {
1969
+ img.onload = () => resolve();
1970
+ img.onerror = () => resolve();
1971
+ });
1972
+ });
1973
+ await Promise.all([
1974
+ ...promises,
1975
+ document.fonts.ready
1976
+ ]);
1977
+ await new Promise(r => setTimeout(r, 100));
1978
+ }
1979
+ window.renderReady = waitForImages();
1980
+ </script>
1981
+ </body>
1982
+ </html>
1983
+ `;
1984
+ let page = null;
1985
+ try {
1986
+ page = await ctx.puppeteer.page();
1987
+ if (config.debug) {
1988
+ page.on("console", (msg) => ctx.logger("pig").debug(`[Summary] ${msg.text()}`));
1989
+ }
1990
+ const baseHeight = 1200;
1991
+ const tripHeight = Math.min(logs.length, 12) * 90;
1992
+ const extraHeight = logs.length > 12 ? 60 : 0;
1993
+ const totalHeight = baseHeight + tripHeight + extraHeight + (logs.length === 0 ? 200 : 0);
1994
+ await page.setViewport({ width: 1080, height: Math.max(1920, totalHeight), deviceScaleFactor: 1 });
1995
+ await page.setContent(html, { waitUntil: "domcontentloaded" });
1996
+ await page.evaluate(() => window["renderReady"]);
1997
+ const buffer = await page.screenshot({ type: "png", fullPage: true });
1998
+ const filename = `pig_summary_${data.userId}_${year}_${month}.png`;
1999
+ ctx.logger("pig").info(`月度总结卡片已生成: ${filename}`);
2000
+ return { buffer, filename };
2001
+ } catch (e) {
2002
+ ctx.logger("pig").error("Failed to generate summary card", e);
2003
+ throw e;
2004
+ } finally {
2005
+ if (page) {
2006
+ try {
2007
+ await page.close();
2008
+ } catch (closeError) {
2009
+ if (config.debug) {
2010
+ ctx.logger("pig").warn(`Failed to close puppeteer page: ${closeError}`);
2011
+ }
2012
+ }
2013
+ }
2014
+ }
2015
+ }
2016
+ __name(generateMonthlySummaryCard, "generateMonthlySummaryCard");
2017
+ async function prepareMonthlySummary(ctx, userId, platform, username, avatarUrl, year, month) {
2018
+ const logs = await getMonthlyLogs(ctx, userId, platform, year, month);
2019
+ const countriesSet = /* @__PURE__ */ new Set();
2020
+ const locationsSet = /* @__PURE__ */ new Set();
2021
+ for (const log of logs) {
2022
+ countriesSet.add(log.country);
2023
+ locationsSet.add(log.location);
2024
+ }
2025
+ return {
2026
+ userId,
2027
+ platform,
2028
+ username,
2029
+ avatarUrl,
2030
+ year,
2031
+ month,
2032
+ logs,
2033
+ totalTrips: logs.length,
2034
+ countriesVisited: Array.from(countriesSet),
2035
+ locationsVisited: Array.from(locationsSet)
2036
+ };
2037
+ }
2038
+ __name(prepareMonthlySummary, "prepareMonthlySummary");
2039
+ function escapeHtml2(text) {
2040
+ const map = {
2041
+ "&": "&amp;",
2042
+ "<": "&lt;",
2043
+ ">": "&gt;",
2044
+ '"': "&quot;",
2045
+ "'": "&#039;"
2046
+ };
2047
+ return text.replace(/[&<>"']/g, (m) => map[m]);
2048
+ }
2049
+ __name(escapeHtml2, "escapeHtml");
2050
+
1301
2051
  // src/config.ts
1302
2052
  var import_koishi = require("koishi");
1303
2053
  var Config = import_koishi.Schema.intersect([
1304
2054
  import_koishi.Schema.object({
1305
2055
  outputMode: import_koishi.Schema.union(["text", "image"]).default("image").description("输出模式:text 纯文本,image 生成精美卡片"),
1306
- emojiFont: import_koishi.Schema.union([
1307
- import_koishi.Schema.const("System").description("系统默认"),
1308
- import_koishi.Schema.const("Noto Color Emoji").description("Noto Color Emoji"),
1309
- import_koishi.Schema.const("Twemoji").description("Twemoji (Twitter Emoji)")
1310
- ]).default("System").description("Emoji 字体偏好(需确保容器内已安装相应字体)"),
1311
2056
  travelMessageTemplate: import_koishi.Schema.string().default("去了 {landmark},{country}!📸").description("旅行消息模板(可用变量:{landmark} 地标名, {country} 国家名)")
1312
2057
  }).description("基础设置"),
1313
2058
  import_koishi.Schema.object({
1314
2059
  llmLocationEnabled: import_koishi.Schema.boolean().default(false).description("启用后使用 LLM 动态生成全球旅行地点,关闭则使用预设地点库"),
1315
2060
  llmLocationModel: import_koishi.Schema.dynamic("model").description("用于生成地点的模型(推荐使用快速模型如 gemini-flash)"),
1316
- unsplashAccessKey: import_koishi.Schema.string().role("secret").default("").description("用于获取高质量风景背景图(从 unsplash.com/developers 免费申请)")
2061
+ llmLocationCustomContext: import_koishi.Schema.string().role("textarea").default("").description("自定义生成上下文(如:关注北欧神话、赛博朋克风格建筑等,留空则完全随机)"),
2062
+ imageSearchPrompt: import_koishi.Schema.string().default("{landmark} {country} landscape").description("搜图关键词模板(可用变量:{landmark} 地标英文名, {country} 国家英文名, {city} 城市英文名)"),
2063
+ unsplashAccessKey: import_koishi.Schema.string().role("secret").default("").description("Unsplash API Access Key (可选)"),
2064
+ pexelsApiKey: import_koishi.Schema.string().role("secret").default("").description("Pexels API Key (可选,作为 Unsplash 的补充)"),
2065
+ backgroundFetchMode: import_koishi.Schema.union([
2066
+ import_koishi.Schema.const("auto").description("自动:尽量内联远程图片,遇到易超时域名则直接使用 URL"),
2067
+ import_koishi.Schema.const("always").description("强制服务端拉取并内联(更稳但可能慢)"),
2068
+ import_koishi.Schema.const("never").description("不进行服务端拉取,直接使用 URL")
2069
+ ]).default("auto").description("背景图服务端拉取策略"),
2070
+ backgroundFetchTimeoutMs: import_koishi.Schema.number().default(8e3).description("背景图服务端拉取超时(毫秒)")
1317
2071
  }).description("地点与图片 🌍"),
1318
2072
  import_koishi.Schema.object({
1319
2073
  aigcEnabled: import_koishi.Schema.boolean().default(false).description("启用后使用 AI 生成小猪旅行插画(需要 media-luna 插件)"),
@@ -1331,6 +2085,7 @@ var Config = import_koishi.Schema.intersect([
1331
2085
  useStorageService: import_koishi.Schema.boolean().default(true).description("使用 chatluna-storage-service 缓存图片(推荐)"),
1332
2086
  storageCacheHours: import_koishi.Schema.number().default(24).description("图片缓存时间(小时)"),
1333
2087
  logRetentionDays: import_koishi.Schema.number().default(45).description("旅行记录保留天数"),
2088
+ monthlySummaryEnabled: import_koishi.Schema.boolean().default(false).description("每月1日自动生成上月旅行总结"),
1334
2089
  logPath: import_koishi.Schema.string().default("./data/pig/logs").description("本地日志存储路径(仅在不使用存储服务时生效)")
1335
2090
  }).description("存储设置 💾"),
1336
2091
  import_koishi.Schema.object({
@@ -1421,6 +2176,79 @@ function apply(ctx, config) {
1421
2176
  const result = await triggerTravelSequence(ctx, config, userInfo, platform);
1422
2177
  return formatTravelMessage(result, userId, config);
1423
2178
  });
2179
+ ctx.command("pig.summary [year:number] [month:number]", "生成月度旅行总结(调试用)").option("all", "-a 生成所有用户的总结").action(async ({ session, options }, yearArg, monthArg) => {
2180
+ const now = /* @__PURE__ */ new Date();
2181
+ let year = yearArg ?? now.getFullYear();
2182
+ let month = monthArg ?? now.getMonth();
2183
+ if (!monthArg && now.getMonth() === 0) {
2184
+ year = now.getFullYear() - 1;
2185
+ month = 12;
2186
+ } else if (!monthArg) {
2187
+ month = now.getMonth();
2188
+ }
2189
+ if (month < 1 || month > 12) {
2190
+ return "月份必须在 1-12 之间";
2191
+ }
2192
+ await session.send(`正在生成 ${year}年${month}月 的旅行总结...`);
2193
+ try {
2194
+ if (options.all) {
2195
+ const users = await getUsersWithLogsInMonth(ctx, year, month);
2196
+ if (users.length === 0) {
2197
+ return `${year}年${month}月 没有任何旅行记录`;
2198
+ }
2199
+ await session.send(`找到 ${users.length} 位用户有旅行记录,开始生成...`);
2200
+ for (const { userId, platform } of users) {
2201
+ try {
2202
+ const summaryData = await prepareMonthlySummary(
2203
+ ctx,
2204
+ userId,
2205
+ platform,
2206
+ userId,
2207
+ "",
2208
+ year,
2209
+ month
2210
+ );
2211
+ const result = await generateMonthlySummaryCard(ctx, config, summaryData);
2212
+ const base64 = result.buffer.toString("base64");
2213
+ await session.send(`用户 ${userId} 的总结:
2214
+ ${import_koishi2.segment.image(`base64://${base64}`)}`);
2215
+ } catch (e) {
2216
+ ctx.logger("pig").error(`Failed to generate summary for user ${userId}:`, e);
2217
+ await session.send(`生成用户 ${userId} 的总结时出错: ${e}`);
2218
+ }
2219
+ }
2220
+ return `已完成 ${users.length} 位用户的月度总结生成`;
2221
+ } else {
2222
+ const platform = session.platform;
2223
+ const userId = session.userId;
2224
+ let avatarUrl = session.author?.avatar || "";
2225
+ if (!avatarUrl && (platform === "onebot" || platform === "qq" || platform.includes("qq"))) {
2226
+ avatarUrl = `https://q.qlogo.cn/headimg_dl?dst_uin=${userId}&spec=640`;
2227
+ }
2228
+ const userInfo = {
2229
+ userId,
2230
+ username: session.author?.nickname || session.author?.name || session.username || userId,
2231
+ avatarUrl
2232
+ };
2233
+ const summaryData = await prepareMonthlySummary(
2234
+ ctx,
2235
+ userId,
2236
+ platform,
2237
+ userInfo.username,
2238
+ userInfo.avatarUrl,
2239
+ year,
2240
+ month
2241
+ );
2242
+ const result = await generateMonthlySummaryCard(ctx, config, summaryData);
2243
+ const base64 = result.buffer.toString("base64");
2244
+ return `${import_koishi2.segment.at(userId)} ${year}年${month}月 旅行总结
2245
+ ${import_koishi2.segment.image(`base64://${base64}`)}`;
2246
+ }
2247
+ } catch (e) {
2248
+ ctx.logger("pig").error("Failed to generate monthly summary:", e);
2249
+ return `生成月度总结失败: ${e}`;
2250
+ }
2251
+ });
1424
2252
  ctx.middleware(async (session, next) => {
1425
2253
  if (!config.experimentalAutoDetect) return next();
1426
2254
  if (!session.userId || !session.content) return next();
@@ -1471,9 +2299,46 @@ function apply(ctx, config) {
1471
2299
  return next();
1472
2300
  });
1473
2301
  ctx.cron("0 0 1 * *", async () => {
2302
+ if (!config.monthlySummaryEnabled) {
2303
+ ctx.logger("pig").debug("Monthly summary is disabled, skipping...");
2304
+ return;
2305
+ }
1474
2306
  const now = /* @__PURE__ */ new Date();
1475
- const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
1476
- ctx.logger("pig").info("Generating monthly travel handbook...");
2307
+ let year = now.getFullYear();
2308
+ let month = now.getMonth();
2309
+ if (month === 0) {
2310
+ year -= 1;
2311
+ month = 12;
2312
+ }
2313
+ ctx.logger("pig").info(`Generating monthly travel handbook for ${year}/${month}...`);
2314
+ try {
2315
+ const users = await getUsersWithLogsInMonth(ctx, year, month);
2316
+ if (users.length === 0) {
2317
+ ctx.logger("pig").info(`No travel logs found for ${year}/${month}`);
2318
+ return;
2319
+ }
2320
+ ctx.logger("pig").info(`Found ${users.length} users with travel logs for ${year}/${month}`);
2321
+ for (const { userId, platform } of users) {
2322
+ try {
2323
+ const summaryData = await prepareMonthlySummary(
2324
+ ctx,
2325
+ userId,
2326
+ platform,
2327
+ userId,
2328
+ "",
2329
+ year,
2330
+ month
2331
+ );
2332
+ await generateMonthlySummaryCard(ctx, config, summaryData);
2333
+ ctx.logger("pig").info(`Generated summary for user ${userId}`);
2334
+ } catch (e) {
2335
+ ctx.logger("pig").error(`Failed to generate summary for user ${userId}:`, e);
2336
+ }
2337
+ }
2338
+ ctx.logger("pig").info(`Monthly handbook generation completed for ${year}/${month}`);
2339
+ } catch (e) {
2340
+ ctx.logger("pig").error("Failed to generate monthly travel handbook:", e);
2341
+ }
1477
2342
  });
1478
2343
  ctx.cron("0 3 * * *", async () => {
1479
2344
  const cutoffDate = /* @__PURE__ */ new Date();