@cablate/banini-tracker 2.0.9 → 2.0.10

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/dist/cli.js CHANGED
@@ -62,6 +62,8 @@ program
62
62
  .description('抓取最新貼文(輸出 JSON 到 stdout)')
63
63
  .option('-s, --source <source>', '來源:fb', 'fb')
64
64
  .option('-n, --limit <n>', '每個來源抓幾篇', '3')
65
+ .option('--since <date>', '只抓此時間之後的貼文(YYYY-MM-DD 或 ISO 時間戳或相對時間如 "2 months")')
66
+ .option('--until <date>', '只抓此時間之前的貼文')
65
67
  .option('--no-dedup', '不做去重,抓到什麼就輸出什麼')
66
68
  .option('--mark-seen', '輸出後自動標記為已讀')
67
69
  .action(async (opts) => {
@@ -69,7 +71,8 @@ program
69
71
  const config = loadConfig();
70
72
  const limit = parseInt(opts.limit, 10);
71
73
  let posts = [];
72
- const fp = await fetchFacebookPosts(config.targets.facebookPageUrl, config.apifyToken, limit);
74
+ const fetchOpts = (opts.since || opts.until) ? { since: opts.since, until: opts.until } : undefined;
75
+ const fp = await fetchFacebookPosts(config.targets.facebookPageUrl, config.apifyToken, limit, fetchOpts);
73
76
  posts.push(...fp);
74
77
  // 按時間從新到舊
75
78
  posts.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
@@ -12,4 +12,8 @@ export interface FacebookPost {
12
12
  mediaType: string;
13
13
  mediaUrl: string;
14
14
  }
15
- export declare function fetchFacebookPosts(pageUrl: string, token: string, maxPosts?: number): Promise<FacebookPost[]>;
15
+ export interface FetchOptions {
16
+ since?: string;
17
+ until?: string;
18
+ }
19
+ export declare function fetchFacebookPosts(pageUrl: string, token: string, maxPosts?: number, options?: FetchOptions): Promise<FacebookPost[]>;
package/dist/facebook.js CHANGED
@@ -1,17 +1,22 @@
1
- export async function fetchFacebookPosts(pageUrl, token, maxPosts = 3) {
1
+ export async function fetchFacebookPosts(pageUrl, token, maxPosts = 3, options) {
2
2
  const actorId = 'apify~facebook-posts-scraper';
3
3
  const url = `https://api.apify.com/v2/acts/${actorId}/run-sync-get-dataset-items`;
4
+ const body = {
5
+ startUrls: [{ url: pageUrl }],
6
+ resultsLimit: maxPosts,
7
+ captionText: true,
8
+ };
9
+ if (options?.since)
10
+ body.onlyPostsNewerThan = options.since;
11
+ if (options?.until)
12
+ body.onlyPostsOlderThan = options.until;
4
13
  const res = await fetch(url, {
5
14
  method: 'POST',
6
15
  headers: {
7
16
  'Content-Type': 'application/json',
8
17
  Authorization: `Bearer ${token}`,
9
18
  },
10
- body: JSON.stringify({
11
- startUrls: [{ url: pageUrl }],
12
- resultsLimit: maxPosts,
13
- captionText: true,
14
- }),
19
+ body: JSON.stringify(body),
15
20
  signal: AbortSignal.timeout(180_000),
16
21
  });
17
22
  if (!res.ok) {
package/dist/index.js CHANGED
@@ -66,7 +66,8 @@ async function runInner(opts) {
66
66
  const allPosts = [];
67
67
  // 1. 抓取 Facebook(含 retry)
68
68
  try {
69
- const fbPosts = await withRetry(() => fetchFacebookPosts(FB_PAGE_URL, apifyToken, opts.maxPosts), { label: 'Facebook', maxRetries: 2, baseDelayMs: 5000 });
69
+ const fetchOpts = (opts.since || opts.until) ? { since: opts.since, until: opts.until } : undefined;
70
+ const fbPosts = await withRetry(() => fetchFacebookPosts(FB_PAGE_URL, apifyToken, opts.maxPosts, fetchOpts), { label: 'Facebook', maxRetries: 2, baseDelayMs: 5000 });
70
71
  allPosts.push(...fbPosts.map(fromFacebook));
71
72
  }
72
73
  catch (err) {
@@ -266,16 +267,42 @@ async function runInner(opts) {
266
267
  writeFileSync(outFile, JSON.stringify({ timestamp: new Date().toISOString(), posts: newPosts, analysis }, null, 2), 'utf-8');
267
268
  console.log(`結果已存檔: ${outFile}`);
268
269
  }
270
+ /**
271
+ * 產生台北時間今天指定時分的 ISO 時間戳
272
+ * 用於 Apify onlyPostsNewerThan 參數
273
+ */
274
+ function taipeiToday(hours, minutes = 0) {
275
+ const now = new Date();
276
+ const taipeiStr = now.toLocaleString('en-US', { timeZone: 'Asia/Taipei' });
277
+ const taipeiNow = new Date(taipeiStr);
278
+ taipeiNow.setHours(hours, minutes, 0, 0);
279
+ // 轉回 UTC:台北 = UTC+8
280
+ const utc = new Date(taipeiNow.getTime() - 8 * 60 * 60 * 1000);
281
+ return utc.toISOString();
282
+ }
283
+ function taipeiYesterday(hours, minutes = 0) {
284
+ const now = new Date();
285
+ const taipeiStr = now.toLocaleString('en-US', { timeZone: 'Asia/Taipei' });
286
+ const taipeiNow = new Date(taipeiStr);
287
+ taipeiNow.setDate(taipeiNow.getDate() - 1);
288
+ taipeiNow.setHours(hours, minutes, 0, 0);
289
+ const utc = new Date(taipeiNow.getTime() - 8 * 60 * 60 * 1000);
290
+ return utc.toISOString();
291
+ }
269
292
  // ── 入口 ────────────────────────────────────────────────────
270
293
  if (isCronMode) {
271
- // 盤中:週一到五 09:00-13:30,每 30 分鐘,FB only 抓 1 篇
272
- // cron 不支援半小時結束,用 9:00-13:00 30 + 13:30 單獨一個
294
+ // 早晨補漏:每天 08:00,抓前一晚 22:00 之後的貼文
295
+ cron.schedule('0 8 * * *', () => {
296
+ run({ maxPosts: 3, isDryRun: false, label: '早晨', since: taipeiYesterday(22, 0) })
297
+ .catch((err) => console.error('[早晨] 執行失敗:', err));
298
+ }, { timezone: 'Asia/Taipei' });
299
+ // 盤中:週一到五 09:00-13:30,每 30 分鐘,抓 08:30 之後的貼文
273
300
  cron.schedule('7,37 9-12 * * 1-5', () => {
274
- run({ maxPosts: 1, isDryRun: false, label: '盤中' })
301
+ run({ maxPosts: 1, isDryRun: false, label: '盤中', since: taipeiToday(8, 30) })
275
302
  .catch((err) => console.error('[盤中] 執行失敗:', err));
276
303
  }, { timezone: 'Asia/Taipei' });
277
304
  cron.schedule('7 13 * * 1-5', () => {
278
- run({ maxPosts: 1, isDryRun: false, label: '盤中' })
305
+ run({ maxPosts: 1, isDryRun: false, label: '盤中', since: taipeiToday(8, 30) })
279
306
  .catch((err) => console.error('[盤中] 執行失敗:', err));
280
307
  }, { timezone: 'Asia/Taipei' });
281
308
  // 追蹤更新:週一到五 15:00(收盤後更新預測追蹤)
@@ -283,15 +310,16 @@ if (isCronMode) {
283
310
  updateTracking()
284
311
  .catch((err) => console.error('[追蹤更新] 執行失敗:', err));
285
312
  }, { timezone: 'Asia/Taipei' });
286
- // 盤後:每天晚上 23:00,FB 3
313
+ // 盤後:每天晚上 23:03,抓 13:30 之後的貼文
287
314
  cron.schedule('3 23 * * *', () => {
288
- run({ maxPosts: 3, isDryRun: false, label: '盤後' })
315
+ run({ maxPosts: 3, isDryRun: false, label: '盤後', since: taipeiToday(13, 30) })
289
316
  .catch((err) => console.error('[盤後] 執行失敗:', err));
290
317
  }, { timezone: 'Asia/Taipei' });
291
318
  console.log('=== 巴逆逆排程已啟動 ===');
292
- console.log(' 盤中:週一~五 09:07/09:37/10:07/.../13:07(FB, 1 篇)');
319
+ console.log(' 早晨:每天 08:00(前晚 22:00 起,3 篇)');
320
+ console.log(' 盤中:週一~五 09:07/09:37/10:07/.../13:07(08:30 起,1 篇)');
293
321
  console.log(' 追蹤更新:週一~五 15:00(預測追蹤判定)');
294
- console.log(' 盤後:每天 23:03(FB, 3 篇)');
322
+ console.log(' 盤後:每天 23:03(13:30 起,3 篇)');
295
323
  console.log(' 按 Ctrl+C 停止\n');
296
324
  }
297
325
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cablate/banini-tracker",
3
- "version": "2.0.9",
3
+ "version": "2.0.10",
4
4
  "description": "巴逆逆反指標追蹤器 — 常駐排程 + CLI 雙模式",
5
5
  "type": "module",
6
6
  "bin": {