Files
koc-loop/lib/task-group-service.ts

159 lines
4.2 KiB
TypeScript
Raw Normal View History

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<TaskChatEnsureResult> {
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<TaskChatRow>();
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<boolean> {
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");
}