| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- import { isoDateString, type ExportFile } from "@pipeline/shared";
- import { getTimezone, createLogger, describeError } from "@pipeline/shared/node";
- import { uploadToOss, type OssConfig } from "./oss.js";
- import { sendFeishuMessage, type FeishuConfig } from "./feishu.js";
- const log = createLogger("publish");
- export type { OssConfig, FeishuConfig };
- export interface PublishConfig {
- oss?: OssConfig;
- feishu?: FeishuConfig;
- }
- export interface PublishContext {
- jobId: string;
- template: string;
- platforms: string[];
- }
- export interface UploadedFile {
- platform: string;
- filePath: string;
- ossUrl: string;
- }
- export interface PublishOutcome {
- /** true when there is no OSS configured, or every file uploaded successfully. */
- ok: boolean;
- uploaded: UploadedFile[];
- /** Present when an upload failed (the render itself still succeeded). */
- error?: string;
- }
- function parseBool(value: string | undefined, fallback: boolean): boolean {
- if (value === undefined || value === "") return fallback;
- return value === "1" || value.toLowerCase() === "true";
- }
- /**
- * Build a PublishConfig from the raw YAML config merged with environment
- * variables. Env vars take precedence and are the recommended place for secrets.
- * Returns undefined when neither OSS nor Feishu is usable.
- */
- export function resolvePublishConfig(
- raw?: Record<string, any> | null
- ): PublishConfig | undefined {
- const ossRaw = raw?.oss ?? {};
- const accessKeyId = process.env.OSS_ACCESS_KEY_ID || ossRaw.accessKeyId || "";
- const accessKeySecret =
- process.env.OSS_ACCESS_KEY_SECRET || ossRaw.accessKeySecret || "";
- const region = process.env.OSS_REGION || ossRaw.region || "";
- const bucket = process.env.OSS_BUCKET || ossRaw.bucket || "";
- const oss: OssConfig = {
- region,
- bucket,
- accessKeyId,
- accessKeySecret,
- endpoint: process.env.OSS_ENDPOINT || ossRaw.endpoint,
- path: process.env.OSS_PATH || ossRaw.path,
- publicDomain: process.env.OSS_PUBLIC_DOMAIN || ossRaw.publicDomain,
- secure: parseBool(process.env.OSS_SECURE, ossRaw.secure ?? true),
- };
- const ossEnabled = Boolean(oss.region && oss.bucket && oss.accessKeyId && oss.accessKeySecret);
- const feishuRaw = raw?.feishu ?? {};
- const webhook = process.env.FEISHU_WEBHOOK_URL || feishuRaw.webhook || "";
- const feishu: FeishuConfig = {
- webhook,
- secret: process.env.FEISHU_WEBHOOK_SECRET || feishuRaw.secret,
- };
- const feishuEnabled = Boolean(feishu.webhook);
- if (!ossEnabled && !feishuEnabled) return undefined;
- return {
- oss: ossEnabled ? oss : undefined,
- feishu: feishuEnabled ? feishu : undefined,
- };
- }
- async function safeNotify(config: FeishuConfig, text: string): Promise<boolean> {
- try {
- await sendFeishuMessage(config, text);
- return true;
- } catch (err) {
- // Notification is best-effort (must not mask the real outcome), but log it
- // so a misconfigured webhook / signing failure / network error is visible
- // (including the fetch cause: DNS / connection / TLS) instead of silent.
- log.error(`feishu send failed: ${describeError(err)}`);
- return false;
- }
- }
- function buildSuccessText(
- ctx: PublishContext,
- uploaded: UploadedFile[],
- files: ExportFile[]
- ): string {
- const lines = uploaded.length
- ? uploaded.map((u) => `- [${u.platform}] ${u.ossUrl}`)
- : files.map((f) => `- [${f.platform}] ${f.filePath}`);
- return [
- "✅ 视频生成成功",
- `模板: ${ctx.template}`,
- `平台: ${ctx.platforms.join(", ")}`,
- `任务: ${ctx.jobId}`,
- "资源:",
- ...lines,
- ].join("\n");
- }
- function buildUploadFailureText(
- ctx: PublishContext,
- files: ExportFile[],
- error: string
- ): string {
- return [
- "⚠️ 视频已生成,但 OSS 上传失败",
- `模板: ${ctx.template}`,
- `平台: ${ctx.platforms.join(", ")}`,
- `任务: ${ctx.jobId}`,
- `本地文件: ${files.map((f) => f.filePath).join(", ")}`,
- `错误: ${error}`,
- ].join("\n");
- }
- function buildGenerationFailureText(ctx: PublishContext, error: string): string {
- return [
- "❌ 视频生成失败",
- `模板: ${ctx.template}`,
- `平台: ${ctx.platforms.join(", ")}`,
- `任务: ${ctx.jobId}`,
- `错误: ${error}`,
- ].join("\n");
- }
- /**
- * Upload all exported files to OSS (if configured) and notify Feishu.
- * - Upload success → push OSS resource links.
- * - Upload failure → push failure info (render still succeeded locally).
- * - No OSS configured → push success with local file paths.
- */
- export async function runPublish(
- files: ExportFile[],
- publish: PublishConfig,
- ctx: PublishContext
- ): Promise<PublishOutcome> {
- const dateDir = isoDateString(new Date(), getTimezone());
- const uploaded: UploadedFile[] = [];
- if (publish.oss) {
- log.info(`oss upload start files=${files.length} prefix=${publish.oss.path ?? "(root)"}`);
- try {
- for (const file of files) {
- const result = await uploadToOss(file.filePath, publish.oss, { dateDir });
- uploaded.push({ platform: file.platform, filePath: file.filePath, ossUrl: result.url });
- log.info(`oss uploaded [${file.platform}] key=${result.key} url=${result.url}`);
- }
- } catch (err) {
- const error = describeError(err);
- log.error(`oss upload failed: ${error}`);
- if (publish.feishu) {
- const ok = await safeNotify(publish.feishu, buildUploadFailureText(ctx, files, error));
- log.info(`feishu upload-failure notice ${ok ? "sent" : "failed"}`);
- }
- return { ok: false, uploaded, error };
- }
- }
- if (publish.feishu) {
- const ok = await safeNotify(publish.feishu, buildSuccessText(ctx, uploaded, files));
- log.info(`feishu success notice ${ok ? "sent" : "failed"}`);
- }
- return { ok: true, uploaded };
- }
- /** Notify Feishu that the render itself failed before any file was produced. */
- export async function notifyGenerationFailure(
- publish: PublishConfig,
- ctx: PublishContext,
- error: string
- ): Promise<void> {
- if (!publish.feishu) return;
- await safeNotify(publish.feishu, buildGenerationFailureText(ctx, error));
- }
|