vidpipe 1.3.13 → 1.3.14
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 +8260 -8236
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +573 -641
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -446,6 +446,12 @@ function saveGlobalConfig(config2) {
|
|
|
446
446
|
chmodSync(configPath, 384);
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
|
+
function getGlobalConfigValue(section, key) {
|
|
450
|
+
const config2 = loadGlobalConfig();
|
|
451
|
+
const sectionValues = config2[section];
|
|
452
|
+
const value = sectionValues[key];
|
|
453
|
+
return typeof value === "string" ? value : void 0;
|
|
454
|
+
}
|
|
449
455
|
function setGlobalConfigValue(section, key, value) {
|
|
450
456
|
const config2 = loadGlobalConfig();
|
|
451
457
|
const sectionValues = config2[section];
|
|
@@ -4661,6 +4667,150 @@ var require_node = __commonJS({
|
|
|
4661
4667
|
}
|
|
4662
4668
|
});
|
|
4663
4669
|
|
|
4670
|
+
// src/L3-services/postStore/postStore.ts
|
|
4671
|
+
function getQueueDir() {
|
|
4672
|
+
const { OUTPUT_DIR } = getConfig();
|
|
4673
|
+
return join(OUTPUT_DIR, "publish-queue");
|
|
4674
|
+
}
|
|
4675
|
+
function getPublishedDir() {
|
|
4676
|
+
const { OUTPUT_DIR } = getConfig();
|
|
4677
|
+
return join(OUTPUT_DIR, "published");
|
|
4678
|
+
}
|
|
4679
|
+
async function readQueueItem(folderPath, id) {
|
|
4680
|
+
const metadataPath = join(folderPath, "metadata.json");
|
|
4681
|
+
const postPath = join(folderPath, "post.md");
|
|
4682
|
+
try {
|
|
4683
|
+
const metadataRaw = await readTextFile(metadataPath);
|
|
4684
|
+
const metadata = JSON.parse(metadataRaw);
|
|
4685
|
+
let postContent = "";
|
|
4686
|
+
try {
|
|
4687
|
+
postContent = await readTextFile(postPath);
|
|
4688
|
+
} catch {
|
|
4689
|
+
logger_default.debug(`No post.md found for ${String(id).replace(/[\r\n]/g, "")}`);
|
|
4690
|
+
}
|
|
4691
|
+
const videoPath = join(folderPath, "media.mp4");
|
|
4692
|
+
const imagePath = join(folderPath, "media.png");
|
|
4693
|
+
let mediaPath = null;
|
|
4694
|
+
let hasMedia = false;
|
|
4695
|
+
if (await fileExists(videoPath)) {
|
|
4696
|
+
mediaPath = videoPath;
|
|
4697
|
+
hasMedia = true;
|
|
4698
|
+
} else if (await fileExists(imagePath)) {
|
|
4699
|
+
mediaPath = imagePath;
|
|
4700
|
+
hasMedia = true;
|
|
4701
|
+
}
|
|
4702
|
+
return {
|
|
4703
|
+
id,
|
|
4704
|
+
metadata,
|
|
4705
|
+
postContent,
|
|
4706
|
+
hasMedia,
|
|
4707
|
+
mediaPath,
|
|
4708
|
+
folderPath
|
|
4709
|
+
};
|
|
4710
|
+
} catch (err) {
|
|
4711
|
+
logger_default.debug(`Failed to read queue item ${String(id).replace(/[\r\n]/g, "")}: ${String(err).replace(/[\r\n]/g, "")}`);
|
|
4712
|
+
return null;
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
async function getPendingItems() {
|
|
4716
|
+
const queueDir = getQueueDir();
|
|
4717
|
+
await ensureDirectory(queueDir);
|
|
4718
|
+
let entries;
|
|
4719
|
+
try {
|
|
4720
|
+
const dirents = await listDirectoryWithTypes(queueDir);
|
|
4721
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4722
|
+
} catch {
|
|
4723
|
+
return [];
|
|
4724
|
+
}
|
|
4725
|
+
const items = [];
|
|
4726
|
+
for (const name of entries) {
|
|
4727
|
+
const item = await readQueueItem(join(queueDir, name), name);
|
|
4728
|
+
if (item) items.push(item);
|
|
4729
|
+
}
|
|
4730
|
+
items.sort((a, b) => {
|
|
4731
|
+
if (a.hasMedia !== b.hasMedia) return a.hasMedia ? -1 : 1;
|
|
4732
|
+
return a.metadata.createdAt.localeCompare(b.metadata.createdAt);
|
|
4733
|
+
});
|
|
4734
|
+
return items;
|
|
4735
|
+
}
|
|
4736
|
+
async function createItem(id, metadata, postContent, mediaSourcePath) {
|
|
4737
|
+
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
4738
|
+
throw new Error(`Invalid ID format: ${id}`);
|
|
4739
|
+
}
|
|
4740
|
+
const folderPath = join(getQueueDir(), basename(id));
|
|
4741
|
+
await ensureDirectory(folderPath);
|
|
4742
|
+
await writeJsonFile(join(folderPath, "metadata.json"), metadata);
|
|
4743
|
+
await writeTextFile(join(folderPath, "post.md"), postContent);
|
|
4744
|
+
let hasMedia = false;
|
|
4745
|
+
const ext = mediaSourcePath ? extname(mediaSourcePath) : ".mp4";
|
|
4746
|
+
const mediaFilename = `media${ext}`;
|
|
4747
|
+
const mediaPath = join(folderPath, mediaFilename);
|
|
4748
|
+
if (mediaSourcePath) {
|
|
4749
|
+
await copyFile(mediaSourcePath, mediaPath);
|
|
4750
|
+
hasMedia = true;
|
|
4751
|
+
}
|
|
4752
|
+
logger_default.debug(`Created queue item: ${String(id).replace(/[\r\n]/g, "")}`);
|
|
4753
|
+
return {
|
|
4754
|
+
id,
|
|
4755
|
+
metadata,
|
|
4756
|
+
postContent,
|
|
4757
|
+
hasMedia,
|
|
4758
|
+
mediaPath: hasMedia ? mediaPath : null,
|
|
4759
|
+
folderPath
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4762
|
+
async function getPublishedItems() {
|
|
4763
|
+
const publishedDir = getPublishedDir();
|
|
4764
|
+
await ensureDirectory(publishedDir);
|
|
4765
|
+
let entries;
|
|
4766
|
+
try {
|
|
4767
|
+
const dirents = await listDirectoryWithTypes(publishedDir);
|
|
4768
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4769
|
+
} catch {
|
|
4770
|
+
return [];
|
|
4771
|
+
}
|
|
4772
|
+
const items = [];
|
|
4773
|
+
for (const name of entries) {
|
|
4774
|
+
const item = await readQueueItem(join(publishedDir, name), name);
|
|
4775
|
+
if (item) items.push(item);
|
|
4776
|
+
}
|
|
4777
|
+
items.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
|
|
4778
|
+
return items;
|
|
4779
|
+
}
|
|
4780
|
+
async function getScheduledItemsByIdeaIds(ideaIds) {
|
|
4781
|
+
if (ideaIds.length === 0) return [];
|
|
4782
|
+
const ideaIdSet = new Set(ideaIds);
|
|
4783
|
+
const [pendingItems, publishedItems] = await Promise.all([
|
|
4784
|
+
getPendingItems(),
|
|
4785
|
+
getPublishedItems()
|
|
4786
|
+
]);
|
|
4787
|
+
return [...pendingItems, ...publishedItems].filter(
|
|
4788
|
+
(item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
|
|
4789
|
+
);
|
|
4790
|
+
}
|
|
4791
|
+
async function itemExists(id) {
|
|
4792
|
+
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
4793
|
+
throw new Error(`Invalid ID format: ${id}`);
|
|
4794
|
+
}
|
|
4795
|
+
if (await fileExists(join(getQueueDir(), basename(id)))) {
|
|
4796
|
+
return "pending";
|
|
4797
|
+
}
|
|
4798
|
+
if (await fileExists(join(getPublishedDir(), basename(id)))) {
|
|
4799
|
+
return "published";
|
|
4800
|
+
}
|
|
4801
|
+
return null;
|
|
4802
|
+
}
|
|
4803
|
+
var init_postStore = __esm({
|
|
4804
|
+
"src/L3-services/postStore/postStore.ts"() {
|
|
4805
|
+
"use strict";
|
|
4806
|
+
init_types();
|
|
4807
|
+
init_environment();
|
|
4808
|
+
init_configLogger();
|
|
4809
|
+
init_fileSystem();
|
|
4810
|
+
init_paths();
|
|
4811
|
+
}
|
|
4812
|
+
});
|
|
4813
|
+
|
|
4664
4814
|
// src/L7-app/sdk/VidPipeSDK.ts
|
|
4665
4815
|
init_types();
|
|
4666
4816
|
init_environment();
|
|
@@ -7403,16 +7553,10 @@ var LateApiClient = class {
|
|
|
7403
7553
|
return data.accounts ?? [];
|
|
7404
7554
|
}
|
|
7405
7555
|
async getScheduledPosts(platform) {
|
|
7406
|
-
|
|
7407
|
-
if (platform) params.set("platform", platform);
|
|
7408
|
-
const data = await this.request(`/posts?${params}`);
|
|
7409
|
-
return data.posts ?? [];
|
|
7556
|
+
return this.listPosts({ status: "scheduled", platform });
|
|
7410
7557
|
}
|
|
7411
7558
|
async getDraftPosts(platform) {
|
|
7412
|
-
|
|
7413
|
-
if (platform) params.set("platform", platform);
|
|
7414
|
-
const data = await this.request(`/posts?${params}`);
|
|
7415
|
-
return data.posts ?? [];
|
|
7559
|
+
return this.listPosts({ status: "draft", platform });
|
|
7416
7560
|
}
|
|
7417
7561
|
async createPost(params) {
|
|
7418
7562
|
const data = await this.request("/posts", {
|
|
@@ -7518,6 +7662,7 @@ function createLateApiClient(...args) {
|
|
|
7518
7662
|
// src/L2-clients/scheduleStore/scheduleStore.ts
|
|
7519
7663
|
init_fileSystem();
|
|
7520
7664
|
init_paths();
|
|
7665
|
+
init_globalConfig();
|
|
7521
7666
|
async function readScheduleFile(filePath) {
|
|
7522
7667
|
return readTextFile(filePath);
|
|
7523
7668
|
}
|
|
@@ -7529,7 +7674,10 @@ async function writeScheduleFile(filePath, content) {
|
|
|
7529
7674
|
});
|
|
7530
7675
|
}
|
|
7531
7676
|
function resolveSchedulePath(configPath) {
|
|
7532
|
-
|
|
7677
|
+
if (configPath) return configPath;
|
|
7678
|
+
const globalPath = getGlobalConfigValue("defaults", "scheduleConfig");
|
|
7679
|
+
if (globalPath) return globalPath;
|
|
7680
|
+
return join(process.cwd(), "schedule.json");
|
|
7533
7681
|
}
|
|
7534
7682
|
|
|
7535
7683
|
// src/L3-services/scheduler/scheduleConfig.ts
|
|
@@ -7794,7 +7942,7 @@ function getPlatformSchedule(platform, clipType) {
|
|
|
7794
7942
|
avoidDays: sub.avoidDays
|
|
7795
7943
|
};
|
|
7796
7944
|
}
|
|
7797
|
-
if (
|
|
7945
|
+
if (schedule.slots.length === 0 && schedule.byClipType) {
|
|
7798
7946
|
const allSlots = [];
|
|
7799
7947
|
const allAvoidDays = /* @__PURE__ */ new Set();
|
|
7800
7948
|
for (const sub of Object.values(schedule.byClipType)) {
|
|
@@ -7815,194 +7963,342 @@ function getDisplacementConfig() {
|
|
|
7815
7963
|
return cachedConfig?.displacement ?? { ...defaultDisplacement };
|
|
7816
7964
|
}
|
|
7817
7965
|
|
|
7818
|
-
// src/L3-services/
|
|
7819
|
-
|
|
7820
|
-
init_environment();
|
|
7966
|
+
// src/L3-services/scheduler/realign.ts
|
|
7967
|
+
init_postStore();
|
|
7821
7968
|
init_configLogger();
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7969
|
+
|
|
7970
|
+
// src/L3-services/scheduler/scheduler.ts
|
|
7971
|
+
init_configLogger();
|
|
7972
|
+
init_postStore();
|
|
7973
|
+
var MAX_LOOKAHEAD_DAYS = 730;
|
|
7974
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
7975
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
7976
|
+
function normalizeDateTime(isoString) {
|
|
7977
|
+
return new Date(isoString).getTime();
|
|
7827
7978
|
}
|
|
7828
|
-
function
|
|
7829
|
-
|
|
7830
|
-
return join(OUTPUT_DIR, "published");
|
|
7979
|
+
function sanitizeLogValue(value) {
|
|
7980
|
+
return value.replace(/[\r\n]/g, "");
|
|
7831
7981
|
}
|
|
7832
|
-
|
|
7833
|
-
const
|
|
7834
|
-
|
|
7982
|
+
function getTimezoneOffset(timezone, date) {
|
|
7983
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7984
|
+
timeZone: timezone,
|
|
7985
|
+
timeZoneName: "longOffset"
|
|
7986
|
+
});
|
|
7987
|
+
const parts = formatter.formatToParts(date);
|
|
7988
|
+
const tzPart = parts.find((part) => part.type === "timeZoneName");
|
|
7989
|
+
const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
|
|
7990
|
+
if (match) return match[1];
|
|
7991
|
+
if (tzPart?.value === "GMT") return "+00:00";
|
|
7992
|
+
logger_default.warn(
|
|
7993
|
+
`Could not parse timezone offset for timezone "${timezone}" on date "${date.toISOString()}". Raw timeZoneName part: "${tzPart?.value ?? "undefined"}". Falling back to UTC (+00:00).`
|
|
7994
|
+
);
|
|
7995
|
+
return "+00:00";
|
|
7996
|
+
}
|
|
7997
|
+
function buildSlotDatetime(date, time, timezone) {
|
|
7998
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7999
|
+
timeZone: timezone,
|
|
8000
|
+
year: "numeric",
|
|
8001
|
+
month: "2-digit",
|
|
8002
|
+
day: "2-digit"
|
|
8003
|
+
});
|
|
8004
|
+
const parts = formatter.formatToParts(date);
|
|
8005
|
+
const yearPart = parts.find((part) => part.type === "year")?.value;
|
|
8006
|
+
const monthPart = parts.find((part) => part.type === "month")?.value;
|
|
8007
|
+
const dayPart = parts.find((part) => part.type === "day")?.value;
|
|
8008
|
+
const year = yearPart ?? String(date.getFullYear());
|
|
8009
|
+
const month = (monthPart ?? String(date.getMonth() + 1)).padStart(2, "0");
|
|
8010
|
+
const day = (dayPart ?? String(date.getDate())).padStart(2, "0");
|
|
8011
|
+
const offset = getTimezoneOffset(timezone, date);
|
|
8012
|
+
return `${year}-${month}-${day}T${time}:00${offset}`;
|
|
8013
|
+
}
|
|
8014
|
+
function getDayOfWeekInTimezone(date, timezone) {
|
|
8015
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8016
|
+
timeZone: timezone,
|
|
8017
|
+
weekday: "short"
|
|
8018
|
+
});
|
|
8019
|
+
const short = formatter.format(date).toLowerCase().slice(0, 3);
|
|
8020
|
+
const map = {
|
|
8021
|
+
sun: "sun",
|
|
8022
|
+
mon: "mon",
|
|
8023
|
+
tue: "tue",
|
|
8024
|
+
wed: "wed",
|
|
8025
|
+
thu: "thu",
|
|
8026
|
+
fri: "fri",
|
|
8027
|
+
sat: "sat"
|
|
8028
|
+
};
|
|
8029
|
+
return map[short] ?? "mon";
|
|
8030
|
+
}
|
|
8031
|
+
async function fetchScheduledPostsSafe(platform) {
|
|
7835
8032
|
try {
|
|
7836
|
-
const
|
|
7837
|
-
|
|
7838
|
-
let postContent = "";
|
|
7839
|
-
try {
|
|
7840
|
-
postContent = await readTextFile(postPath);
|
|
7841
|
-
} catch {
|
|
7842
|
-
logger_default.debug(`No post.md found for ${String(id).replace(/[\r\n]/g, "")}`);
|
|
7843
|
-
}
|
|
7844
|
-
const videoPath = join(folderPath, "media.mp4");
|
|
7845
|
-
const imagePath = join(folderPath, "media.png");
|
|
7846
|
-
let mediaPath = null;
|
|
7847
|
-
let hasMedia = false;
|
|
7848
|
-
if (await fileExists(videoPath)) {
|
|
7849
|
-
mediaPath = videoPath;
|
|
7850
|
-
hasMedia = true;
|
|
7851
|
-
} else if (await fileExists(imagePath)) {
|
|
7852
|
-
mediaPath = imagePath;
|
|
7853
|
-
hasMedia = true;
|
|
7854
|
-
}
|
|
7855
|
-
return {
|
|
7856
|
-
id,
|
|
7857
|
-
metadata,
|
|
7858
|
-
postContent,
|
|
7859
|
-
hasMedia,
|
|
7860
|
-
mediaPath,
|
|
7861
|
-
folderPath
|
|
7862
|
-
};
|
|
8033
|
+
const client = new LateApiClient();
|
|
8034
|
+
return await client.getScheduledPosts(platform);
|
|
7863
8035
|
} catch (err) {
|
|
7864
|
-
|
|
7865
|
-
|
|
8036
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8037
|
+
logger_default.warn(`Late API unreachable, using local data only: ${msg}`);
|
|
8038
|
+
return [];
|
|
7866
8039
|
}
|
|
7867
8040
|
}
|
|
7868
|
-
async function
|
|
7869
|
-
const
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
const items = [];
|
|
7879
|
-
for (const name of entries) {
|
|
7880
|
-
const item = await readQueueItem(join(queueDir, name), name);
|
|
7881
|
-
if (item) items.push(item);
|
|
8041
|
+
async function buildBookedMap(platform) {
|
|
8042
|
+
const [latePosts, publishedItems] = await Promise.all([
|
|
8043
|
+
fetchScheduledPostsSafe(platform),
|
|
8044
|
+
getPublishedItems()
|
|
8045
|
+
]);
|
|
8046
|
+
const ideaLinkedPostIds = /* @__PURE__ */ new Set();
|
|
8047
|
+
for (const item of publishedItems) {
|
|
8048
|
+
if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
|
|
8049
|
+
ideaLinkedPostIds.add(item.metadata.latePostId);
|
|
8050
|
+
}
|
|
7882
8051
|
}
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
8052
|
+
const map = /* @__PURE__ */ new Map();
|
|
8053
|
+
for (const post of latePosts) {
|
|
8054
|
+
if (!post.scheduledFor) continue;
|
|
8055
|
+
for (const scheduledPlatform of post.platforms) {
|
|
8056
|
+
if (!platform || scheduledPlatform.platform === platform) {
|
|
8057
|
+
const ms = normalizeDateTime(post.scheduledFor);
|
|
8058
|
+
map.set(ms, {
|
|
8059
|
+
scheduledFor: post.scheduledFor,
|
|
8060
|
+
source: "late",
|
|
8061
|
+
postId: post._id,
|
|
8062
|
+
platform: scheduledPlatform.platform,
|
|
8063
|
+
status: post.status,
|
|
8064
|
+
ideaLinked: ideaLinkedPostIds.has(post._id)
|
|
8065
|
+
});
|
|
8066
|
+
}
|
|
8067
|
+
}
|
|
7892
8068
|
}
|
|
7893
|
-
const
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
8069
|
+
for (const item of publishedItems) {
|
|
8070
|
+
if (platform && item.metadata.platform !== platform) continue;
|
|
8071
|
+
if (!item.metadata.scheduledFor) continue;
|
|
8072
|
+
const ms = normalizeDateTime(item.metadata.scheduledFor);
|
|
8073
|
+
if (!map.has(ms)) {
|
|
8074
|
+
map.set(ms, {
|
|
8075
|
+
scheduledFor: item.metadata.scheduledFor,
|
|
8076
|
+
source: "local",
|
|
8077
|
+
itemId: item.id,
|
|
8078
|
+
platform: item.metadata.platform,
|
|
8079
|
+
ideaLinked: Boolean(item.metadata.ideaIds?.length)
|
|
8080
|
+
});
|
|
8081
|
+
}
|
|
7904
8082
|
}
|
|
7905
|
-
|
|
7906
|
-
return {
|
|
7907
|
-
id,
|
|
7908
|
-
metadata,
|
|
7909
|
-
postContent,
|
|
7910
|
-
hasMedia,
|
|
7911
|
-
mediaPath: hasMedia ? mediaPath : null,
|
|
7912
|
-
folderPath
|
|
7913
|
-
};
|
|
8083
|
+
return map;
|
|
7914
8084
|
}
|
|
7915
|
-
async function
|
|
7916
|
-
const
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
} catch {
|
|
7923
|
-
return [];
|
|
8085
|
+
async function getIdeaLinkedLatePostIds() {
|
|
8086
|
+
const publishedItems = await getPublishedItems();
|
|
8087
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8088
|
+
for (const item of publishedItems) {
|
|
8089
|
+
if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
|
|
8090
|
+
ids.add(item.metadata.latePostId);
|
|
8091
|
+
}
|
|
7924
8092
|
}
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
8093
|
+
return ids;
|
|
8094
|
+
}
|
|
8095
|
+
function* generateTimeslots(platformConfig, timezone, fromMs, maxMs) {
|
|
8096
|
+
const baseDate = new Date(fromMs);
|
|
8097
|
+
const upperMs = maxMs ?? fromMs + MAX_LOOKAHEAD_DAYS * DAY_MS;
|
|
8098
|
+
for (let dayOffset = 0; dayOffset <= MAX_LOOKAHEAD_DAYS; dayOffset++) {
|
|
8099
|
+
const day = new Date(baseDate);
|
|
8100
|
+
day.setDate(day.getDate() + dayOffset);
|
|
8101
|
+
const dayOfWeek = getDayOfWeekInTimezone(day, timezone);
|
|
8102
|
+
if (platformConfig.avoidDays.includes(dayOfWeek)) continue;
|
|
8103
|
+
const dayCandidates = [];
|
|
8104
|
+
for (const slot of platformConfig.slots) {
|
|
8105
|
+
if (!slot.days.includes(dayOfWeek)) continue;
|
|
8106
|
+
const datetime = buildSlotDatetime(day, slot.time, timezone);
|
|
8107
|
+
const ms = normalizeDateTime(datetime);
|
|
8108
|
+
if (ms <= fromMs) continue;
|
|
8109
|
+
if (ms > upperMs) continue;
|
|
8110
|
+
dayCandidates.push({ datetime, ms });
|
|
8111
|
+
}
|
|
8112
|
+
dayCandidates.sort((a, b) => a.ms - b.ms);
|
|
8113
|
+
for (const candidate of dayCandidates) yield candidate;
|
|
8114
|
+
if (dayCandidates.length === 0) {
|
|
8115
|
+
const dayStartMs = normalizeDateTime(buildSlotDatetime(day, "00:00", timezone));
|
|
8116
|
+
if (dayStartMs > upperMs) break;
|
|
8117
|
+
}
|
|
7929
8118
|
}
|
|
7930
|
-
items.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
|
|
7931
|
-
return items;
|
|
7932
8119
|
}
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
return [...pendingItems, ...publishedItems].filter(
|
|
7941
|
-
(item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
|
|
7942
|
-
);
|
|
8120
|
+
function passesIdeaSpacing(candidateMs, candidatePlatform, ideaRefs, samePlatformMs, crossPlatformMs) {
|
|
8121
|
+
for (const ref of ideaRefs) {
|
|
8122
|
+
const diff = Math.abs(candidateMs - ref.scheduledForMs);
|
|
8123
|
+
if (ref.platform === candidatePlatform && diff < samePlatformMs) return false;
|
|
8124
|
+
if (diff < crossPlatformMs) return false;
|
|
8125
|
+
}
|
|
8126
|
+
return true;
|
|
7943
8127
|
}
|
|
7944
|
-
async function
|
|
7945
|
-
const
|
|
7946
|
-
|
|
8128
|
+
async function getIdeaReferences(ideaIds, bookedMap) {
|
|
8129
|
+
const sameIdeaPosts = await getScheduledItemsByIdeaIds(ideaIds);
|
|
8130
|
+
const lateSlotsByPostId = /* @__PURE__ */ new Map();
|
|
8131
|
+
const localSlotsByItemId = /* @__PURE__ */ new Map();
|
|
8132
|
+
for (const slot of bookedMap.values()) {
|
|
8133
|
+
if (slot.postId) {
|
|
8134
|
+
const arr = lateSlotsByPostId.get(slot.postId) ?? [];
|
|
8135
|
+
arr.push(slot);
|
|
8136
|
+
lateSlotsByPostId.set(slot.postId, arr);
|
|
8137
|
+
}
|
|
8138
|
+
if (slot.itemId) {
|
|
8139
|
+
const arr = localSlotsByItemId.get(slot.itemId) ?? [];
|
|
8140
|
+
arr.push(slot);
|
|
8141
|
+
localSlotsByItemId.set(slot.itemId, arr);
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8144
|
+
const refs = [];
|
|
8145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8146
|
+
const addRef = (platform, scheduledFor) => {
|
|
8147
|
+
if (!scheduledFor) return;
|
|
8148
|
+
const key = `${platform}@${scheduledFor}`;
|
|
8149
|
+
if (seen.has(key)) return;
|
|
8150
|
+
seen.add(key);
|
|
8151
|
+
refs.push({ platform, scheduledForMs: normalizeDateTime(scheduledFor) });
|
|
8152
|
+
};
|
|
8153
|
+
for (const item of sameIdeaPosts) {
|
|
8154
|
+
addRef(item.metadata.platform, item.metadata.scheduledFor);
|
|
8155
|
+
if (item.metadata.latePostId) {
|
|
8156
|
+
for (const slot of lateSlotsByPostId.get(item.metadata.latePostId) ?? []) {
|
|
8157
|
+
addRef(slot.platform, slot.scheduledFor);
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
for (const slot of localSlotsByItemId.get(item.id) ?? []) {
|
|
8161
|
+
addRef(slot.platform, slot.scheduledFor);
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
return refs;
|
|
8165
|
+
}
|
|
8166
|
+
async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
|
|
8167
|
+
const indent = " ".repeat(ctx.depth);
|
|
8168
|
+
let checked = 0;
|
|
8169
|
+
let skippedBooked = 0;
|
|
8170
|
+
let skippedSpacing = 0;
|
|
8171
|
+
logger_default.debug(`${indent}[schedulePost] Looking for slot for ${label} (idea=${isIdeaPost}) from ${new Date(fromMs).toISOString()}`);
|
|
8172
|
+
for (const { datetime, ms } of generateTimeslots(platformConfig, ctx.timezone, fromMs)) {
|
|
8173
|
+
checked++;
|
|
8174
|
+
const booked = ctx.bookedMap.get(ms);
|
|
8175
|
+
if (!booked) {
|
|
8176
|
+
if (isIdeaPost && ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
|
|
8177
|
+
skippedSpacing++;
|
|
8178
|
+
if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
|
|
8179
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping`);
|
|
8180
|
+
}
|
|
8181
|
+
continue;
|
|
8182
|
+
}
|
|
8183
|
+
logger_default.debug(`${indent}[schedulePost] \u2705 Found empty slot: ${datetime} (checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing)`);
|
|
8184
|
+
return datetime;
|
|
8185
|
+
}
|
|
8186
|
+
if (isIdeaPost && ctx.displacementEnabled && !booked.ideaLinked && booked.source === "late" && booked.postId) {
|
|
8187
|
+
if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
|
|
8188
|
+
skippedSpacing++;
|
|
8189
|
+
if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
|
|
8190
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping (even though displaceable)`);
|
|
8191
|
+
}
|
|
8192
|
+
continue;
|
|
8193
|
+
}
|
|
8194
|
+
logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by non-idea post ${booked.postId} \u2014 displacing`);
|
|
8195
|
+
const newHome = await schedulePost(
|
|
8196
|
+
platformConfig,
|
|
8197
|
+
ms,
|
|
8198
|
+
false,
|
|
8199
|
+
`displaced:${booked.postId}`,
|
|
8200
|
+
{ ...ctx, depth: ctx.depth + 1 }
|
|
8201
|
+
);
|
|
8202
|
+
if (newHome) {
|
|
8203
|
+
if (!ctx.dryRun) {
|
|
8204
|
+
try {
|
|
8205
|
+
await ctx.lateClient.schedulePost(booked.postId, newHome);
|
|
8206
|
+
} catch (err) {
|
|
8207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8208
|
+
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Failed to displace ${booked.postId} via Late API: ${msg} \u2014 skipping slot`);
|
|
8209
|
+
continue;
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
logger_default.info(`${indent}[schedulePost] \u{1F4E6} Displaced ${booked.postId}: ${datetime} \u2192 ${newHome}`);
|
|
8213
|
+
ctx.bookedMap.delete(ms);
|
|
8214
|
+
const newMs = normalizeDateTime(newHome);
|
|
8215
|
+
ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
|
|
8216
|
+
logger_default.debug(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked} candidates)`);
|
|
8217
|
+
return datetime;
|
|
8218
|
+
}
|
|
8219
|
+
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace ${booked.postId} \u2014 no empty slot found after ${datetime}`);
|
|
8220
|
+
}
|
|
8221
|
+
if (booked.ideaLinked) {
|
|
8222
|
+
skippedBooked++;
|
|
8223
|
+
if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
|
|
8224
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken by idea post ${booked.postId ?? booked.itemId} \u2014 skipping`);
|
|
8225
|
+
}
|
|
8226
|
+
continue;
|
|
8227
|
+
}
|
|
8228
|
+
skippedBooked++;
|
|
8229
|
+
if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
|
|
8230
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken (${booked.source}/${booked.postId ?? booked.itemId}) \u2014 skipping`);
|
|
8231
|
+
}
|
|
8232
|
+
}
|
|
8233
|
+
logger_default.warn(`[schedulePost] \u274C No slot found for ${label} \u2014 checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing`);
|
|
8234
|
+
return null;
|
|
7947
8235
|
}
|
|
7948
|
-
async function
|
|
7949
|
-
|
|
7950
|
-
|
|
8236
|
+
async function findNextSlot(platform, clipType, options) {
|
|
8237
|
+
const config2 = await loadScheduleConfig();
|
|
8238
|
+
const platformConfig = getPlatformSchedule(platform, clipType);
|
|
8239
|
+
if (!platformConfig) {
|
|
8240
|
+
logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
|
|
8241
|
+
return null;
|
|
7951
8242
|
}
|
|
7952
|
-
|
|
7953
|
-
|
|
8243
|
+
const { timezone } = config2;
|
|
8244
|
+
const nowMs = Date.now();
|
|
8245
|
+
const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
|
|
8246
|
+
const isIdeaAware = ideaIds.length > 0;
|
|
8247
|
+
const bookedMap = await buildBookedMap(platform);
|
|
8248
|
+
const ideaLinkedPostIds = await getIdeaLinkedLatePostIds();
|
|
8249
|
+
const label = `${platform}/${clipType ?? "default"}`;
|
|
8250
|
+
let ideaRefs = [];
|
|
8251
|
+
let samePlatformMs = 0;
|
|
8252
|
+
let crossPlatformMs = 0;
|
|
8253
|
+
if (isIdeaAware) {
|
|
8254
|
+
const allBookedMap = await buildBookedMap();
|
|
8255
|
+
ideaRefs = await getIdeaReferences(ideaIds, allBookedMap);
|
|
8256
|
+
const spacingConfig = getIdeaSpacingConfig();
|
|
8257
|
+
samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
|
|
8258
|
+
crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
|
|
8259
|
+
}
|
|
8260
|
+
logger_default.info(`[findNextSlot] Scheduling ${label} (idea=${isIdeaAware}, booked=${bookedMap.size} slots, spacingRefs=${ideaRefs.length})`);
|
|
8261
|
+
const ctx = {
|
|
8262
|
+
timezone,
|
|
8263
|
+
bookedMap,
|
|
8264
|
+
ideaLinkedPostIds,
|
|
8265
|
+
lateClient: new LateApiClient(),
|
|
8266
|
+
displacementEnabled: getDisplacementConfig().enabled,
|
|
8267
|
+
dryRun: false,
|
|
8268
|
+
depth: 0,
|
|
8269
|
+
ideaRefs,
|
|
8270
|
+
samePlatformMs,
|
|
8271
|
+
crossPlatformMs,
|
|
8272
|
+
platform
|
|
8273
|
+
};
|
|
8274
|
+
const result = await schedulePost(platformConfig, nowMs, isIdeaAware, label, ctx);
|
|
8275
|
+
if (!result) {
|
|
8276
|
+
logger_default.warn(`[findNextSlot] No available slot for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
|
|
7954
8277
|
}
|
|
7955
|
-
|
|
7956
|
-
|
|
8278
|
+
return result;
|
|
8279
|
+
}
|
|
8280
|
+
async function getScheduleCalendar(startDate, endDate) {
|
|
8281
|
+
const bookedMap = await buildBookedMap();
|
|
8282
|
+
let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
|
|
8283
|
+
platform: slot.platform,
|
|
8284
|
+
scheduledFor: slot.scheduledFor,
|
|
8285
|
+
source: slot.source,
|
|
8286
|
+
postId: slot.postId,
|
|
8287
|
+
itemId: slot.itemId
|
|
8288
|
+
}));
|
|
8289
|
+
if (startDate) {
|
|
8290
|
+
const startMs = startDate.getTime();
|
|
8291
|
+
filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) >= startMs);
|
|
7957
8292
|
}
|
|
7958
|
-
|
|
8293
|
+
if (endDate) {
|
|
8294
|
+
const endMs = endDate.getTime();
|
|
8295
|
+
filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) <= endMs);
|
|
8296
|
+
}
|
|
8297
|
+
filtered.sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
|
|
8298
|
+
return filtered;
|
|
7959
8299
|
}
|
|
7960
8300
|
|
|
7961
8301
|
// src/L3-services/scheduler/realign.ts
|
|
7962
|
-
init_configLogger();
|
|
7963
|
-
function getTimezoneOffset(timezone, date) {
|
|
7964
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7965
|
-
timeZone: timezone,
|
|
7966
|
-
timeZoneName: "longOffset"
|
|
7967
|
-
});
|
|
7968
|
-
const parts = formatter.formatToParts(date);
|
|
7969
|
-
const tzPart = parts.find((p) => p.type === "timeZoneName");
|
|
7970
|
-
const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
|
|
7971
|
-
if (match) return match[1];
|
|
7972
|
-
if (tzPart?.value === "GMT") return "+00:00";
|
|
7973
|
-
return "+00:00";
|
|
7974
|
-
}
|
|
7975
|
-
function buildSlotDatetime(date, time, timezone) {
|
|
7976
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7977
|
-
timeZone: timezone,
|
|
7978
|
-
year: "numeric",
|
|
7979
|
-
month: "2-digit",
|
|
7980
|
-
day: "2-digit"
|
|
7981
|
-
});
|
|
7982
|
-
const parts = formatter.formatToParts(date);
|
|
7983
|
-
const year = parts.find((p) => p.type === "year")?.value ?? String(date.getFullYear());
|
|
7984
|
-
const month = (parts.find((p) => p.type === "month")?.value ?? String(date.getMonth() + 1)).padStart(2, "0");
|
|
7985
|
-
const day = (parts.find((p) => p.type === "day")?.value ?? String(date.getDate())).padStart(2, "0");
|
|
7986
|
-
const offset = getTimezoneOffset(timezone, date);
|
|
7987
|
-
return `${year}-${month}-${day}T${time}:00${offset}`;
|
|
7988
|
-
}
|
|
7989
|
-
function getDayOfWeekInTimezone(date, timezone) {
|
|
7990
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7991
|
-
timeZone: timezone,
|
|
7992
|
-
weekday: "short"
|
|
7993
|
-
});
|
|
7994
|
-
const short = formatter.format(date).toLowerCase().slice(0, 3);
|
|
7995
|
-
const map = {
|
|
7996
|
-
sun: "sun",
|
|
7997
|
-
mon: "mon",
|
|
7998
|
-
tue: "tue",
|
|
7999
|
-
wed: "wed",
|
|
8000
|
-
thu: "thu",
|
|
8001
|
-
fri: "fri",
|
|
8002
|
-
sat: "sat"
|
|
8003
|
-
};
|
|
8004
|
-
return map[short] ?? "mon";
|
|
8005
|
-
}
|
|
8006
8302
|
var PLATFORM_ALIASES2 = { twitter: "x" };
|
|
8007
8303
|
function normalizeSchedulePlatform(platform) {
|
|
8008
8304
|
return PLATFORM_ALIASES2[platform] ?? platform;
|
|
@@ -8035,33 +8331,22 @@ async function fetchAllPosts(client, statuses, platform) {
|
|
|
8035
8331
|
}
|
|
8036
8332
|
return allPosts;
|
|
8037
8333
|
}
|
|
8038
|
-
function
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
if (!slot.days.includes(dayOfWeek)) continue;
|
|
8055
|
-
const iso = buildSlotDatetime(day, slot.time, timezone);
|
|
8056
|
-
const ms = new Date(iso).getTime();
|
|
8057
|
-
if (ms <= nowMs) continue;
|
|
8058
|
-
if (!bookedMs.has(ms)) {
|
|
8059
|
-
available.push(iso);
|
|
8060
|
-
bookedMs.add(ms);
|
|
8061
|
-
}
|
|
8062
|
-
}
|
|
8063
|
-
}
|
|
8064
|
-
return available;
|
|
8334
|
+
function isOnValidSlot(iso, schedule, timezone) {
|
|
8335
|
+
if (schedule.slots.length === 0) return false;
|
|
8336
|
+
const date = new Date(iso);
|
|
8337
|
+
const dayOfWeek = getDayOfWeekInTimezone(date, timezone);
|
|
8338
|
+
if (schedule.avoidDays.includes(dayOfWeek)) return false;
|
|
8339
|
+
const timeFormatter = new Intl.DateTimeFormat("en-US", {
|
|
8340
|
+
timeZone: timezone,
|
|
8341
|
+
hour: "2-digit",
|
|
8342
|
+
minute: "2-digit",
|
|
8343
|
+
hour12: false
|
|
8344
|
+
});
|
|
8345
|
+
const timeParts = timeFormatter.formatToParts(date);
|
|
8346
|
+
const hour = timeParts.find((p) => p.type === "hour")?.value ?? "00";
|
|
8347
|
+
const minute = timeParts.find((p) => p.type === "minute")?.value ?? "00";
|
|
8348
|
+
const timeKey = `${hour}:${minute}`;
|
|
8349
|
+
return schedule.slots.some((slot) => slot.time === timeKey && slot.days.includes(dayOfWeek));
|
|
8065
8350
|
}
|
|
8066
8351
|
async function buildRealignPlan(options = {}) {
|
|
8067
8352
|
const config2 = await loadScheduleConfig();
|
|
@@ -8073,9 +8358,8 @@ async function buildRealignPlan(options = {}) {
|
|
|
8073
8358
|
return { posts: [], toCancel: [], skipped: 0, unmatched: 0, totalFetched: 0 };
|
|
8074
8359
|
}
|
|
8075
8360
|
const { byLatePostId, byContent } = options.clipTypeMaps ?? await buildClipTypeMaps();
|
|
8076
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
8077
8361
|
let unmatched = 0;
|
|
8078
|
-
|
|
8362
|
+
const tagged = [];
|
|
8079
8363
|
for (const post of allPosts) {
|
|
8080
8364
|
const platform = post.platforms[0]?.platform;
|
|
8081
8365
|
if (!platform) continue;
|
|
@@ -8083,74 +8367,90 @@ async function buildRealignPlan(options = {}) {
|
|
|
8083
8367
|
if (!clipType && post.content) {
|
|
8084
8368
|
const contentKey = `${platform}::${normalizeContent(post.content)}`;
|
|
8085
8369
|
clipType = byContent.get(contentKey) ?? null;
|
|
8086
|
-
if (clipType) contentMatched++;
|
|
8087
8370
|
}
|
|
8088
8371
|
if (!clipType) {
|
|
8089
8372
|
clipType = "short";
|
|
8090
8373
|
unmatched++;
|
|
8091
8374
|
}
|
|
8092
|
-
|
|
8093
|
-
if (!grouped.has(key)) grouped.set(key, []);
|
|
8094
|
-
grouped.get(key).push({ post, platform, clipType });
|
|
8375
|
+
tagged.push({ post, platform, clipType });
|
|
8095
8376
|
}
|
|
8096
|
-
const
|
|
8097
|
-
|
|
8098
|
-
|
|
8377
|
+
const bookedMap = await buildBookedMap();
|
|
8378
|
+
const ctx = {
|
|
8379
|
+
timezone,
|
|
8380
|
+
bookedMap,
|
|
8381
|
+
ideaLinkedPostIds: /* @__PURE__ */ new Set(),
|
|
8382
|
+
lateClient: client,
|
|
8383
|
+
displacementEnabled: getDisplacementConfig().enabled,
|
|
8384
|
+
dryRun: true,
|
|
8385
|
+
depth: 0,
|
|
8386
|
+
ideaRefs: [],
|
|
8387
|
+
samePlatformMs: 0,
|
|
8388
|
+
crossPlatformMs: 0,
|
|
8389
|
+
platform: ""
|
|
8390
|
+
};
|
|
8391
|
+
for (const [, slot] of bookedMap) {
|
|
8392
|
+
if (slot.ideaLinked && slot.postId) {
|
|
8393
|
+
ctx.ideaLinkedPostIds.add(slot.postId);
|
|
8394
|
+
}
|
|
8099
8395
|
}
|
|
8100
8396
|
const result = [];
|
|
8101
8397
|
const toCancel = [];
|
|
8102
8398
|
let skipped = 0;
|
|
8103
|
-
|
|
8104
|
-
const
|
|
8399
|
+
tagged.sort((a, b) => {
|
|
8400
|
+
const aIdea = ctx.ideaLinkedPostIds.has(a.post._id) ? 0 : 1;
|
|
8401
|
+
const bIdea = ctx.ideaLinkedPostIds.has(b.post._id) ? 0 : 1;
|
|
8402
|
+
return aIdea - bIdea;
|
|
8403
|
+
});
|
|
8404
|
+
const nowMs = Date.now();
|
|
8405
|
+
for (const { post, platform, clipType } of tagged) {
|
|
8105
8406
|
const schedulePlatform = normalizeSchedulePlatform(platform);
|
|
8106
|
-
const
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
if (post.status === "cancelled") continue;
|
|
8111
|
-
toCancel.push({
|
|
8112
|
-
post,
|
|
8113
|
-
platform,
|
|
8114
|
-
clipType: ct,
|
|
8115
|
-
reason: `No schedule slots for ${schedulePlatform}/${clipType}`
|
|
8116
|
-
});
|
|
8407
|
+
const platformConfig = getPlatformSchedule(schedulePlatform, clipType);
|
|
8408
|
+
if (!platformConfig || platformConfig.slots.length === 0) {
|
|
8409
|
+
if (post.status !== "cancelled") {
|
|
8410
|
+
toCancel.push({ post, platform, clipType, reason: `No schedule slots for ${schedulePlatform}/${clipType}` });
|
|
8117
8411
|
}
|
|
8118
8412
|
continue;
|
|
8119
8413
|
}
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
if (!newSlot) {
|
|
8130
|
-
if (post.status !== "cancelled") {
|
|
8131
|
-
toCancel.push({
|
|
8132
|
-
post,
|
|
8133
|
-
platform,
|
|
8134
|
-
clipType: posts[i].clipType,
|
|
8135
|
-
reason: `No more available slots for ${schedulePlatform}/${clipType}`
|
|
8136
|
-
});
|
|
8137
|
-
}
|
|
8138
|
-
continue;
|
|
8414
|
+
if (post.scheduledFor && post.status === "scheduled" && isOnValidSlot(post.scheduledFor, platformConfig, timezone)) {
|
|
8415
|
+
skipped++;
|
|
8416
|
+
continue;
|
|
8417
|
+
}
|
|
8418
|
+
if (post.scheduledFor) {
|
|
8419
|
+
const currentMs2 = new Date(post.scheduledFor).getTime();
|
|
8420
|
+
const currentBooked = bookedMap.get(currentMs2);
|
|
8421
|
+
if (currentBooked?.postId === post._id) {
|
|
8422
|
+
bookedMap.delete(currentMs2);
|
|
8139
8423
|
}
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8424
|
+
}
|
|
8425
|
+
const isIdea = ctx.ideaLinkedPostIds.has(post._id);
|
|
8426
|
+
const label = `${schedulePlatform}/${clipType}:${post._id.slice(-6)}`;
|
|
8427
|
+
const newSlot = await schedulePost(platformConfig, nowMs, isIdea, label, ctx);
|
|
8428
|
+
if (!newSlot) {
|
|
8429
|
+
if (post.status !== "cancelled") {
|
|
8430
|
+
toCancel.push({ post, platform, clipType, reason: `No available slot for ${schedulePlatform}/${clipType}` });
|
|
8145
8431
|
}
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8432
|
+
continue;
|
|
8433
|
+
}
|
|
8434
|
+
const newMs = new Date(newSlot).getTime();
|
|
8435
|
+
ctx.bookedMap.set(newMs, {
|
|
8436
|
+
scheduledFor: newSlot,
|
|
8437
|
+
source: "late",
|
|
8438
|
+
postId: post._id,
|
|
8439
|
+
platform: schedulePlatform,
|
|
8440
|
+
ideaLinked: isIdea
|
|
8441
|
+
});
|
|
8442
|
+
const currentMs = post.scheduledFor ? new Date(post.scheduledFor).getTime() : 0;
|
|
8443
|
+
if (currentMs === newMs && post.status === "scheduled") {
|
|
8444
|
+
skipped++;
|
|
8445
|
+
continue;
|
|
8153
8446
|
}
|
|
8447
|
+
result.push({
|
|
8448
|
+
post,
|
|
8449
|
+
platform,
|
|
8450
|
+
clipType,
|
|
8451
|
+
oldScheduledFor: post.scheduledFor ?? null,
|
|
8452
|
+
newScheduledFor: newSlot
|
|
8453
|
+
});
|
|
8154
8454
|
}
|
|
8155
8455
|
result.sort((a, b) => new Date(a.newScheduledFor).getTime() - new Date(b.newScheduledFor).getTime());
|
|
8156
8456
|
return { posts: result, toCancel, skipped, unmatched, totalFetched: allPosts.length };
|
|
@@ -8198,381 +8498,6 @@ async function executeRealignPlan(plan, onProgress) {
|
|
|
8198
8498
|
return { updated, cancelled, failed, errors };
|
|
8199
8499
|
}
|
|
8200
8500
|
|
|
8201
|
-
// src/L3-services/scheduler/scheduler.ts
|
|
8202
|
-
init_configLogger();
|
|
8203
|
-
function normalizeDateTime(isoString) {
|
|
8204
|
-
return new Date(isoString).getTime();
|
|
8205
|
-
}
|
|
8206
|
-
var CHUNK_DAYS = 14;
|
|
8207
|
-
var MAX_LOOKAHEAD_DAYS = 730;
|
|
8208
|
-
var DEFAULT_IDEA_WINDOW_DAYS = 14;
|
|
8209
|
-
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8210
|
-
var HOUR_MS = 60 * 60 * 1e3;
|
|
8211
|
-
function sanitizeLogValue(value) {
|
|
8212
|
-
return value.replace(/[\r\n]/g, "");
|
|
8213
|
-
}
|
|
8214
|
-
function getTimezoneOffset2(timezone, date) {
|
|
8215
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8216
|
-
timeZone: timezone,
|
|
8217
|
-
timeZoneName: "longOffset"
|
|
8218
|
-
});
|
|
8219
|
-
const parts = formatter.formatToParts(date);
|
|
8220
|
-
const tzPart = parts.find((part) => part.type === "timeZoneName");
|
|
8221
|
-
const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
|
|
8222
|
-
if (match) return match[1];
|
|
8223
|
-
if (tzPart?.value === "GMT") return "+00:00";
|
|
8224
|
-
logger_default.warn(
|
|
8225
|
-
`Could not parse timezone offset for timezone "${timezone}" on date "${date.toISOString()}". Raw timeZoneName part: "${tzPart?.value ?? "undefined"}". Falling back to UTC (+00:00).`
|
|
8226
|
-
);
|
|
8227
|
-
return "+00:00";
|
|
8228
|
-
}
|
|
8229
|
-
function buildSlotDatetime2(date, time, timezone) {
|
|
8230
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8231
|
-
timeZone: timezone,
|
|
8232
|
-
year: "numeric",
|
|
8233
|
-
month: "2-digit",
|
|
8234
|
-
day: "2-digit"
|
|
8235
|
-
});
|
|
8236
|
-
const parts = formatter.formatToParts(date);
|
|
8237
|
-
const yearPart = parts.find((part) => part.type === "year")?.value;
|
|
8238
|
-
const monthPart = parts.find((part) => part.type === "month")?.value;
|
|
8239
|
-
const dayPart = parts.find((part) => part.type === "day")?.value;
|
|
8240
|
-
const year = yearPart ?? String(date.getFullYear());
|
|
8241
|
-
const month = (monthPart ?? String(date.getMonth() + 1)).padStart(2, "0");
|
|
8242
|
-
const day = (dayPart ?? String(date.getDate())).padStart(2, "0");
|
|
8243
|
-
const offset = getTimezoneOffset2(timezone, date);
|
|
8244
|
-
return `${year}-${month}-${day}T${time}:00${offset}`;
|
|
8245
|
-
}
|
|
8246
|
-
function getDayOfWeekInTimezone2(date, timezone) {
|
|
8247
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8248
|
-
timeZone: timezone,
|
|
8249
|
-
weekday: "short"
|
|
8250
|
-
});
|
|
8251
|
-
const short = formatter.format(date).toLowerCase().slice(0, 3);
|
|
8252
|
-
const map = {
|
|
8253
|
-
sun: "sun",
|
|
8254
|
-
mon: "mon",
|
|
8255
|
-
tue: "tue",
|
|
8256
|
-
wed: "wed",
|
|
8257
|
-
thu: "thu",
|
|
8258
|
-
fri: "fri",
|
|
8259
|
-
sat: "sat"
|
|
8260
|
-
};
|
|
8261
|
-
return map[short] ?? "mon";
|
|
8262
|
-
}
|
|
8263
|
-
async function fetchScheduledPostsSafe(platform) {
|
|
8264
|
-
try {
|
|
8265
|
-
const client = new LateApiClient();
|
|
8266
|
-
return await client.getScheduledPosts(platform);
|
|
8267
|
-
} catch (err) {
|
|
8268
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8269
|
-
logger_default.warn(`Late API unreachable, using local data only: ${msg}`);
|
|
8270
|
-
return [];
|
|
8271
|
-
}
|
|
8272
|
-
}
|
|
8273
|
-
async function buildBookedSlots(platform) {
|
|
8274
|
-
const [latePosts, publishedItems] = await Promise.all([
|
|
8275
|
-
fetchScheduledPostsSafe(platform),
|
|
8276
|
-
getPublishedItems()
|
|
8277
|
-
]);
|
|
8278
|
-
const slots = [];
|
|
8279
|
-
for (const post of latePosts) {
|
|
8280
|
-
if (!post.scheduledFor) continue;
|
|
8281
|
-
for (const scheduledPlatform of post.platforms) {
|
|
8282
|
-
if (!platform || scheduledPlatform.platform === platform) {
|
|
8283
|
-
slots.push({
|
|
8284
|
-
scheduledFor: post.scheduledFor,
|
|
8285
|
-
source: "late",
|
|
8286
|
-
postId: post._id,
|
|
8287
|
-
platform: scheduledPlatform.platform,
|
|
8288
|
-
status: post.status
|
|
8289
|
-
});
|
|
8290
|
-
}
|
|
8291
|
-
}
|
|
8292
|
-
}
|
|
8293
|
-
for (const item of publishedItems) {
|
|
8294
|
-
if (platform && item.metadata.platform !== platform) continue;
|
|
8295
|
-
if (!item.metadata.scheduledFor) continue;
|
|
8296
|
-
slots.push({
|
|
8297
|
-
scheduledFor: item.metadata.scheduledFor,
|
|
8298
|
-
source: "local",
|
|
8299
|
-
itemId: item.id,
|
|
8300
|
-
platform: item.metadata.platform
|
|
8301
|
-
});
|
|
8302
|
-
}
|
|
8303
|
-
return slots;
|
|
8304
|
-
}
|
|
8305
|
-
function buildIdeaReferences(sameIdeaPosts, allBookedSlots) {
|
|
8306
|
-
const lateSlotsByPostId = /* @__PURE__ */ new Map();
|
|
8307
|
-
const localSlotsByItemId = /* @__PURE__ */ new Map();
|
|
8308
|
-
for (const slot of allBookedSlots) {
|
|
8309
|
-
if (slot.postId) {
|
|
8310
|
-
const slots = lateSlotsByPostId.get(slot.postId) ?? [];
|
|
8311
|
-
slots.push(slot);
|
|
8312
|
-
lateSlotsByPostId.set(slot.postId, slots);
|
|
8313
|
-
}
|
|
8314
|
-
if (slot.itemId) {
|
|
8315
|
-
const slots = localSlotsByItemId.get(slot.itemId) ?? [];
|
|
8316
|
-
slots.push(slot);
|
|
8317
|
-
localSlotsByItemId.set(slot.itemId, slots);
|
|
8318
|
-
}
|
|
8319
|
-
}
|
|
8320
|
-
const references = [];
|
|
8321
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8322
|
-
const addReference = (platformName, scheduledFor) => {
|
|
8323
|
-
if (!scheduledFor) return;
|
|
8324
|
-
const key = `${platformName}@${scheduledFor}`;
|
|
8325
|
-
if (seen.has(key)) return;
|
|
8326
|
-
seen.add(key);
|
|
8327
|
-
references.push({ platform: platformName, scheduledFor });
|
|
8328
|
-
};
|
|
8329
|
-
for (const item of sameIdeaPosts) {
|
|
8330
|
-
addReference(item.metadata.platform, item.metadata.scheduledFor);
|
|
8331
|
-
if (item.metadata.latePostId) {
|
|
8332
|
-
for (const slot of lateSlotsByPostId.get(item.metadata.latePostId) ?? []) {
|
|
8333
|
-
addReference(slot.platform, slot.scheduledFor);
|
|
8334
|
-
}
|
|
8335
|
-
}
|
|
8336
|
-
for (const slot of localSlotsByItemId.get(item.id) ?? []) {
|
|
8337
|
-
addReference(slot.platform, slot.scheduledFor);
|
|
8338
|
-
}
|
|
8339
|
-
}
|
|
8340
|
-
return references;
|
|
8341
|
-
}
|
|
8342
|
-
function createSpacingGuard(ideaReferences, samePlatformHours, crossPlatformHours) {
|
|
8343
|
-
const samePlatformWindowMs = samePlatformHours * HOUR_MS;
|
|
8344
|
-
const crossPlatformWindowMs = crossPlatformHours * HOUR_MS;
|
|
8345
|
-
return (candidateMs, candidatePlatform) => {
|
|
8346
|
-
for (const reference of ideaReferences) {
|
|
8347
|
-
const referenceMs = normalizeDateTime(reference.scheduledFor);
|
|
8348
|
-
const diff = Math.abs(candidateMs - referenceMs);
|
|
8349
|
-
if (reference.platform === candidatePlatform && diff < samePlatformWindowMs) {
|
|
8350
|
-
return false;
|
|
8351
|
-
}
|
|
8352
|
-
if (diff < crossPlatformWindowMs) {
|
|
8353
|
-
return false;
|
|
8354
|
-
}
|
|
8355
|
-
}
|
|
8356
|
-
return true;
|
|
8357
|
-
};
|
|
8358
|
-
}
|
|
8359
|
-
function resolveSearchWindow(nowMs, options) {
|
|
8360
|
-
const defaultWindowEndMs = nowMs + DEFAULT_IDEA_WINDOW_DAYS * DAY_MS;
|
|
8361
|
-
const publishBy = options?.publishBy;
|
|
8362
|
-
if (!publishBy) {
|
|
8363
|
-
return {
|
|
8364
|
-
emptyWindowEndMs: defaultWindowEndMs,
|
|
8365
|
-
displacementWindowEndMs: defaultWindowEndMs
|
|
8366
|
-
};
|
|
8367
|
-
}
|
|
8368
|
-
const publishByMs = normalizeDateTime(publishBy);
|
|
8369
|
-
if (Number.isNaN(publishByMs)) {
|
|
8370
|
-
logger_default.warn(`Invalid publishBy "${sanitizeLogValue(publishBy)}" provided; scheduling normally without urgency bias`);
|
|
8371
|
-
return {};
|
|
8372
|
-
}
|
|
8373
|
-
const daysUntilPublishBy = (publishByMs - nowMs) / DAY_MS;
|
|
8374
|
-
if (daysUntilPublishBy <= 0) {
|
|
8375
|
-
logger_default.warn(`publishBy "${sanitizeLogValue(publishBy)}" has already passed; scheduling normally without urgency bias`);
|
|
8376
|
-
return {};
|
|
8377
|
-
}
|
|
8378
|
-
if (daysUntilPublishBy < 3) {
|
|
8379
|
-
logger_default.debug(`Urgent publishBy "${sanitizeLogValue(publishBy)}"; prioritizing earliest displaceable slot`);
|
|
8380
|
-
}
|
|
8381
|
-
return {
|
|
8382
|
-
emptyWindowEndMs: publishByMs,
|
|
8383
|
-
displacementWindowEndMs: daysUntilPublishBy < 7 ? Math.min(publishByMs, nowMs + 3 * DAY_MS) : publishByMs
|
|
8384
|
-
};
|
|
8385
|
-
}
|
|
8386
|
-
function findEmptySlot({
|
|
8387
|
-
platformConfig,
|
|
8388
|
-
timezone,
|
|
8389
|
-
bookedDatetimes,
|
|
8390
|
-
platform,
|
|
8391
|
-
searchFromMs,
|
|
8392
|
-
includeSearchDay = false,
|
|
8393
|
-
maxCandidateMs,
|
|
8394
|
-
passesCandidate
|
|
8395
|
-
}) {
|
|
8396
|
-
if (maxCandidateMs !== void 0 && maxCandidateMs < searchFromMs) {
|
|
8397
|
-
return null;
|
|
8398
|
-
}
|
|
8399
|
-
const baseDate = new Date(searchFromMs);
|
|
8400
|
-
const initialOffset = includeSearchDay ? 0 : 1;
|
|
8401
|
-
let maxDayOffset = MAX_LOOKAHEAD_DAYS;
|
|
8402
|
-
if (maxCandidateMs !== void 0) {
|
|
8403
|
-
maxDayOffset = Math.min(
|
|
8404
|
-
MAX_LOOKAHEAD_DAYS,
|
|
8405
|
-
Math.max(initialOffset, Math.ceil((maxCandidateMs - searchFromMs) / DAY_MS))
|
|
8406
|
-
);
|
|
8407
|
-
}
|
|
8408
|
-
let startOffset = initialOffset;
|
|
8409
|
-
while (startOffset <= maxDayOffset) {
|
|
8410
|
-
const endOffset = Math.min(startOffset + CHUNK_DAYS - 1, maxDayOffset);
|
|
8411
|
-
const candidates = [];
|
|
8412
|
-
for (let dayOffset = startOffset; dayOffset <= endOffset; dayOffset++) {
|
|
8413
|
-
const candidateDate = new Date(baseDate);
|
|
8414
|
-
candidateDate.setDate(candidateDate.getDate() + dayOffset);
|
|
8415
|
-
const dayOfWeek = getDayOfWeekInTimezone2(candidateDate, timezone);
|
|
8416
|
-
if (platformConfig.avoidDays.includes(dayOfWeek)) continue;
|
|
8417
|
-
for (const slot of platformConfig.slots) {
|
|
8418
|
-
if (!slot.days.includes(dayOfWeek)) continue;
|
|
8419
|
-
const candidate = buildSlotDatetime2(candidateDate, slot.time, timezone);
|
|
8420
|
-
const candidateMs = normalizeDateTime(candidate);
|
|
8421
|
-
if (candidateMs <= searchFromMs) continue;
|
|
8422
|
-
if (maxCandidateMs !== void 0 && candidateMs > maxCandidateMs) continue;
|
|
8423
|
-
if (bookedDatetimes.has(candidateMs)) continue;
|
|
8424
|
-
if (passesCandidate && !passesCandidate(candidateMs, platform)) continue;
|
|
8425
|
-
candidates.push(candidate);
|
|
8426
|
-
}
|
|
8427
|
-
}
|
|
8428
|
-
candidates.sort((left, right) => normalizeDateTime(left) - normalizeDateTime(right));
|
|
8429
|
-
if (candidates.length > 0) {
|
|
8430
|
-
return candidates[0];
|
|
8431
|
-
}
|
|
8432
|
-
startOffset = endOffset + 1;
|
|
8433
|
-
}
|
|
8434
|
-
return null;
|
|
8435
|
-
}
|
|
8436
|
-
async function tryDisplacement({
|
|
8437
|
-
bookedSlots,
|
|
8438
|
-
platform,
|
|
8439
|
-
platformConfig,
|
|
8440
|
-
timezone,
|
|
8441
|
-
bookedDatetimes,
|
|
8442
|
-
options,
|
|
8443
|
-
nowMs,
|
|
8444
|
-
maxCandidateMs,
|
|
8445
|
-
passesSpacing
|
|
8446
|
-
}) {
|
|
8447
|
-
const displacementConfig = getDisplacementConfig();
|
|
8448
|
-
if (!displacementConfig.enabled || !options.ideaIds?.length) {
|
|
8449
|
-
return null;
|
|
8450
|
-
}
|
|
8451
|
-
const candidateSlots = bookedSlots.filter((slot) => {
|
|
8452
|
-
const slotMs = normalizeDateTime(slot.scheduledFor);
|
|
8453
|
-
if (slotMs <= nowMs) return false;
|
|
8454
|
-
if (maxCandidateMs !== void 0 && slotMs > maxCandidateMs) return false;
|
|
8455
|
-
return true;
|
|
8456
|
-
}).sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
|
|
8457
|
-
const lateClient = new LateApiClient();
|
|
8458
|
-
const publishedItemCache = /* @__PURE__ */ new Map();
|
|
8459
|
-
for (const slot of candidateSlots) {
|
|
8460
|
-
if (slot.source !== "late" || !slot.postId) continue;
|
|
8461
|
-
const candidateMs = normalizeDateTime(slot.scheduledFor);
|
|
8462
|
-
if (passesSpacing && !passesSpacing(candidateMs, platform)) continue;
|
|
8463
|
-
let publishedItem = publishedItemCache.get(slot.postId);
|
|
8464
|
-
if (publishedItem === void 0) {
|
|
8465
|
-
publishedItem = await getPublishedItemByLatePostId(slot.postId);
|
|
8466
|
-
publishedItemCache.set(slot.postId, publishedItem);
|
|
8467
|
-
}
|
|
8468
|
-
if (!publishedItem) {
|
|
8469
|
-
continue;
|
|
8470
|
-
}
|
|
8471
|
-
if (publishedItem.metadata.ideaIds?.length) {
|
|
8472
|
-
continue;
|
|
8473
|
-
}
|
|
8474
|
-
const displacedPlatformConfig = publishedItem?.metadata.clipType ? getPlatformSchedule(platform, publishedItem.metadata.clipType) ?? platformConfig : platformConfig;
|
|
8475
|
-
const newSlot = findEmptySlot({
|
|
8476
|
-
platformConfig: displacedPlatformConfig,
|
|
8477
|
-
timezone,
|
|
8478
|
-
bookedDatetimes,
|
|
8479
|
-
platform,
|
|
8480
|
-
searchFromMs: candidateMs,
|
|
8481
|
-
includeSearchDay: true
|
|
8482
|
-
});
|
|
8483
|
-
if (!newSlot) continue;
|
|
8484
|
-
await lateClient.schedulePost(slot.postId, newSlot);
|
|
8485
|
-
logger_default.info(
|
|
8486
|
-
`Displaced post ${sanitizeLogValue(slot.postId)} from ${sanitizeLogValue(slot.scheduledFor)} to ${sanitizeLogValue(newSlot)} for idea-linked content`
|
|
8487
|
-
);
|
|
8488
|
-
return {
|
|
8489
|
-
slot: slot.scheduledFor,
|
|
8490
|
-
displaced: {
|
|
8491
|
-
postId: slot.postId,
|
|
8492
|
-
originalSlot: slot.scheduledFor,
|
|
8493
|
-
newSlot
|
|
8494
|
-
}
|
|
8495
|
-
};
|
|
8496
|
-
}
|
|
8497
|
-
return null;
|
|
8498
|
-
}
|
|
8499
|
-
async function findNextSlot(platform, clipType, options) {
|
|
8500
|
-
const config2 = await loadScheduleConfig();
|
|
8501
|
-
const platformConfig = getPlatformSchedule(platform, clipType);
|
|
8502
|
-
if (!platformConfig) {
|
|
8503
|
-
logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
|
|
8504
|
-
return null;
|
|
8505
|
-
}
|
|
8506
|
-
const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
|
|
8507
|
-
const isIdeaAware = ideaIds.length > 0;
|
|
8508
|
-
const nowMs = Date.now();
|
|
8509
|
-
const { timezone } = config2;
|
|
8510
|
-
const [allBookedSlots, sameIdeaPosts] = await Promise.all([
|
|
8511
|
-
isIdeaAware ? buildBookedSlots() : Promise.resolve([]),
|
|
8512
|
-
isIdeaAware ? getScheduledItemsByIdeaIds(ideaIds) : Promise.resolve([])
|
|
8513
|
-
]);
|
|
8514
|
-
const bookedSlots = isIdeaAware ? allBookedSlots.filter((slot) => slot.platform === platform) : await buildBookedSlots(platform);
|
|
8515
|
-
const bookedDatetimes = new Set(bookedSlots.map((slot) => normalizeDateTime(slot.scheduledFor)));
|
|
8516
|
-
const spacingConfig = isIdeaAware ? getIdeaSpacingConfig() : null;
|
|
8517
|
-
const spacingGuard = spacingConfig ? createSpacingGuard(
|
|
8518
|
-
buildIdeaReferences(sameIdeaPosts, allBookedSlots),
|
|
8519
|
-
spacingConfig.samePlatformHours,
|
|
8520
|
-
spacingConfig.crossPlatformHours
|
|
8521
|
-
) : void 0;
|
|
8522
|
-
const searchWindow = isIdeaAware ? resolveSearchWindow(nowMs, options) : {};
|
|
8523
|
-
const emptySlot = findEmptySlot({
|
|
8524
|
-
platformConfig,
|
|
8525
|
-
timezone,
|
|
8526
|
-
bookedDatetimes,
|
|
8527
|
-
platform,
|
|
8528
|
-
searchFromMs: nowMs,
|
|
8529
|
-
maxCandidateMs: searchWindow.emptyWindowEndMs,
|
|
8530
|
-
passesCandidate: spacingGuard
|
|
8531
|
-
});
|
|
8532
|
-
if (emptySlot) {
|
|
8533
|
-
logger_default.debug(`Found available slot for ${sanitizeLogValue(platform)}: ${sanitizeLogValue(emptySlot)}`);
|
|
8534
|
-
return emptySlot;
|
|
8535
|
-
}
|
|
8536
|
-
if (isIdeaAware) {
|
|
8537
|
-
const displaced = await tryDisplacement({
|
|
8538
|
-
bookedSlots,
|
|
8539
|
-
platform,
|
|
8540
|
-
platformConfig,
|
|
8541
|
-
timezone,
|
|
8542
|
-
bookedDatetimes,
|
|
8543
|
-
options: { ...options, ideaIds },
|
|
8544
|
-
nowMs,
|
|
8545
|
-
maxCandidateMs: searchWindow.displacementWindowEndMs,
|
|
8546
|
-
passesSpacing: spacingGuard
|
|
8547
|
-
});
|
|
8548
|
-
if (displaced) {
|
|
8549
|
-
return displaced.slot;
|
|
8550
|
-
}
|
|
8551
|
-
}
|
|
8552
|
-
logger_default.warn(`No available slot found for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
|
|
8553
|
-
return null;
|
|
8554
|
-
}
|
|
8555
|
-
async function getScheduleCalendar(startDate, endDate) {
|
|
8556
|
-
const slots = await buildBookedSlots();
|
|
8557
|
-
let filtered = slots.filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
|
|
8558
|
-
platform: slot.platform,
|
|
8559
|
-
scheduledFor: slot.scheduledFor,
|
|
8560
|
-
source: slot.source,
|
|
8561
|
-
postId: slot.postId,
|
|
8562
|
-
itemId: slot.itemId
|
|
8563
|
-
}));
|
|
8564
|
-
if (startDate) {
|
|
8565
|
-
const startMs = startDate.getTime();
|
|
8566
|
-
filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) >= startMs);
|
|
8567
|
-
}
|
|
8568
|
-
if (endDate) {
|
|
8569
|
-
const endMs = endDate.getTime();
|
|
8570
|
-
filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) <= endMs);
|
|
8571
|
-
}
|
|
8572
|
-
filtered.sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
|
|
8573
|
-
return filtered;
|
|
8574
|
-
}
|
|
8575
|
-
|
|
8576
8501
|
// src/L2-clients/ffmpeg/audioExtraction.ts
|
|
8577
8502
|
init_fileSystem();
|
|
8578
8503
|
init_paths();
|
|
@@ -10079,6 +10004,9 @@ function platformAcceptsMedia(platform, clipType) {
|
|
|
10079
10004
|
return getMediaRule(platform, clipType) !== null;
|
|
10080
10005
|
}
|
|
10081
10006
|
|
|
10007
|
+
// src/L3-services/queueBuilder/queueBuilder.ts
|
|
10008
|
+
init_postStore();
|
|
10009
|
+
|
|
10082
10010
|
// src/L1-infra/image/image.ts
|
|
10083
10011
|
import { default as default7 } from "sharp";
|
|
10084
10012
|
|
|
@@ -10766,7 +10694,7 @@ function buildSystemPrompt(brand, existingIdeas, seedTopics, count, ideaRepo) {
|
|
|
10766
10694
|
}
|
|
10767
10695
|
return promptSections.join("\n");
|
|
10768
10696
|
}
|
|
10769
|
-
function buildUserMessage(count, seedTopics, hasMcpServers) {
|
|
10697
|
+
function buildUserMessage(count, seedTopics, hasMcpServers, userPrompt) {
|
|
10770
10698
|
const focusText = seedTopics.length > 0 ? `Focus areas: ${seedTopics.join(", ")}` : "Focus areas: choose the strongest timely opportunities from the creator context and current trends.";
|
|
10771
10699
|
const steps = [
|
|
10772
10700
|
"1. Call get_brand_context to load the creator profile.",
|
|
@@ -10790,13 +10718,15 @@ function buildUserMessage(count, seedTopics, hasMcpServers) {
|
|
|
10790
10718
|
"5. Call finalize_ideas when done."
|
|
10791
10719
|
);
|
|
10792
10720
|
}
|
|
10793
|
-
|
|
10721
|
+
const sections = [
|
|
10794
10722
|
`Generate ${count} new content ideas.`,
|
|
10795
|
-
focusText
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10723
|
+
focusText
|
|
10724
|
+
];
|
|
10725
|
+
if (userPrompt) {
|
|
10726
|
+
sections.push("", `## User Prompt`, userPrompt);
|
|
10727
|
+
}
|
|
10728
|
+
sections.push("", "Follow this exact workflow:", ...steps);
|
|
10729
|
+
return sections.join("\n");
|
|
10800
10730
|
}
|
|
10801
10731
|
async function loadBrandContext(brandPath) {
|
|
10802
10732
|
if (!brandPath) {
|
|
@@ -11313,7 +11243,7 @@ async function generateIdeas(options = {}) {
|
|
|
11313
11243
|
});
|
|
11314
11244
|
try {
|
|
11315
11245
|
const hasMcpServers = !!(config2.EXA_API_KEY || config2.YOUTUBE_API_KEY || config2.PERPLEXITY_API_KEY);
|
|
11316
|
-
const userMessage = buildUserMessage(count, seedTopics, hasMcpServers);
|
|
11246
|
+
const userMessage = buildUserMessage(count, seedTopics, hasMcpServers, options.prompt);
|
|
11317
11247
|
await agent.run(userMessage);
|
|
11318
11248
|
const ideas = agent.getGeneratedIdeas();
|
|
11319
11249
|
if (!agent.isFinalized()) {
|
|
@@ -16599,7 +16529,8 @@ var defaultKeys = [
|
|
|
16599
16529
|
"brandPath",
|
|
16600
16530
|
"ideasRepo",
|
|
16601
16531
|
"lateProfileId",
|
|
16602
|
-
"geminiModel"
|
|
16532
|
+
"geminiModel",
|
|
16533
|
+
"scheduleConfig"
|
|
16603
16534
|
];
|
|
16604
16535
|
var configKeyMap = {
|
|
16605
16536
|
"openai-key": { section: "credentials", key: "openaiApiKey" },
|
|
@@ -16617,7 +16548,8 @@ var configKeyMap = {
|
|
|
16617
16548
|
"brand-path": { section: "defaults", key: "brandPath" },
|
|
16618
16549
|
"ideas-repo": { section: "defaults", key: "ideasRepo" },
|
|
16619
16550
|
"late-profile-id": { section: "defaults", key: "lateProfileId" },
|
|
16620
|
-
"gemini-model": { section: "defaults", key: "geminiModel" }
|
|
16551
|
+
"gemini-model": { section: "defaults", key: "geminiModel" },
|
|
16552
|
+
"schedule-config": { section: "defaults", key: "scheduleConfig" }
|
|
16621
16553
|
};
|
|
16622
16554
|
var providerDefaults = {
|
|
16623
16555
|
copilot: "Claude Opus 4.6",
|