koishi-plugin-my-pig-group-friends 1.1.1 → 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,10 +14,14 @@ 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;
21
- emojiFont: 'Noto Color Emoji' | 'Twemoji' | 'System';
22
26
  }
23
27
  export declare const Config: Schema<Config>;
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,7 +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()}日`;
180
- const emojiFont = config.emojiFont === "System" ? "" : `"${config.emojiFont}", `;
192
+ const bgCssValue = bgImage ? `url("${bgImage}")` : "none";
181
193
  const html = `
182
194
  <!DOCTYPE html>
183
195
  <html>
@@ -195,12 +207,18 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
195
207
  width: 1080px;
196
208
  height: 1920px;
197
209
  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";
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";
200
212
  background: #f0f0f2;
201
- --bg-image: url('${bgImage}');
213
+ --bg-image: ${bgCssValue};
202
214
  }
203
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
+
204
222
  .wrapper {
205
223
  position: relative;
206
224
  width: 100%;
@@ -215,10 +233,9 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
215
233
  inset: 0;
216
234
  width: 100%;
217
235
  height: 100%;
236
+ display: block;
237
+ object-fit: cover;
218
238
  background-color: #d1d1d6;
219
- background-image: var(--bg-image);
220
- background-size: cover;
221
- background-position: center;
222
239
  z-index: 0;
223
240
  }
224
241
 
@@ -376,7 +393,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
376
393
  .message-text {
377
394
  font-size: 56px;
378
395
  line-height: 1.25;
379
- font-weight: 800;
396
+ font-weight: 900;
380
397
  color: #1d1d1f;
381
398
  letter-spacing: -0.03em;
382
399
  word-break: break-word;
@@ -390,8 +407,11 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
390
407
  box-decoration-break: clone;
391
408
  -webkit-box-decoration-break: clone;
392
409
  display: inline-block;
410
+ font-weight: 900;
411
+ letter-spacing: -0.01em;
393
412
  }
394
413
 
