import type { DatabaseClient } from "./database"; import { createAppChat, getAppChat, hasAppCredentials, sendAppChatMessage, updateAppChat, type WecomConfig, } from "./wecom-client"; type FetchLike = typeof fetch; export type TaskChatEnsureResult = { chatId: string | null; created: boolean; reason?: "no_task" | "no_owner" | "not_enough_members" | "no_credentials"; }; type TaskChatRow = { id: string; name: string; task_chat_id: string | null; owner_userid: string | null; }; function text(value: unknown) { return String(value ?? "").trim(); } export function taskChatId(taskId: string) { return `tgc_${text(taskId)}`; } function chatNameForTask(taskName: string) { return `【任务】${text(taskName)}`.slice(0, 20); } async function syncMembers( config: WecomConfig, chatid: string, members: string[], fetchImpl: FetchLike, ) { const info = await getAppChat(config, chatid, fetchImpl); if (!info) return; const existing = new Set(info.userlist); const missing = members.filter((userid) => !existing.has(userid)); if (missing.length > 0) { await updateAppChat(config, { chatid, addUserList: missing }, fetchImpl); } } export async function ensureTaskChatForMembers( db: DatabaseClient, params: { taskId: string; addUserids?: string[] }, config: WecomConfig, fetchImpl: FetchLike = fetch, ): Promise { const taskId = text(params.taskId); if (!taskId) return { chatId: null, created: false, reason: "no_task" }; if (!hasAppCredentials(config)) { return { chatId: null, created: false, reason: "no_credentials" }; } const task = await db .prepare( `SELECT t.id, t.name, t.task_chat_id, p.owner_userid AS owner_userid FROM tasks t LEFT JOIN projects p ON p.id = t.project_id WHERE t.id = ?`, ) .bind(taskId) .first(); if (!task) return { chatId: null, created: false, reason: "no_task" }; const ownerUserid = text(task.owner_userid); if (!ownerUserid) { return { chatId: null, created: false, reason: "no_owner" }; } const members = [ ...new Set( [ownerUserid, ...(params.addUserids ?? []).map((id) => text(id))].filter( Boolean, ), ), ]; let chatId = text(task.task_chat_id); let createdNow = false; if (!chatId) { chatId = taskChatId(taskId); const existing = await getAppChat(config, chatId, fetchImpl); if (!existing) { if (members.length < 2) { return { chatId: null, created: false, reason: "not_enough_members" }; } const created = await createAppChat( config, { name: chatNameForTask(task.name), userlist: members, chatid: chatId }, fetchImpl, ); chatId = created.chatid; createdNow = true; } await db .prepare("UPDATE tasks SET task_chat_id = ? WHERE id = ?") .bind(chatId, taskId) .run(); } await syncMembers(config, chatId, members, fetchImpl); return { chatId, created: createdNow }; } export async function sendTaskChatMessage( db: DatabaseClient, taskId: string, message: string, config: WecomConfig, fetchImpl: FetchLike = fetch, ): Promise { try { const content = text(message); if (!content) return false; const row = await db .prepare("SELECT task_chat_id FROM tasks WHERE id = ?") .bind(text(taskId)) .first<{ task_chat_id: string | null }>(); const chatId = text(row?.task_chat_id); if (!chatId) return false; await sendAppChatMessage(config, { chatid: chatId, text: content }, fetchImpl); return true; } catch (error) { console.error( `[KOC LOOP] 任务群消息发送失败(task=${taskId})`, error instanceof Error ? error.message : error, ); return false; } } export function taskClaimBroadcastText( taskName: string, claimantName: string, quantity: number, ) { return `【领取】${text(claimantName)} 认领 ${quantity} 份 · ${text(taskName)}`; } export function taskChatIntroText(task: { name: string; brand: string; quantity: number; dueAt: string; }) { return [ `【新任务】${text(task.name)}`, `品牌:${text(task.brand)}|数量:${task.quantity} 份|截止:${text(task.dueAt)}`, ].join("\n"); }