Files
koc-loop/lib/task-group-service.ts
ABAPPLO bd80b2d7d8 feat: 项目负责人+内部任务群+portal 内部身份领取+群主链接归属
- 迁移 0012:projects.owner_userid / tasks.task_chat_id / claims.owner_partner_id
- lib/task-group-service.ts:懒建 appchat 任务群(首个内部群主领取时创建,成员 diff 补员),播报/卡片文案
- wecom-client 新增 appchat create/update/get/send 封装
- OAuth ticket 增可选 uid(v:1 兼容),内部成员领取按 wecom_userid 解析群主身份
- 外部 OAuth 领取人 upsert 打 partner_type='group_owner'
- portal 链接带 ?owner= 穿透 OAuth 往返,claims 记录归属群主
- 后台任务卡「群主链接」面板:复制链接/推送内部群主/任务群状态
- 催办与超期升级同步播报进任务群
2026-09-03 11:30:26 +08:00

159 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}