import type { DatabaseClient } from "./database"; import { getRuntimeEnv } from "./runtime-env"; import { sendAppChatMessage, sendWecomAppMessage, WecomClientError, type WecomConfig, } from "./wecom-client"; import { ensureTaskChatForMembers, sendTaskChatMessage, } from "./task-group-service"; import { getProjectDetail, ProjectError } from "./project-service"; import type { TaskPushRow } from "./wecom-group-push-service"; type FetchLike = typeof fetch; export type AppMessageResult = { userid: string; ok: boolean; error: string; }; export type MainChatMode = "group" | "per_member" | "skipped"; export type TaskChatPushResult = { sent: boolean; chatId: string; reason?: string; }; export type ProjectPushOutcome = { mainChatMode: MainChatMode; appResults: AppMessageResult[]; taskChat: TaskChatPushResult; 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.content_format === "video" ? "视频" : "图文"; return [ `【项目任务】${task.name}`, `品牌:${task.brand}|数量:${task.quantity} 份|截止:${task.due_at}`, `平台:${task.platform}|形式:${formatLabel}`, "请把任务转发到你负责的项目任务群,并跟进群内成员领取与发布。", claimUrl ? `领取链接:${claimUrl}` : "领取链接生成失败,请联系管理员", ].join("\n"); } const TASK_CHAT_SKIP_REASONS: Record = { no_task: "任务不存在", no_owner: "任务与项目都未填负责人", not_enough_members: "任务群成员不足 2 人", no_credentials: "企业微信未配置", }; 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, project_id 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[] = []; let mainChatMode: MainChatMode = "skipped"; const sendPerMember = async () => { const members = detail.project.mainGroupMemberIds; if (members.length === 0) { skippedReasons.push("项目总群未建且未配置成员"); return; } 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 : "发送失败", }); } } mainChatMode = "per_member"; }; if (sendMainGroup) { if (detail.project.mainChatId) { try { await sendAppChatMessage( config, { chatid: detail.project.mainChatId, text: message }, fetchImpl, ); mainChatMode = "group"; } catch (error) { skippedReasons.push( `项目总群群发失败(${ error instanceof Error ? error.message : "未知错误" }),已降级逐人私信`, ); await sendPerMember(); } } else { skippedReasons.push("项目总群未建,已逐人私信"); await sendPerMember(); } } let taskChat: TaskChatPushResult = { sent: false, chatId: "" }; if (sendGroupChats) { const ensure = await ensureTaskChatForMembers(db, { taskId }, config, fetchImpl); if (ensure.chatId) { const sent = await sendTaskChatMessage(db, taskId, message, config, fetchImpl); taskChat = sent ? { sent: true, chatId: ensure.chatId } : { sent: false, chatId: ensure.chatId, reason: "消息发送失败" }; if (!sent) skippedReasons.push("任务群消息发送失败"); } else { const reason = TASK_CHAT_SKIP_REASONS[ensure.reason ?? ""] ?? "任务群暂不可用"; taskChat = { sent: false, chatId: "", reason }; skippedReasons.push(`任务群未发送:${reason}`); } } const linkUrl = task.share_token && portalUrl ? `${portalUrl.replace(/\/$/, "")}/?task=${task.share_token}` : ""; return { mainChatMode, appResults, taskChat, skippedReasons, linkUrl, }; } export { WecomClientError };