feat: 项目总群自动建群+任务负责人后台视角+客户群活码进群+任务编辑
- 项目总群:≥3 人 appchat/create 自动建群(chatid=pgc+projectId),成员 diff 增删同步;DROP project_group_chats;推送改 appchat/send 群发、无群降级逐人 - 任务负责人:tasks.owner_userid(新建/编辑任务通讯录选人),task_set_owner 分配即预建 tgc 任务群发介绍,群主链接只归属负责人 - 任务编辑 task_update:基础字段+截图任务数量/关键词/说明,contents 调平只删 available 尾行 - 删除「一键推送」:task-one-click-push-service 整体移除(recordCardAck 移入 wecom-callback-events) - 负责人后台视角:users.wecom_userid 绑定,lib/owner-scope 行级过滤(undefined=全量/""=空范围/非空=过滤),~30 action 补 managerGate/taskOwnerGate,owner 登录仅见工作台/任务中心/数据回收/复盘报表 - 任务绑定客户群+进群活码:tasks 增 customer_chat_id/name、join_config_id、join_qr_code 4 列,join_way add/update/get/del(换绑走 update 保 config_id 活码不变),后台「客户群」弹窗,portal 认领后扫码自助进群(满员自动开新群) - 迁移 0013/0014/0015;催办支持按任务;企微可信域名 IP 验证文件 - 288 测试全绿;真机全链路验收通过(列群/活码四端点/portal 渲染/解绑清空)
This commit is contained in:
@@ -96,6 +96,11 @@ function parseDueDays(value: string | undefined) {
|
||||
return Math.min(30, Math.floor(parsed));
|
||||
}
|
||||
|
||||
// 企微 appchat 的 chatid 只接受字母和数字(下划线/连字符都会报 86001 chatid invalid)
|
||||
export function sanitizeChatid(value: string) {
|
||||
return String(value ?? "").replace(/[^a-zA-Z0-9]/g, "");
|
||||
}
|
||||
|
||||
export function hasAppCredentials(config: WecomConfig) {
|
||||
return Boolean(config.corpId && config.agentId && config.secret);
|
||||
}
|
||||
@@ -118,9 +123,10 @@ async function readEnvelope(
|
||||
function ensureOk(
|
||||
payload: WecomEnvelope,
|
||||
fallbackMessage: string,
|
||||
allowErrcodes: number[] = [],
|
||||
) {
|
||||
const code = Number(payload.errcode ?? 0);
|
||||
if (code === 0) return;
|
||||
if (code === 0 || allowErrcodes.includes(code)) return;
|
||||
const message = safeMessage(payload.errmsg) || fallbackMessage;
|
||||
if (code === 40014 || code === 42001) {
|
||||
throw new WecomClientError(`企业微信 access_token 无效:${message}`, 401);
|
||||
@@ -336,10 +342,14 @@ export type WecomGroupChat = {
|
||||
export async function listCustomerGroupChats(
|
||||
config: WecomConfig,
|
||||
fetchImpl: FetchLike = fetch,
|
||||
ownerUserids?: string[],
|
||||
): Promise<WecomGroupChat[]> {
|
||||
if (!hasAppCredentials(config)) return [];
|
||||
const token = await fetchAccessToken(config, fetchImpl);
|
||||
|
||||
const ownerFilterUserids = (ownerUserids ?? [])
|
||||
.map((id) => bindingValue(id))
|
||||
.filter((id) => id.length > 0);
|
||||
const chatIds: Array<{ chatId: string; status: number }> = [];
|
||||
let cursor = "";
|
||||
do {
|
||||
@@ -347,7 +357,13 @@ export async function listCustomerGroupChats(
|
||||
const listResp = await fetchImpl(listUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify({ limit: 100, cursor }),
|
||||
body: JSON.stringify({
|
||||
limit: 100,
|
||||
cursor,
|
||||
...(ownerFilterUserids.length > 0
|
||||
? { owner_filter: { userid_list: ownerFilterUserids } }
|
||||
: {}),
|
||||
}),
|
||||
signal: AbortSignal.timeout(12_000),
|
||||
});
|
||||
const listPayload = await readEnvelope(listResp, "获取企业微信客户群列表失败");
|
||||
@@ -642,6 +658,7 @@ async function postAppChat(
|
||||
body: Record<string, unknown>,
|
||||
errorLabel: string,
|
||||
fetchImpl: FetchLike,
|
||||
allowErrcodes: number[] = [],
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (!hasAppCredentials(config)) {
|
||||
throw new WecomClientError(
|
||||
@@ -661,7 +678,7 @@ async function postAppChat(
|
||||
if (!response.ok) {
|
||||
throw new WecomClientError(`${errorLabel}(HTTP ${response.status})`, 502);
|
||||
}
|
||||
ensureOk(payload, errorLabel);
|
||||
ensureOk(payload, errorLabel, allowErrcodes);
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -684,9 +701,12 @@ export async function createAppChat(
|
||||
const payload = await postAppChat(
|
||||
config,
|
||||
"/cgi-bin/appchat/create",
|
||||
{ name, owner: Number(config.agentId), userlist, chatid },
|
||||
// owner 不传:默认建群应用自己为群主,避免把 agentId 误当 userid
|
||||
{ name, userlist, chatid },
|
||||
"创建企业微信群聊会话失败",
|
||||
fetchImpl,
|
||||
// 86215 = chatid 已存在:chatid 由本项目派生(pgc/tgc+ID),视为本应用已建过的群
|
||||
[86215],
|
||||
);
|
||||
return { chatid: bindingValue(payload.chatid) || chatid };
|
||||
}
|
||||
@@ -767,6 +787,105 @@ export async function sendAppChatMessage(
|
||||
);
|
||||
}
|
||||
|
||||
export type TaskJoinWayInput = {
|
||||
chatIdList: string[];
|
||||
roomBaseName: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
function joinWayBody(input: TaskJoinWayInput): Record<string, unknown> {
|
||||
return {
|
||||
scene: 2,
|
||||
chat_id_list: input.chatIdList
|
||||
.map((id) => bindingValue(id))
|
||||
.filter((id) => id.length > 0)
|
||||
.slice(0, 5),
|
||||
auto_create_room: 1,
|
||||
room_base_name: Array.from(input.roomBaseName).slice(0, 40).join(""),
|
||||
state: Array.from(input.state).slice(0, 30).join(""),
|
||||
};
|
||||
}
|
||||
|
||||
export async function addTaskJoinWay(
|
||||
config: WecomConfig,
|
||||
input: TaskJoinWayInput,
|
||||
fetchImpl: FetchLike = fetch,
|
||||
): Promise<{ configId: string }> {
|
||||
const payload = await postAppChat(
|
||||
config,
|
||||
"/cgi-bin/externalcontact/groupchat/add_join_way",
|
||||
joinWayBody(input),
|
||||
"创建企业微信进群活码失败",
|
||||
fetchImpl,
|
||||
);
|
||||
const configId = bindingValue(payload.config_id);
|
||||
if (!configId) {
|
||||
throw new WecomClientError("创建企业微信进群活码失败(未返回 config_id)", 502);
|
||||
}
|
||||
return { configId };
|
||||
}
|
||||
|
||||
export async function getJoinWayQrCode(
|
||||
config: WecomConfig,
|
||||
configId: string,
|
||||
fetchImpl: FetchLike = fetch,
|
||||
): Promise<{ qrCode: string }> {
|
||||
const payload = await postAppChat(
|
||||
config,
|
||||
"/cgi-bin/externalcontact/groupchat/get_join_way",
|
||||
{ config_id: bindingValue(configId) },
|
||||
"获取企业微信进群活码二维码失败",
|
||||
fetchImpl,
|
||||
);
|
||||
const joinWay = payload.join_way as { qr_code?: unknown } | undefined;
|
||||
const qrCode = bindingValue(joinWay?.qr_code);
|
||||
if (!qrCode) {
|
||||
throw new WecomClientError("获取企业微信进群活码二维码失败(未返回二维码)", 502);
|
||||
}
|
||||
return { qrCode };
|
||||
}
|
||||
|
||||
export async function updateTaskJoinWay(
|
||||
config: WecomConfig,
|
||||
input: TaskJoinWayInput & { configId: string },
|
||||
fetchImpl: FetchLike = fetch,
|
||||
): Promise<void> {
|
||||
const configId = bindingValue(input.configId);
|
||||
if (!configId) {
|
||||
throw new WecomClientError("更新企业微信进群活码失败(缺少 config_id)", 400);
|
||||
}
|
||||
// update_join_way 为覆盖式更新,需重传全部字段(含 scene / auto_create_room)
|
||||
await postAppChat(
|
||||
config,
|
||||
"/cgi-bin/externalcontact/groupchat/update_join_way",
|
||||
{ ...joinWayBody(input), config_id: configId },
|
||||
"更新企业微信进群活码失败",
|
||||
fetchImpl,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteJoinWay(
|
||||
config: WecomConfig,
|
||||
configId: string,
|
||||
fetchImpl: FetchLike = fetch,
|
||||
): Promise<void> {
|
||||
const configIdValue = bindingValue(configId);
|
||||
if (!configIdValue) return;
|
||||
try {
|
||||
await postAppChat(
|
||||
config,
|
||||
"/cgi-bin/externalcontact/groupchat/del_join_way",
|
||||
{ config_id: configIdValue },
|
||||
"删除企业微信进群活码失败",
|
||||
fetchImpl,
|
||||
);
|
||||
} catch (error) {
|
||||
// 活码配置可能已被企微侧清理(群解散等),删除是尽力而为,不阻断本地解绑
|
||||
if (error instanceof WecomClientError) return;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getWecomJson(
|
||||
config: WecomConfig,
|
||||
path: string,
|
||||
|
||||
Reference in New Issue
Block a user