koishi-plugin-my-pig-group-friends 1.1.3 → 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/README.md +89 -25
- package/lib/config.d.ts +1 -0
- package/lib/database.d.ts +3 -0
- package/lib/index.js +757 -10
- package/lib/services/pexels.d.ts +2 -1
- package/lib/services/summary.d.ts +38 -0
- package/lib/services/unsplash.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,15 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/koishi-plugin-my-pig-group-friends)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**猪醒** - 虚拟旅行打卡插件,让你的猪猪群友环游世界。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> 你群里有没有那种作息混乱、日夜颠倒的猪猪朋友?现在,每当他们在奇怪的时间冒泡,就送他们去世界各地"旅行"吧!
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
9
|
+
## 功能亮点
|
|
10
|
+
|
|
11
|
+
- **虚拟旅行打卡** - 生成精美毛玻璃效果足迹卡片,记录猪猪的环球之旅
|
|
12
|
+
- **智能日出选点** - LLM 会根据当前时间,选择正在迎接日出的地区作为目的地(毕竟是"猪醒"嘛)
|
|
13
|
+
- **高质量风景图** - 集成 Unsplash / Pexels API,获取目的地真实风景照片
|
|
14
|
+
- **AI 生图支持** - 可选生成小猪在当地旅行的 AI 插画
|
|
15
|
+
- **作息异常检测(实验性)** - 自动检测用户每日首条消息时间,判断作息是否异常
|
|
16
|
+
|
|
17
|
+
## 效果预览
|
|
18
|
+
|
|
19
|
+
当触发旅行时,会生成类似这样的卡片:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────────────────────┐
|
|
23
|
+
│ [目的地风景照片背景] │
|
|
24
|
+
│ │
|
|
25
|
+
│ 🐷 xxx 猪醒! │
|
|
26
|
+
│ │
|
|
27
|
+
│ 去了 冰岛蓝湖温泉,冰岛! │
|
|
28
|
+
│ │
|
|
29
|
+
│ 2024-01-15 06:32 │
|
|
30
|
+
└─────────────────────────────┘
|
|
31
|
+
```
|
|
14
32
|
|
|
15
33
|
## 安装
|
|
16
34
|
|
|
@@ -22,40 +40,86 @@ npm install koishi-plugin-my-pig-group-friends
|
|
|
22
40
|
|
|
23
41
|
### 必需
|
|
24
42
|
- `database` - 存储用户状态和旅行日志
|
|
25
|
-
- `cron` -
|
|
26
|
-
- `puppeteer` -
|
|
43
|
+
- `cron` - 定时清理任务
|
|
44
|
+
- `puppeteer` - 渲染精美卡片
|
|
27
45
|
|
|
28
46
|
### 可选
|
|
29
|
-
- `chatluna` -
|
|
30
|
-
- `chatluna_storage` -
|
|
31
|
-
- `media-luna` -
|
|
47
|
+
- `chatluna` - LLM 动态生成旅行地点(推荐)
|
|
48
|
+
- `chatluna_storage` - 高效管理图片缓存
|
|
49
|
+
- `media-luna` - AI 绘图功能
|
|
32
50
|
|
|
33
51
|
## 使用方法
|
|
34
52
|
|
|
35
53
|
### 指令
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
| 指令 | 说明 |
|
|
56
|
+
|------|------|
|
|
57
|
+
| `pig` | 送自己去旅行 |
|
|
58
|
+
| `pig @某人` | 送指定用户去旅行 |
|
|
59
|
+
| `猪醒` | `pig` 的别名 |
|
|
60
|
+
|
|
61
|
+
### 示例
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
> 猪醒 @张三
|
|
65
|
+
🐷 张三 猪醒!去了 挪威罗弗敦群岛,挪威!
|
|
66
|
+
[精美卡片图片]
|
|
67
|
+
```
|
|
39
68
|
|
|
40
|
-
##
|
|
69
|
+
## 配置说明
|
|
41
70
|
|
|
42
71
|
### 基础设置
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
|
|
73
|
+
| 配置项 | 默认值 | 说明 |
|
|
74
|
+
|--------|--------|------|
|
|
75
|
+
| `outputMode` | `image` | 输出模式:`image` 生成卡片,`text` 纯文本 |
|
|
76
|
+
| `travelMessageTemplate` | `去了 {landmark},{country}!` | 旅行消息模板 |
|
|
45
77
|
|
|
46
78
|
### 地点与图片
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
|
|
80
|
+
| 配置项 | 默认值 | 说明 |
|
|
81
|
+
|--------|--------|------|
|
|
82
|
+
| `llmLocationEnabled` | `false` | 启用 LLM 动态生成地点 |
|
|
83
|
+
| `llmLocationModel` | - | LLM 模型(推荐 gemini-flash 等快速模型) |
|
|
84
|
+
| `llmLocationCustomContext` | - | 自定义偏好(如:北欧风格、赛博朋克建筑等) |
|
|
85
|
+
| `imageSearchPrompt` | `{landmark} {country} landscape` | 图片搜索关键词模板 |
|
|
86
|
+
| `unsplashAccessKey` | - | Unsplash API 密钥 |
|
|
87
|
+
| `pexelsApiKey` | - | Pexels API 密钥(备用图源) |
|
|
50
88
|
|
|
51
89
|
### AI 生图(可选)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
|
|
91
|
+
| 配置项 | 默认值 | 说明 |
|
|
92
|
+
|--------|--------|------|
|
|
93
|
+
| `aigcEnabled` | `false` | 启用 AI 绘图 |
|
|
94
|
+
| `aigcChannel` | - | media-luna 绘图渠道 |
|
|
95
|
+
| `aigcPrompt` | `一个可爱的卡通小猪正在...` | 绘图提示词模板 |
|
|
55
96
|
|
|
56
97
|
### 自动检测(实验性)
|
|
57
|
-
|
|
58
|
-
|
|
98
|
+
|
|
99
|
+
| 配置项 | 默认值 | 说明 |
|
|
100
|
+
|--------|--------|------|
|
|
101
|
+
| `experimentalAutoDetect` | `false` | 自动检测作息异常 |
|
|
102
|
+
| `abnormalThreshold` | `3` | 异常判定阈值(小时) |
|
|
103
|
+
|
|
104
|
+
### 存储设置
|
|
105
|
+
|
|
106
|
+
| 配置项 | 默认值 | 说明 |
|
|
107
|
+
|--------|--------|------|
|
|
108
|
+
| `useStorageService` | `true` | 使用存储服务缓存图片 |
|
|
109
|
+
| `storageCacheHours` | `24` | 图片缓存时间(小时) |
|
|
110
|
+
| `logRetentionDays` | `45` | 旅行记录保留天数 |
|
|
111
|
+
|
|
112
|
+
## 获取 API 密钥
|
|
113
|
+
|
|
114
|
+
### Unsplash(推荐)
|
|
115
|
+
1. 访问 [Unsplash Developers](https://unsplash.com/developers)
|
|
116
|
+
2. 创建应用,获取 Access Key
|
|
117
|
+
3. 免费额度:50 次/小时
|
|
118
|
+
|
|
119
|
+
### Pexels(备用)
|
|
120
|
+
1. 访问 [Pexels API](https://www.pexels.com/api/)
|
|
121
|
+
2. 注册并获取 API Key
|
|
122
|
+
3. 免费额度:200 次/小时
|
|
59
123
|
|
|
60
124
|
## License
|
|
61
125
|
|
package/lib/config.d.ts
CHANGED
package/lib/database.d.ts
CHANGED
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 });
|
|
@@ -146,8 +149,8 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
|
|
|
146
149
|
},
|
|
147
150
|
redirect: "follow"
|
|
148
151
|
});
|
|
149
|
-
const contentType = response.headers
|
|
150
|
-
const contentLength = response.headers
|
|
152
|
+
const contentType = response.headers?.["content-type"] || "";
|
|
153
|
+
const contentLength = response.headers?.["content-length"] || "unknown";
|
|
151
154
|
if (config.debug) {
|
|
152
155
|
ctx.logger("pig").debug(
|
|
153
156
|
`Background fetch response: status=${response.status} url=${response.url} content-type=${contentType || "unknown"} content-length=${contentLength}`
|
|
@@ -1057,7 +1060,8 @@ async function searchUnsplashPhoto(ctx, accessKey, query, debug = false) {
|
|
|
1057
1060
|
{
|
|
1058
1061
|
params: {
|
|
1059
1062
|
query,
|
|
1060
|
-
per_page:
|
|
1063
|
+
per_page: 10,
|
|
1064
|
+
// Fetch more results for variety
|
|
1061
1065
|
orientation: "landscape"
|
|
1062
1066
|
},
|
|
1063
1067
|
headers: {
|
|
@@ -1068,13 +1072,15 @@ async function searchUnsplashPhoto(ctx, accessKey, query, debug = false) {
|
|
|
1068
1072
|
}
|
|
1069
1073
|
);
|
|
1070
1074
|
if (response.results && response.results.length > 0) {
|
|
1071
|
-
|
|
1075
|
+
const randomIndex = Math.floor(Math.random() * response.results.length);
|
|
1076
|
+
const selectedPhoto = response.results[randomIndex];
|
|
1077
|
+
let photoUrl = selectedPhoto.urls.regular;
|
|
1072
1078
|
if (photoUrl.includes("?")) {
|
|
1073
1079
|
photoUrl += "&fm=webp&q=85";
|
|
1074
1080
|
} else {
|
|
1075
1081
|
photoUrl += "?fm=webp&q=85";
|
|
1076
1082
|
}
|
|
1077
|
-
if (debug) ctx.logger("pig").debug(`Unsplash:
|
|
1083
|
+
if (debug) ctx.logger("pig").debug(`Unsplash: Selected photo ${randomIndex + 1}/${response.results.length}, URL: ${photoUrl}`);
|
|
1078
1084
|
return photoUrl;
|
|
1079
1085
|
}
|
|
1080
1086
|
if (debug) ctx.logger("pig").debug("Unsplash: No photos found for query");
|
|
@@ -1100,7 +1106,8 @@ async function searchPexelsPhoto(ctx, apiKey, query, debug = false) {
|
|
|
1100
1106
|
{
|
|
1101
1107
|
params: {
|
|
1102
1108
|
query,
|
|
1103
|
-
per_page:
|
|
1109
|
+
per_page: 10,
|
|
1110
|
+
// Fetch more results for variety
|
|
1104
1111
|
orientation: "landscape"
|
|
1105
1112
|
},
|
|
1106
1113
|
headers: {
|
|
@@ -1110,7 +1117,9 @@ async function searchPexelsPhoto(ctx, apiKey, query, debug = false) {
|
|
|
1110
1117
|
}
|
|
1111
1118
|
);
|
|
1112
1119
|
if (response.photos && response.photos.length > 0) {
|
|
1113
|
-
|
|
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;
|
|
1114
1123
|
if (photoUrl.includes("w=")) {
|
|
1115
1124
|
photoUrl = photoUrl.replace(/w=\d+/, "w=1080");
|
|
1116
1125
|
} else if (photoUrl.includes("?")) {
|
|
@@ -1118,7 +1127,7 @@ async function searchPexelsPhoto(ctx, apiKey, query, debug = false) {
|
|
|
1118
1127
|
} else {
|
|
1119
1128
|
photoUrl += "?w=1080";
|
|
1120
1129
|
}
|
|
1121
|
-
if (debug) ctx.logger("pig").debug(`Pexels:
|
|
1130
|
+
if (debug) ctx.logger("pig").debug(`Pexels: Selected photo ${randomIndex + 1}/${response.photos.length}, URL: ${photoUrl}`);
|
|
1122
1131
|
return photoUrl;
|
|
1123
1132
|
}
|
|
1124
1133
|
if (debug) ctx.logger("pig").debug("Pexels: No photos found for query");
|
|
@@ -1397,7 +1406,10 @@ async function triggerTravelSequence(ctx, config, userInfo, platform) {
|
|
|
1397
1406
|
platform,
|
|
1398
1407
|
timestamp: now,
|
|
1399
1408
|
country: location.country,
|
|
1409
|
+
countryZh: location.countryZh || location.country,
|
|
1400
1410
|
location: location.landmark,
|
|
1411
|
+
locationZh: location.landmarkZh || location.landmark,
|
|
1412
|
+
timezone: location.timezone || "UTC",
|
|
1401
1413
|
imagePath: imageUrl || "",
|
|
1402
1414
|
// 存储 URL 或空
|
|
1403
1415
|
isAIGC
|
|
@@ -1412,6 +1424,630 @@ async function triggerTravelSequence(ctx, config, userInfo, platform) {
|
|
|
1412
1424
|
}
|
|
1413
1425
|
__name(triggerTravelSequence, "triggerTravelSequence");
|
|
1414
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
|
+
"&": "&",
|
|
2042
|
+
"<": "<",
|
|
2043
|
+
">": ">",
|
|
2044
|
+
'"': """,
|
|
2045
|
+
"'": "'"
|
|
2046
|
+
};
|
|
2047
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
2048
|
+
}
|
|
2049
|
+
__name(escapeHtml2, "escapeHtml");
|
|
2050
|
+
|
|
1415
2051
|
// src/config.ts
|
|
1416
2052
|
var import_koishi = require("koishi");
|
|
1417
2053
|
var Config = import_koishi.Schema.intersect([
|
|
@@ -1449,6 +2085,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
1449
2085
|
useStorageService: import_koishi.Schema.boolean().default(true).description("使用 chatluna-storage-service 缓存图片(推荐)"),
|
|
1450
2086
|
storageCacheHours: import_koishi.Schema.number().default(24).description("图片缓存时间(小时)"),
|
|
1451
2087
|
logRetentionDays: import_koishi.Schema.number().default(45).description("旅行记录保留天数"),
|
|
2088
|
+
monthlySummaryEnabled: import_koishi.Schema.boolean().default(false).description("每月1日自动生成上月旅行总结"),
|
|
1452
2089
|
logPath: import_koishi.Schema.string().default("./data/pig/logs").description("本地日志存储路径(仅在不使用存储服务时生效)")
|
|
1453
2090
|
}).description("存储设置 💾"),
|
|
1454
2091
|
import_koishi.Schema.object({
|
|
@@ -1539,6 +2176,79 @@ function apply(ctx, config) {
|
|
|
1539
2176
|
const result = await triggerTravelSequence(ctx, config, userInfo, platform);
|
|
1540
2177
|
return formatTravelMessage(result, userId, config);
|
|
1541
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
|
+
});
|
|
1542
2252
|
ctx.middleware(async (session, next) => {
|
|
1543
2253
|
if (!config.experimentalAutoDetect) return next();
|
|
1544
2254
|
if (!session.userId || !session.content) return next();
|
|
@@ -1589,9 +2299,46 @@ function apply(ctx, config) {
|
|
|
1589
2299
|
return next();
|
|
1590
2300
|
});
|
|
1591
2301
|
ctx.cron("0 0 1 * *", async () => {
|
|
2302
|
+
if (!config.monthlySummaryEnabled) {
|
|
2303
|
+
ctx.logger("pig").debug("Monthly summary is disabled, skipping...");
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
1592
2306
|
const now = /* @__PURE__ */ new Date();
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
+
}
|
|
1595
2342
|
});
|
|
1596
2343
|
ctx.cron("0 3 * * *", async () => {
|
|
1597
2344
|
const cutoffDate = /* @__PURE__ */ new Date();
|
package/lib/services/pexels.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
/**
|
|
3
|
-
* Search for a photo on Pexels and return
|
|
3
|
+
* Search for a photo on Pexels and return a randomly selected large-sized URL
|
|
4
|
+
* Fetches multiple results and randomly selects one to avoid repetitive images
|
|
4
5
|
* @returns The photo URL or null if not found/error
|
|
5
6
|
*/
|
|
6
7
|
export declare function searchPexelsPhoto(ctx: Context, apiKey: string, query: string, debug?: boolean): Promise<string | null>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
import { PigTravelLog } from '../database';
|
|
4
|
+
export interface MonthlySummaryData {
|
|
5
|
+
userId: string;
|
|
6
|
+
platform: string;
|
|
7
|
+
username: string;
|
|
8
|
+
avatarUrl: string;
|
|
9
|
+
year: number;
|
|
10
|
+
month: number;
|
|
11
|
+
logs: PigTravelLog[];
|
|
12
|
+
totalTrips: number;
|
|
13
|
+
countriesVisited: string[];
|
|
14
|
+
locationsVisited: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface SummaryCardResult {
|
|
17
|
+
buffer: Buffer;
|
|
18
|
+
filename: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 获取指定用户指定月份的旅行记录
|
|
22
|
+
*/
|
|
23
|
+
export declare function getMonthlyLogs(ctx: Context, userId: string, platform: string, year: number, month: number): Promise<PigTravelLog[]>;
|
|
24
|
+
/**
|
|
25
|
+
* 获取所有有旅行记录的用户(指定月份)
|
|
26
|
+
*/
|
|
27
|
+
export declare function getUsersWithLogsInMonth(ctx: Context, year: number, month: number): Promise<{
|
|
28
|
+
userId: string;
|
|
29
|
+
platform: string;
|
|
30
|
+
}[]>;
|
|
31
|
+
/**
|
|
32
|
+
* 生成月度总结卡片
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateMonthlySummaryCard(ctx: Context, config: Config, data: MonthlySummaryData): Promise<SummaryCardResult>;
|
|
35
|
+
/**
|
|
36
|
+
* 准备月度总结数据
|
|
37
|
+
*/
|
|
38
|
+
export declare function prepareMonthlySummary(ctx: Context, userId: string, platform: string, username: string, avatarUrl: string, year: number, month: number): Promise<MonthlySummaryData>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
/**
|
|
3
|
-
* Search for a photo on Unsplash and return
|
|
3
|
+
* Search for a photo on Unsplash and return a randomly selected regular-sized URL
|
|
4
|
+
* Fetches multiple results and randomly selects one to avoid repetitive images
|
|
4
5
|
* @returns The photo URL or null if not found/error
|
|
5
6
|
*/
|
|
6
7
|
export declare function searchUnsplashPhoto(ctx: Context, accessKey: string, query: string, debug?: boolean): Promise<string | null>;
|