414
+
395
415
  .divider {
396
416
  height: 2px;
397
417
  background: rgba(0,0,0,0.08);
@@ -424,14 +444,14 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
424
444
 
425
445
  .location-pill span {
426
446
  font-size: 26px;
427
- font-weight: 700;
447
+ font-weight: 800;
428
448
  color: #ffffff;
429
449
  letter-spacing: 0.02em;
430
450
  }
431
451
 
432
452
  .landmark-name {
433
453
  font-size: 38px;
434
- font-weight: 800;
454
+ font-weight: 900;
435
455
  color: #1d1d1f;
436
456
  letter-spacing: -0.01em;
437
457
  padding-left: 6px;
@@ -463,7 +483,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
463
483
  </head>
464
484
  <body>
465
485
  <div class="wrapper">
466
- <div class="bg-image"></div>
486
+ <img class="bg-image" src="${bgImage || ""}" alt="" />
467
487
  <div class="bg-overlay"></div>
468
488
 
469
489
  <div class="card-container">
@@ -482,7 +502,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
482
502
 
483
503
  <div class="message-body">
484
504
  <div class="message-text">
485
- 今天 🐷猪醒在<br/>
505
+ 今天 <span class="twemoji">🐷</span>猪醒在<br/>
486
506
  <span class="highlight">${data.location.landmarkZh || data.location.landmark}</span>
487
507
  </div>
488
508
  </div>
@@ -498,7 +518,7 @@ async function generateFootprintCard(ctx, config, data, userInfo, platform, back
498
518
  </div>
499
519
 
500
520
  <div class="brand-tag">
501
- <div class="brand-icon">🐷</div>
521
+ <div class="brand-icon"><span class="twemoji">🐷</span></div>
502
522
  <div class="brand-name">Pig Travel</div>
503
523
  </div>
504
524
  </div>
@@ -1066,6 +1086,50 @@ async function searchUnsplashPhoto(ctx, accessKey, query, debug = false) {
1066
1086
  }
1067
1087
  __name(searchUnsplashPhoto, "searchUnsplashPhoto");
1068
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
+
1069
1133
  // src/services/location.ts
1070
1134
  var LOCATION_CATEGORIES = [
1071
1135
  "自然奇观(峡谷、瀑布、火山、冰川、沙漠绿洲等)",
@@ -1080,13 +1144,43 @@ var LOCATION_CATEGORIES = [
1080
1144
  "神秘与奇特之地(地质奇观、UFO小镇、怪异地貌等)"
1081
1145
  ];
1082
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");
1083
1176
  function getRandomPromptHints() {
1084
1177
  const category = LOCATION_CATEGORIES[Math.floor(Math.random() * LOCATION_CATEGORIES.length)];
1085
1178
  const continent = CONTINENTS[Math.floor(Math.random() * CONTINENTS.length)];
1086
1179
  const hotCountries = ["法国", "日本", "意大利", "美国", "英国", "中国", "西班牙", "泰国", "澳大利亚"];
1087
1180
  const shuffled = hotCountries.sort(() => Math.random() - 0.5);
1088
1181
  const avoidCountries = shuffled.slice(0, 3 + Math.floor(Math.random() * 4)).join("、");
1089
- return { category, continent, avoidCountries };
1182
+ const sunriseHint = getSunriseTimezoneHint();
1183
+ return { category, continent, avoidCountries, sunriseHint };
1090
1184
  }
1091
1185
  __name(getRandomPromptHints, "getRandomPromptHints");
1092
1186
  var LOCATION_GENERATION_PROMPT = `你是一位资深旅行探险家,专门发掘世界各地的独特目的地。
@@ -1121,12 +1215,23 @@ async function generateLocationWithLLM(ctx, config) {
1121
1215
  return getRandomStaticLocation();
1122
1216
  }
1123
1217
  const hints = getRandomPromptHints();
1124
- const userPrompt = `请生成一个位于【${hints.continent}】的【${hints.category}】类型的旅游目的地。
1218
+ let userPrompt = `请生成一个【${hints.category}】类型的旅游目的地。
1125
1219
 
1126
- 特别要求:这次请避开 ${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 += `
1127
1232
 
1128
1233
  直接输出JSON,不要有任何其他文字。`;
1129
- 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}`);
1130
1235
  const messages = [
1131
1236
  new import_messages.SystemMessage(LOCATION_GENERATION_PROMPT),
1132
1237
  new import_messages.HumanMessage(userPrompt)
@@ -1137,29 +1242,37 @@ async function generateLocationWithLLM(ctx, config) {
1137
1242
  const location = parseLocationResponse(content);
1138
1243
  if (location) {
1139
1244
  ctx.logger("pig").info(`LLM generated location: ${location.landmarkZh} (${location.landmark}), ${location.countryZh}`);
1140
- 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");
1141
1248
  const searchQueries = [
1249
+ formatQuery(template),
1250
+ // Primary: User template
1142
1251
  `${location.landmark} ${location.country}`,
1143
- // Most specific: landmark + country
1252
+ // Fallback 1: specific
1144
1253
  location.city ? `${location.city} ${location.country}` : null,
1145
- // City + country
1146
- `${location.country} landscape`,
1147
- // Country landscape
1254
+ // Fallback 2: city
1148
1255
  location.country
1149
- // Just country name
1256
+ // Fallback 3: country
1150
1257
  ].filter(Boolean);
1151
1258
  let photoUrl = null;
1152
1259
  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);
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
+ }
1155
1268
  if (photoUrl) {
1156
- if (config.debug) ctx.logger("pig").info(`Using Unsplash photo: ${photoUrl}`);
1269
+ ctx.logger("pig").info(`Found photo URL: ${photoUrl}`);
1157
1270
  location.landscapeUrl = photoUrl;
1158
1271
  break;
1159
1272
  }
1160
1273
  }
1161
1274
  if (!photoUrl && config.debug) {
1162
- 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");
1163
1276
  }
1164
1277
  }
1165
1278
  return location;
@@ -1190,7 +1303,8 @@ function parseLocationResponse(content) {
1190
1303
  landscapeUrl: data.landscapeUrl
1191
1304
  };
1192
1305
  if (!location.landscapeUrl.startsWith("http")) {
1193
- 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}`;
1194
1308
  }
1195
1309
  return location;
1196
1310
  } catch (e) {
@@ -1303,17 +1417,21 @@ var import_koishi = require("koishi");
1303
1417
  var Config = import_koishi.Schema.intersect([
1304
1418
  import_koishi.Schema.object({
1305
1419
  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
1420
  travelMessageTemplate: import_koishi.Schema.string().default("去了 {landmark},{country}!📸").description("旅行消息模板(可用变量:{landmark} 地标名, {country} 国家名)")
1312
1421
  }).description("基础设置"),
1313
1422
  import_koishi.Schema.object({
1314
1423
  llmLocationEnabled: import_koishi.Schema.boolean().default(false).description("启用后使用 LLM 动态生成全球旅行地点,关闭则使用预设地点库"),
1315
1424
  llmLocationModel: import_koishi.Schema.dynamic("model").description("用于生成地点的模型(推荐使用快速模型如 gemini-flash)"),
1316
- 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("背景图服务端拉取超时(毫秒)")
1317
1435
  }).description("地点与图片 🌍"),
1318
1436
  import_koishi.Schema.object({
1319
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.1",
3
+ "version": "1.1.3",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "files": [