koishi-plugin-my-pig-group-friends 1.1.0 → 1.1.3

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 CHANGED
@@ -14,7 +14,12 @@ export interface Config {
14
14
  logPath: string;
15
15
  llmLocationEnabled: boolean;
16
16
  llmLocationModel: string;
17
+ llmLocationCustomContext: string;
18
+ imageSearchPrompt: string;
17
19
  unsplashAccessKey: string;
20
+ pexelsApiKey: string;
21
+ backgroundFetchMode: 'auto' | 'always' | 'never';
22
+ backgroundFetchTimeoutMs: number;
18
23
  logRetentionDays: number;
19
24
  experimentalAutoDetect: boolean;
20
25
  debug: boolean;
package/lib/index.js CHANGED
@@ -101,6 +101,13 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
101
101
  return "image/jpeg";
102
102
  }, "sniffMime");
103
103
  const inlineMaxBytes = 900 * 1024;
104
+ const fetchTimeoutMs = config.backgroundFetchTimeoutMs ?? 8e3;
105
+ const shouldServerFetch = /* @__PURE__ */ __name((url) => {
106
+ const mode = config.backgroundFetchMode || "auto";
107
+ if (mode === "never") return false;
108
+ if (mode === "always") return true;
109
+ return !/^https?:\/\/(images|source)\.unsplash\.com\//i.test(url);
110
+ }, "shouldServerFetch");
104
111
  const fetchToDataUrl = /* @__PURE__ */ __name(async (url) => {
105
112
  const normalized = normalizeImageUrl(url);
106
113
  if (config.debug) ctx.logger("pig").debug(`Normalized background URL: ${normalized}`);
@@ -122,12 +129,17 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
122
129
  }
123
130
  }
