import type { DatabaseClient } from "./database"; import { getRuntimeEnv } from "./runtime-env"; import { sendWecomAppMessage, WecomClientError, type WecomConfig, } from "./wecom-client"; import { pushTaskToGroupChats, type GroupPushResult, type TaskPushRow, } from "./wecom-group-push-service"; import { getProjectDetail, ProjectError } from "./project-service"; type FetchLike = typeof fetch; export type AppMessageResult = { userid: string; ok: boolean; error: string; }; export type ProjectPushOutcome = { appResults: AppMessageResult[]; groupResults: GroupPushResult[]; groupChatCount: number; skippedReasons: string[]; linkUrl: string; }; function text(value: unknown) { return String(value ?? "").trim(); } export function buildProjectAppMessage( task: Pick< TaskPushRow, | "name" | "brand" | "quantity" | "due_at" | "task_type" | "platform" | "content_format" | "share_token" >, portalUrl: string, ): string { const portal = text(portalUrl).replace(/\/$/, ""); const claimUrl = portal && task.share_token ? `${portal}/?task=${task.share_token}` : ""; const formatLabel = task.task_type === "screenshot_collect" ? "截图回收" : task.content_format === "video" ? "视频" : "图文"; return [ `【项目任务】${task.name}`, `品牌:${task.brand}|数量:${task.quantity} 份|截止:${task.due_at}`, `平台:${task.platform}|形式:${formatLabel}`, "请把任务转发到你负责的项目任务群,并跟进群内成员领取与发布。", claimUrl ? `领取链接:${claimUrl}` : "领取链接生成失败,请联系管理员", ].join("\n"); } export async function pushProjectTask( db: DatabaseClient, params: { taskId: unknown; sendMainGroup?: unknown; sendGroupChats?: unknown; text?: unknown; }, config: WecomConfig, fetchImpl: FetchLike = fetch, ): Promise { const taskId = text(params.taskId); if (!taskId) throw new ProjectError("缺少 taskId"); const sendMainGroup = params.sendMainGroup !== false; const sendGroupChats = params.sendGroupChats !== false; if (!sendMainGroup && !sendGroupChats) { throw new ProjectError("请至少选择一个发送目标(项目总群或任务群)"); } const task = await db .prepare( `SELECT id, name, brand, quantity, due_at, task_type, platform, content_format, share_token FROM tasks WHERE id = ?`, ) .bind(taskId) .first(); if (!task) throw new ProjectError("任务不存在", 404); if (!text(task.project_id)) { throw new ProjectError("该任务未挂项目,请先在任务中心关联项目"); } const detail = await getProjectDetail(db, task.project_id); const portalUrl = text(getRuntimeEnv().KOC_PORTAL_URL); const fallback = buildProjectAppMessage(task, portalUrl); const message = text(params.text) || fallback; const skippedReasons: string[] = []; const appResults: AppMessageResult[] = []; if (sendMainGroup) { const members = detail.project.mainGroupMemberIds; if (members.length === 0) { skippedReasons.push("项目总群未配置成员"); } else { for (const userid of members) { try { await sendWecomAppMessage([userid], message, config, fetchImpl); appResults.push({ userid, ok: true, error: "" }); } catch (error) { appResults.push({ userid, ok: false, error: error instanceof Error ? error.message : "发送失败", }); } } } } let groupResults: GroupPushResult[] = []; let groupChatCount = 0; let linkUrl = task.share_token && portalUrl ? `${portalUrl.replace(/\/$/, "")}/?task=${task.share_token}` : ""; if (sendGroupChats) { const chatIds = detail.groupChats .filter((chat) => !chat.missing) .map((chat) => chat.chatId); if (chatIds.length === 0) { skippedReasons.push("项目没有可用的任务群"); } else { const summary = await pushTaskToGroupChats( db, { taskId, chatIds, text: message }, config, fetchImpl, ); groupResults = summary.results; groupChatCount = chatIds.length; linkUrl = summary.linkUrl; } } return { appResults, groupResults, groupChatCount, skippedReasons, linkUrl, }; } export { WecomClientError };