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 记录归属群主
- 后台任务卡「群主链接」面板:复制链接/推送内部群主/任务群状态
- 催办与超期升级同步播报进任务群
This commit is contained in:
ABAPPLO
2026-09-03 11:30:26 +08:00
parent 5d2a17f5b5
commit bd80b2d7d8
17 changed files with 1466 additions and 39 deletions

View File

@@ -731,6 +731,7 @@ export async function POST(request: Request) {
name: body.name,
brand: body.brand,
mainGroupMemberIds: body.mainGroupMemberIds,
ownerUserid: body.ownerUserid,
note: body.note,
}),
});
@@ -814,6 +815,85 @@ export async function POST(request: Request) {
return Response.json({ refresh: status });
} else if (body.action === "account_field_refresh_status") {
return Response.json({ refresh: getAccountFieldRefreshStatus() });
} else if (body.action === "task_group_links") {
const taskId = String(body.taskId ?? "").trim();
const task = await db
.prepare(
"SELECT id, name, share_token, task_chat_id FROM tasks WHERE id = ?",
)
.bind(taskId)
.first<{
id: string;
name: string;
share_token: string | null;
task_chat_id: string;
}>();
if (!task) return Response.json({ error: "任务不存在" }, { status: 404 });
const portalUrl = String(env.KOC_PORTAL_URL ?? "").replace(/\/$/, "");
const owners = await listGroupOwnerPartners(db);
return Response.json({
taskName: task.name,
taskChatId: task.task_chat_id ?? "",
claimUrl: task.share_token
? `${portalUrl}/?task=${task.share_token}`
: "",
owners: task.share_token
? owners.map((owner) => ({
partnerId: owner.id,
name: owner.name,
wecomUserid: owner.wecom_userid,
isInternal: Boolean(owner.wecom_userid),
link: `${portalUrl}/?task=${task.share_token}&owner=${owner.id}`,
}))
: [],
});
} else if (body.action === "task_group_push") {
const taskId = String(body.taskId ?? "").trim();
const task = await db
.prepare(
"SELECT id, name, share_token FROM tasks WHERE id = ?",
)
.bind(taskId)
.first<{ id: string; name: string; share_token: string | null }>();
if (!task) return Response.json({ error: "任务不存在" }, { status: 404 });
if (!task.share_token) {
return Response.json({ error: "任务缺少领取链接" }, { status: 400 });
}
const owners = (await listGroupOwnerPartners(db)).filter(
(owner) => owner.wecom_userid,
);
if (owners.length === 0) {
return Response.json(
{ error: "没有可推送的内部群主,请先在后台登记群主 userid" },
{ status: 400 },
);
}
const portalUrl = String(env.KOC_PORTAL_URL ?? "").replace(/\/$/, "");
const wecomConfig = resolveWecomConfig(env as unknown as WecomBindings);
let sent = 0;
let failed = 0;
const failList: string[] = [];
for (const owner of owners) {
const link = `${portalUrl}/?task=${task.share_token}&owner=${owner.id}`;
try {
const result = await sendWecomAppMessage(
[owner.wecom_userid],
`【任务派发】${task.name}\n请把领取链接转发给你的 KOC(链接已绑定你的群主身份):\n${link}`,
wecomConfig,
);
if (result.skipped) {
failed += 1;
failList.push(owner.name);
} else {
sent += result.sent;
failed += result.failed;
}
} catch {
failed += 1;
failList.push(owner.name);
}
}
return Response.json({ sent, failed, failList });
} else if (body.action === "wecom_group_chats") {
const groups = await listGroupChatRows(db);
return Response.json({ groups });