124
131
  if (/^https?:\/\//i.test(normalized)) {
132
+ if (!shouldServerFetch(normalized)) {
133
+ if (config.debug) {
134
+ ctx.logger("pig").debug(`Skip server-side fetch for background: ${normalized}`);
135
+ }
136
+ return normalized;
137
+ }
125
138
  try {
126
139
  if (config.debug) ctx.logger("pig").debug(`Server-side fetching background: ${normalized}`);
127
140
  const response = await ctx.http(normalized, {
128
141
  responseType: "arraybuffer",
129
- timeout: 8e3,
130
- // Reduced from 15s to 8s for better responsiveness
142
+ timeout: fetchTimeoutMs,
131
143
  headers: {
132
144
  "User-Agent": "Mozilla/5.0",
133
145
  "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"
@@ -177,6 +189,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
177
189
  }
178
190
  const now = /* @__PURE__ */ new Date();
179
191
  const dateStr = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`;
192
+ const bgCssValue = bgImage ? `url("${bgImage}")` : "none";
180
193
  const html = `
181
194
  <!DOCTYPE html>
182
195
  <html>
@@ -194,12 +207,18 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
194
207
  width: 1080px;
195
208
  height: 1920px;
196
209
  overflow: hidden;
197
- /* Prioritize system fonts with wide Unicode coverage for emoji and CJK support */
198
- 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, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
210
+ /* Default emoji font set to Noto Color Emoji to maintain layout consistency */
211
+ 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";
199
212
  background: #f0f0f2;
200
- --bg-image: url('${bgImage}');
213
+ --bg-image: ${bgCssValue};
201
214
  }
202
215
 
216
+ /* Specific class for the pig emoji to use Twemoji */
217
+ .twemoji {
218
+ font-family: "Twemoji", "Noto Color Emoji", sans-serif;
219
+ }
220
+
221
+
203
222
  .wrapper {
204
223
  position: relative;
205
224
  width: 100%;
@@ -214,10 +233,9 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
214
233
  inset: 0;
215
234
  width: 100%;
216
235
  height: 100%;
236
+ display: block;
237
+ object-fit: cover;
217
238
  background-color: #d1d1d6;
218
- background-image: var(--bg-image);
219
- background-size: cover;
220
- background-position: center;
221
239
  z-index: 0;
222
240
  }
223
241
 
@@ -375,7 +393,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
375
393
  .message-text {
376
394
  font-size: 56px;
377
395
  line-height: 1.25;
378
- font-weight: 800;
396
+ font-weight: 900;
379
397
  color: #1d1d1f;
380
398
  letter-spacing: -0.03em;
381
399
  word-break: break-word;
@@ -389,8 +407,11 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
389
407
  box-decoration-break: clone;
390
408
  -webkit-box-decoration-break: clone;
391
409
  display: inline-block;
410
+ font-weight: 900;
411
+ letter-spacing: -0.01em;
392
412
  }
393
413
 
414
+
394
415
  .divider {
395
416
  height: 2px;
396
417
  background: rgba(0,0,0,0.08);
@@ -423,14 +444,14 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
423
444
 
424
445
  .location-pill span {
425
446
  font-size: 26px;
426
- font-weight: 700;
447
+ font-weight: 800;
427
448
  color: #ffffff;
428
449
  letter-spacing: 0.02em;
429
450
  }
430
451
 
431
452
  .landmark-name {
432
453
  font-size: 38px;
433
- font-weight: 800;
454
+ font-weight: 900;
434
455
  color: #1d1d1f;
435
456
  letter-spacing: -0.01em;
436
457
  padding-left: 6px;
@@ -462,7 +483,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
462
483
  </head>
463
484
  <body>
464
485
  <div class="wrapper">
465
- <div class="bg-image"></div>
486
+ <img class="bg-image" src="${bgImage || ""}" alt="" />
466
487
  <div class="bg-overlay"></div>
467
488
 
468
489
  <div class="card-container">
@@ -481,7 +502,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
481
502
 
482
503
  <div class="message-body">
483
504
  <div class="message-text">
484
- 今天 🐷猪醒在<br/>
505
+ 今天 <span class="twemoji">🐷</span>猪醒在<br/>
485
506
  <span class="highlight">${data.location.landmarkZh || data.location.landmark}</span>
486
507
  </div>
487
508
  </div>
@@ -497,7 +518,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
497
518
  </div>
498
519
 
499
520
  <div class="brand-tag">
500
- <div class="brand-icon">🐷</div>
521
+ <div class="brand-icon"><span class="twemoji">🐷</span></div>
501
522
  <div class="brand-name">Pig Travel</div>
502
523
  </div>
503
524
  </div>
@@ -1065,6 +1086,50 @@ async function searchUnsplashPhoto(ctx, accessKey, query, debug = false) {
1065
1086
  }
1066
1087
  __name(searchUnsplashPhoto, "searchUnsplashPhoto");
1067
1088
 
1089
+ // src/services/pexels.ts
1090
+ var PEXELS_API_BASE = "https://api.pexels.com/v1";
1091
+ async function searchPexelsPhoto(ctx, apiKey, query, debug = false) {
1092
+ if (!apiKey) {
1093
+ if (debug) ctx.logger("pig").debug("Pexels: No API key provided");
1094
+ return null;
1095
+ }
1096
+ try {
1097
+ if (debug) ctx.logger("pig").debug(`Pexels: Searching for "${query}"`);
1098
+ const response = await ctx.http.get(
1099
+ `${PEXELS_API_BASE}/search`,
1100
+ {
1101
+ params: {
1102
+ query,
1103
+ per_page: 1,
1104
+ orientation: "landscape"
1105
+ },
1106
+ headers: {
1107
+ Authorization: apiKey
1108
+ },
1109
+ timeout: 1e4
1110
+ }
1111
+ );
1112
+ if (response.photos && response.photos.length > 0) {
1113
+ let photoUrl = response.photos[0].src.large2x || response.photos[0].src.large;
1114
+ if (photoUrl.includes("w=")) {
1115
+ photoUrl = photoUrl.replace(/w=\d+/, "w=1080");
1116
+ } else if (photoUrl.includes("?")) {
1117
+ photoUrl += "&w=1080";
1118
+ } else {
1119
+ photoUrl += "?w=1080";
1120
+ }
1121
+ if (debug) ctx.logger("pig").debug(`Pexels: Found photo URL: ${photoUrl}`);
1122
+ return photoUrl;
1123
+ }
1124
+ if (debug) ctx.logger("pig").debug("Pexels: No photos found for query");
1125
+ return null;
1126
+ } catch (e) {
1127
+ ctx.logger("pig").warn(`Pexels API error: ${e}`);
1128
+ return null;
1129
+ }
1130
+ }
1131
+ __name(searchPexelsPhoto, "searchPexelsPhoto");
1132
+
1068
1133
  // src/services/location.ts
1069
1134
  var LOCATION_CATEGORIES = [
1070
1135
  "自然奇观(峡谷、瀑布、火山、冰川、沙漠绿洲等)",
@@ -1079,13 +1144,43 @@ var LOCATION_CATEGORIES = [
1079
1144
  "神秘与奇特之地(地质奇观、UFO小镇、怪异地貌等)"
1080
1145
  ];
1081
1146
  var CONTINENTS = ["亚洲", "欧洲", "非洲", "北美洲", "南美洲", "大洋洲", "南极洲"];
1147
+ function getSunriseTimezoneHint() {
1148
+ const now = /* @__PURE__ */ new Date();
1149
+ const utcHour = now.getUTCHours();
1150
+ const targetLocalHour = 6;
1151
+ const idealOffset = targetLocalHour - utcHour;
1152
+ const normalizeOffset = /* @__PURE__ */ __name((offset) => {
1153
+ if (offset < -12) return offset + 24;
1154
+ if (offset > 14) return offset - 24;
1155
+ return offset;
1156
+ }, "normalizeOffset");
1157
+ const minOffset = normalizeOffset(idealOffset - 1);
1158
+ const maxOffset = normalizeOffset(idealOffset + 1);
1159
+ const getRegionByOffset = /* @__PURE__ */ __name((offset) => {
1160
+ if (offset >= 9 && offset <= 12) return "东亚、澳大利亚东部、太平洋岛屿";
1161
+ if (offset >= 5 && offset <= 8) return "南亚、东南亚、中亚";
1162
+ if (offset >= 2 && offset <= 4) return "中东、东欧、东非";
1163
+ if (offset >= -1 && offset <= 1) return "西欧、西非";
1164
+ if (offset >= -5 && offset <= -2) return "南美洲东部、大西洋";
1165
+ if (offset >= -8 && offset <= -6) return "北美洲西部、太平洋东部";
1166
+ if (offset >= -12 && offset <= -9) return "太平洋中部、夏威夷、阿拉斯加";
1167
+ return "全球各地";
1168
+ }, "getRegionByOffset");
1169
+ const formatOffset = /* @__PURE__ */ __name((offset) => offset >= 0 ? `UTC+${offset}` : `UTC${offset}`, "formatOffset");
1170
+ return {
1171
+ utcOffsetRange: `${formatOffset(minOffset)} 到 ${formatOffset(maxOffset)}`,
1172
+ regionHint: getRegionByOffset(idealOffset)
1173
+ };
1174
+ }
1175
+ __name(getSunriseTimezoneHint, "getSunriseTimezoneHint");
1082
1176
  function getRandomPromptHints() {
1083
1177
  const category = LOCATION_CATEGORIES[Math.floor(Math.random() * LOCATION_CATEGORIES.length)];
1084
1178
  const continent = CONTINENTS[Math.floor(Math.random() * CONTINENTS.length)];
1085
1179
  const hotCountries = ["法国", "日本", "意大利", "美国", "英国", "中国", "西班牙", "泰国", "澳大利亚"];
1086
1180
  const shuffled = hotCountries.sort(() => Math.random() - 0.5);
1087
1181
  const avoidCountries = shuffled.slice(0, 3 + Math.floor(Math.random() * 4)).join("、");
1088
- return { category, continent, avoidCountries };
1182
+ const sunriseHint = getSunriseTimezoneHint();
1183
+ return { category, continent, avoidCountries, sunriseHint };
1089
1184
  }
1090
1185
  __name(getRandomPromptHints, "getRandomPromptHints");
1091
1186
  var LOCATION_GENERATION_PROMPT = `你是一位资深旅行探险家,专门发掘世界各地的独特目的地。
@@ -1120,12 +1215,23 @@ async function generateLocationWithLLM(ctx, config) {
1120
1215
  return getRandomStaticLocation();
1121
1216
  }
1122
1217
  const hints = getRandomPromptHints();
1123
- const userPrompt = `请生成一个位于【${hints.continent}】的【${hints.category}】类型的旅游目的地。
1218
+ let userPrompt = `请生成一个【${hints.category}】类型的旅游目的地。
1124
1219
 
1125
- 特别要求:这次请避开 ${hints.avoidCountries} 这些热门国家,选择一个更独特、更少人知道的地方。
1220
+ 🌅 时区要求(重要):当前 UTC 时间是 ${(/* @__PURE__ */ new Date()).getUTCHours()}:${String((/* @__PURE__ */ new Date()).getUTCMinutes()).padStart(2, "0")},请选择一个正处于日出时段(当地时间约 5:00-7:00)的地点。
1221
+ 符合条件的时区范围大约是 ${hints.sunriseHint.utcOffsetRange},对应地区包括:${hints.sunriseHint.regionHint}。
1222
+
1223
+ 如果上述地区没有合适的【${hints.category}】类型目的地,可以适当放宽到邻近时区,但优先选择正在迎接日出的地方。
1224
+
1225
+ 特别要求:请避开 ${hints.avoidCountries} 这些热门国家,选择一个更独特、更少人知道的地方。要求这个地方在地理位置或文化上具有独特性。`;
1226
+ if (config.llmLocationCustomContext) {
1227
+ userPrompt += `
1228
+
1229
+ 此外,请参考以下用户提供的偏好或上下文:${config.llmLocationCustomContext}`;
1230
+ }
1231
+ userPrompt += `
1126
1232
 
1127
1233
  直接输出JSON,不要有任何其他文字。`;
1128
- if (config.debug) ctx.logger("pig").debug(`Location prompt hints: ${hints.continent}, ${hints.category}`);
1234
+ if (config.debug) ctx.logger("pig").debug(`Location prompt hints: sunrise=${hints.sunriseHint.regionHint}, category=${hints.category}`);
1129
1235
  const messages = [
1130
1236
  new import_messages.SystemMessage(LOCATION_GENERATION_PROMPT),
1131
1237
  new import_messages.HumanMessage(userPrompt)
@@ -1136,29 +1242,37 @@ async function generateLocationWithLLM(ctx, config) {
1136
1242
  const location = parseLocationResponse(content);
1137
1243
  if (location) {
1138
1244
  ctx.logger("pig").info(`LLM generated location: ${location.landmarkZh} (${location.landmark}), ${location.countryZh}`);
1139
- if (config.unsplashAccessKey) {
1245
+ if (config.unsplashAccessKey || config.pexelsApiKey) {
1246
+ const template = config.imageSearchPrompt || "{landmark} {country} landscape";
1247
+ const formatQuery = /* @__PURE__ */ __name((tmpl) => tmpl.replace("{landmark}", location.landmark).replace("{country}", location.country).replace("{city}", location.city || "").trim(), "formatQuery");
1140
1248
  const searchQueries = [
1249
+ formatQuery(template),
1250
+ // Primary: User template
1141
1251
  `${location.landmark} ${location.country}`,
1142
- // Most specific: landmark + country
1252
+ // Fallback 1: specific
1143
1253
  location.city ? `${location.city} ${location.country}` : null,
1144
- // City + country
1145
- `${location.country} landscape`,
1146
- // Country landscape
1254
+ // Fallback 2: city
1147
1255
  location.country
1148
- // Just country name
1256
+ // Fallback 3: country
1149
1257
  ].filter(Boolean);
1150
1258
  let photoUrl = null;
1151
1259
  for (const query of searchQueries) {
1152
- if (config.debug) ctx.logger("pig").debug(`Searching Unsplash for: ${query}`);
1153
- photoUrl = await searchUnsplashPhoto(ctx, config.unsplashAccessKey, query, config.debug);
1260
+ if (config.unsplashAccessKey) {
1261
+ if (config.debug) ctx.logger("pig").debug(`Searching Unsplash for: ${query}`);
1262
+ photoUrl = await searchUnsplashPhoto(ctx, config.unsplashAccessKey, query, config.debug);
1263
+ }
1264
+ if (!photoUrl && config.pexelsApiKey) {
1265
+ if (config.debug) ctx.logger("pig").debug(`Searching Pexels for: ${query}`);
1266
+ photoUrl = await searchPexelsPhoto(ctx, config.pexelsApiKey, query, config.debug);
1267
+ }
1154
1268
  if (photoUrl) {
1155
- if (config.debug) ctx.logger("pig").info(`Using Unsplash photo: ${photoUrl}`);
1269
+ ctx.logger("pig").info(`Found photo URL: ${photoUrl}`);
1156
1270
  location.landscapeUrl = photoUrl;
1157
1271
  break;
1158
1272
  }
1159
1273
  }
1160
1274
  if (!photoUrl && config.debug) {
1161
- ctx.logger("pig").debug("All Unsplash searches returned no results, using LLM-generated URL");
1275
+ ctx.logger("pig").debug("All image searches returned no results, using LLM-generated URL fallback");
1162
1276
  }
1163
1277
  }
1164
1278
  return location;
@@ -1189,7 +1303,8 @@ function parseLocationResponse(content) {
1189
1303
  landscapeUrl: data.landscapeUrl
1190
1304
  };
1191
1305
  if (!location.landscapeUrl.startsWith("http")) {
1192
- location.landscapeUrl = `https://images.unsplash.com/featured/?${encodeURIComponent(location.landmark)},${encodeURIComponent(location.country)}`;
1306
+ const query = `${encodeURIComponent(location.landmark)},${encodeURIComponent(location.country)}`;
1307
+ location.landscapeUrl = `https://images.unsplash.com/featured/?${query}`;
1193
1308
  }
1194
1309
  return location;
1195
1310
  } catch (e) {
@@ -1307,7 +1422,16 @@ var Config = import_koishi.Schema.intersect([
1307
1422
  import_koishi.Schema.object({
1308
1423
  llmLocationEnabled: import_koishi.Schema.boolean().default(false).description("启用后使用 LLM 动态生成全球旅行地点,关闭则使用预设地点库"),
1309
1424
  llmLocationModel: import_koishi.Schema.dynamic("model").description("用于生成地点的模型(推荐使用快速模型如 gemini-flash)"),
1310
- unsplashAccessKey: import_koishi.Schema.string().role("secret").default("").description("用于获取高质量风景背景图(从 unsplash.com/developers 免费申请)")
1425
+ llmLocationCustomContext: import_koishi.Schema.string().role("textarea").default("").description("自定义生成上下文(如:关注北欧神话、赛博朋克风格建筑等,留空则完全随机)"),
1426
+ imageSearchPrompt: import_koishi.Schema.string().default("{landmark} {country} landscape").description("搜图关键词模板(可用变量:{landmark} 地标英文名, {country} 国家英文名, {city} 城市英文名)"),
1427
+ unsplashAccessKey: import_koishi.Schema.string().role("secret").default("").description("Unsplash API Access Key (可选)"),
1428
+ pexelsApiKey: import_koishi.Schema.string().role("secret").default("").description("Pexels API Key (可选,作为 Unsplash 的补充)"),
1429
+ backgroundFetchMode: import_koishi.Schema.union([
1430
+ import_koishi.Schema.const("auto").description("自动:尽量内联远程图片,遇到易超时域名则直接使用 URL"),
1431
+ import_koishi.Schema.const("always").description("强制服务端拉取并内联(更稳但可能慢)"),
1432
+ import_koishi.Schema.const("never").description("不进行服务端拉取,直接使用 URL")
1433
+ ]).default("auto").description("背景图服务端拉取策略"),
1434
+ backgroundFetchTimeoutMs: import_koishi.Schema.number().default(8e3).description("背景图服务端拉取超时(毫秒)")
1311
1435
  }).description("地点与图片 🌍"),
1312
1436
  import_koishi.Schema.object({
1313
1437
  aigcEnabled: import_koishi.Schema.boolean().default(false).description("启用后使用 AI 生成小猪旅行插画(需要 media-luna 插件)"),
@@ -0,0 +1,6 @@
1
+ import { Context } from 'koishi';
2
+ /**
3
+ * Search for a photo on Pexels and return the large-sized URL
4
+ * @returns The photo URL or null if not found/error
5
+ */
6
+ export declare function searchPexelsPhoto(ctx: Context, apiKey: string, query: string, debug?: boolean): Promise<string | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-my-pig-group-friends",
3
- "version": "1.1.0",
3
+ "version": "1.1.3",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "files": [