- tasks 新增 recovery_enabled/recovery_due_at/recovery_instructions/recovery_example_images,回收挂自己名下(先发布后交截图),不再置 complete - 存量 screenshot_collect 迁移并入父任务后删除(mysql/0020 含 DELETE,生产执行前务必备份) - 新 action task_enable_recovery:ghost 面板一键开通回收,截止默认=发布截止 - 企微催办双阶段(待发布/截图待提交);超期自动释放仅发布阶段 - admin 删 groupTaskMasters 聚合改单卡双面板;portal 详情页回收截图区块+批次回收进度 - 测试 309 全绿 + Playwright E2E 20/20
1262 lines
45 KiB
TypeScript
1262 lines
45 KiB
TypeScript
import { getRuntimeEnv } from "../../../lib/runtime-env";
|
||
import { runInBackground } from "../../../lib/background";
|
||
|
||
const env = getRuntimeEnv();
|
||
import {
|
||
backfillAccountProfiles,
|
||
enrichDistributionAccount,
|
||
} from "../../../lib/account-enrichment-service";
|
||
import {
|
||
ensureSchema,
|
||
getDashboardData,
|
||
getRawDb,
|
||
uid,
|
||
} from "../../../lib/mvp-db";
|
||
import {
|
||
collectDistributionMetrics,
|
||
createCollectionRunTasks,
|
||
retryFailedCollections,
|
||
runDueScheduledCollections,
|
||
shanghaiDateFromTimestamp,
|
||
} from "../../../lib/collection-service";
|
||
import {
|
||
resolveCollectionMcpConfig,
|
||
type CollectionMcpBindings,
|
||
} from "../../../lib/mcp-collection-client";
|
||
import { adminForbidden, isAdminRequest } from "../../../lib/admin-auth";
|
||
import {
|
||
FeishuSourceError,
|
||
readFeishuSource,
|
||
type FeishuBindings,
|
||
} from "../../../lib/feishu-client";
|
||
import {
|
||
createDistributionTask,
|
||
enableTaskRecovery,
|
||
updateTask,
|
||
} from "../../../lib/task-service";
|
||
import {
|
||
DistributionReleaseError,
|
||
releaseUnfinishedDistribution,
|
||
} from "../../../lib/distribution-release-service";
|
||
import {
|
||
resolveWecomConfig,
|
||
sendWecomAppMessage,
|
||
sendWecomRobotMessage,
|
||
listExternalContacts,
|
||
listInternalContacts,
|
||
WecomClientError,
|
||
type WecomBindings,
|
||
} from "../../../lib/wecom-client";
|
||
import {
|
||
runDueSoonWecomNotifications,
|
||
runOverdueAutoRelease,
|
||
runOverdueEscalation,
|
||
} from "../../../lib/wecom-notifier-service";
|
||
import {
|
||
listGroupChatRows,
|
||
listGroupPushes,
|
||
pushTaskToGroupChats,
|
||
syncGroupChats,
|
||
} from "../../../lib/wecom-group-push-service";
|
||
import {
|
||
bindTaskCustomerChat,
|
||
unbindTaskCustomerChat,
|
||
} from "../../../lib/task-customer-chat-service";
|
||
import { isManagerRequest, managerForbidden } from "../../../lib/user-auth";
|
||
import {
|
||
getOwnerScope,
|
||
isDistributionOwner,
|
||
isProjectOwner,
|
||
isTaskOwner,
|
||
isTaskProjectOwner,
|
||
ownerForbidden,
|
||
ownerScopeUserid,
|
||
projectForbidden,
|
||
} from "../../../lib/owner-scope";
|
||
import { extractPublishUrl } from "../../../lib/publish-url";
|
||
import {
|
||
getAccountFieldRefreshStatus,
|
||
startAccountFieldRefresh,
|
||
} from "../../../lib/account-field-refresh-service";
|
||
import {
|
||
deleteProject,
|
||
ensureProjectMainChat,
|
||
findOrCreateGroupOwnerPartner,
|
||
getProjectDetail,
|
||
listGroupOwnerPartners,
|
||
listProjects,
|
||
ProjectError,
|
||
saveGroupOwnerPartner,
|
||
saveProject,
|
||
} from "../../../lib/project-service";
|
||
import { assignTaskOwner } from "../../../lib/task-group-service";
|
||
import { pushProjectTask } from "../../../lib/project-push-service";
|
||
|
||
type ActionBody = {
|
||
action?: string;
|
||
[key: string]: unknown;
|
||
};
|
||
|
||
function numberValue(value: unknown, fallback = 0) {
|
||
const parsed = Number(value);
|
||
return Number.isFinite(parsed) ? parsed : fallback;
|
||
}
|
||
|
||
async function assignOwnerQuietly(taskId: string, ownerUserid: unknown) {
|
||
const owner = String(ownerUserid ?? "").trim();
|
||
if (!owner) return "";
|
||
try {
|
||
const assignment = await assignTaskOwner(
|
||
getRawDb(),
|
||
{ taskId, ownerUserid: owner },
|
||
resolveWecomConfig(env as unknown as WecomBindings),
|
||
);
|
||
return assignment.warning ?? "";
|
||
} catch (error) {
|
||
return error instanceof Error ? error.message : String(error);
|
||
}
|
||
}
|
||
|
||
export async function POST(request: Request) {
|
||
if (!(await isAdminRequest(request))) return adminForbidden();
|
||
try {
|
||
await ensureSchema();
|
||
const body = (await request.json()) as ActionBody;
|
||
const db = getRawDb();
|
||
let ownerWarning = "";
|
||
const scope = await getOwnerScope(request);
|
||
const managerGate = () =>
|
||
scope.kind === "owner" ? managerForbidden() : null;
|
||
const taskOwnerGate = async (taskId: unknown, options?: { strict?: boolean }) => {
|
||
if (scope.kind === "full") return null;
|
||
const id = String(taskId ?? "").trim();
|
||
return id &&
|
||
(await isTaskOwner(db, id, scope.wecomUserId, options))
|
||
? null
|
||
: ownerForbidden();
|
||
};
|
||
const distributionOwnerGate = async (distributionId: unknown) => {
|
||
if (scope.kind === "full") return null;
|
||
const id = String(distributionId ?? "").trim();
|
||
return id && (await isDistributionOwner(db, id, scope.wecomUserId))
|
||
? null
|
||
: ownerForbidden();
|
||
};
|
||
const projectOwnerGate = async (projectId: unknown) => {
|
||
if (scope.kind === "full") return null;
|
||
const id = String(projectId ?? "").trim();
|
||
return id && (await isProjectOwner(db, id, scope.wecomUserId))
|
||
? null
|
||
: projectForbidden();
|
||
};
|
||
const taskProjectOwnerGate = async (taskId: unknown) => {
|
||
if (scope.kind === "full") return null;
|
||
const id = String(taskId ?? "").trim();
|
||
return id && (await isTaskProjectOwner(db, id, scope.wecomUserId))
|
||
? null
|
||
: projectForbidden();
|
||
};
|
||
|
||
if (body.action === "inspect_feishu") {
|
||
const source = await readFeishuSource(
|
||
String(body.feishuUrl ?? "").trim(),
|
||
env as unknown as FeishuBindings,
|
||
);
|
||
return Response.json({
|
||
sheetId: source.sheetId,
|
||
sheetName: source.sheetName,
|
||
syncedAt: source.syncedAt,
|
||
rowCount: source.rows.length,
|
||
imageCount: source.rows.reduce(
|
||
(total, row) => total + row.images.length,
|
||
0,
|
||
),
|
||
videoCount: source.rows.reduce(
|
||
(total, row) => total + row.videos.length,
|
||
0,
|
||
),
|
||
columns: source.columns,
|
||
preview: source.rows.slice(0, 3),
|
||
});
|
||
}
|
||
|
||
if (body.action === "create_task") {
|
||
const forbidden = await projectOwnerGate(body.projectId);
|
||
if (forbidden) return forbidden;
|
||
const name = String(body.name ?? "").trim();
|
||
const brand = String(body.brand ?? "").trim();
|
||
const dueAt = String(body.dueAt ?? "").trim();
|
||
const platform = body.platform === "抖音" ? "抖音" : "小红书";
|
||
const contentFormat = body.contentFormat === "video" ? "video" : "image_text";
|
||
if (!name || !brand || !dueAt) {
|
||
return Response.json(
|
||
{ error: "请补全任务名称、品牌和截止日期" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
const creation = await createDistributionTask(
|
||
{
|
||
feishuUrl: String(body.feishuUrl ?? "").trim(),
|
||
name,
|
||
brand,
|
||
dueAt,
|
||
platform,
|
||
contentFormat,
|
||
projectId: String(body.projectId ?? "").trim(),
|
||
ownerUserid: String(body.ownerUserid ?? "").trim(),
|
||
recoveryEnabled: body.recoveryEnabled !== false,
|
||
recoveryDueAt: String(body.recoveryDueAt ?? "").trim(),
|
||
recoveryInstructions: String(body.recoveryInstructions ?? "").trim(),
|
||
recoveryExampleImageKey: String(body.recoveryExampleImageKey ?? "").trim(),
|
||
},
|
||
env as unknown as FeishuBindings,
|
||
);
|
||
ownerWarning = await assignOwnerQuietly(creation.taskId, body.ownerUserid);
|
||
} else if (body.action === "task_enable_recovery") {
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
if (!taskId) {
|
||
return Response.json({ error: "缺少 taskId" }, { status: 400 });
|
||
}
|
||
const taskRow = await db
|
||
.prepare("SELECT project_id FROM tasks WHERE id = ?")
|
||
.bind(taskId)
|
||
.first<{ project_id: string }>();
|
||
if (!taskRow) {
|
||
return Response.json({ error: "任务不存在" }, { status: 404 });
|
||
}
|
||
const forbidden = await projectOwnerGate(taskRow.project_id);
|
||
if (forbidden) return forbidden;
|
||
await enableTaskRecovery({
|
||
taskId,
|
||
recoveryDueAt: String(body.recoveryDueAt ?? "").trim(),
|
||
recoveryInstructions: String(body.recoveryInstructions ?? "").trim(),
|
||
recoveryExampleImageKey: String(body.recoveryExampleImageKey ?? "").trim(),
|
||
});
|
||
} else if (body.action === "claim") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const partnerId = String(body.partnerId ?? "");
|
||
const taskId = String(body.taskId ?? "");
|
||
const quantity = Math.max(1, Math.min(50, numberValue(body.quantity, 1)));
|
||
const available = await db
|
||
.prepare(
|
||
`SELECT id FROM contents
|
||
WHERE task_id = ? AND status = 'available'
|
||
ORDER BY created_at, id
|
||
LIMIT ${quantity}`,
|
||
)
|
||
.bind(taskId)
|
||
.all<{ id: string }>();
|
||
if (available.results.length === 0) {
|
||
return Response.json({ error: "当前没有可领取内容" }, { status: 409 });
|
||
}
|
||
const statements = available.results.flatMap((content) => [
|
||
db
|
||
.prepare(
|
||
`INSERT INTO distributions
|
||
(id, task_id, content_id, partner_id, status)
|
||
VALUES (?, ?, ?, ?, 'claimed')`,
|
||
)
|
||
.bind(uid("dist"), taskId, content.id, partnerId),
|
||
db
|
||
.prepare("UPDATE contents SET status = 'allocated' WHERE id = ?")
|
||
.bind(content.id),
|
||
]);
|
||
statements.push(
|
||
db
|
||
.prepare(
|
||
`UPDATE tasks SET claimed_quantity = claimed_quantity + ?
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(available.results.length, taskId),
|
||
db
|
||
.prepare(
|
||
`UPDATE partners SET claimed_total = claimed_total + ?
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(available.results.length, partnerId),
|
||
);
|
||
await db.batch(statements);
|
||
} else if (body.action === "release_distribution") {
|
||
if (!(await isManagerRequest(request))) return adminForbidden();
|
||
await releaseUnfinishedDistribution(
|
||
db,
|
||
String(body.distributionId ?? "").trim(),
|
||
);
|
||
} else if (body.action === "update_distribution_publish_url") {
|
||
if (!(await isManagerRequest(request))) return adminForbidden();
|
||
const distributionId = String(body.distributionId ?? "").trim();
|
||
if (!distributionId) {
|
||
return Response.json(
|
||
{ error: "作品记录不存在" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
const current = await db
|
||
.prepare(
|
||
`SELECT d.id, d.task_id, d.partner_id, d.publish_url,
|
||
t.task_type, t.platform, t.collection_start_date, t.collection_days,
|
||
COALESCE(a.nickname, '待识别账号') AS account_nickname
|
||
FROM distributions d
|
||
JOIN tasks t ON t.id = d.task_id
|
||
LEFT JOIN accounts a ON a.id = d.account_id
|
||
WHERE d.id = ?`,
|
||
)
|
||
.bind(distributionId)
|
||
.first<{
|
||
id: string;
|
||
task_id: string;
|
||
partner_id: string;
|
||
publish_url: string | null;
|
||
task_type?: string | null;
|
||
platform: string;
|
||
collection_start_date: string | null;
|
||
collection_days: string;
|
||
account_nickname: string;
|
||
}>();
|
||
if (!current) {
|
||
return Response.json({ error: "作品记录不存在" }, { status: 404 });
|
||
}
|
||
if (current.task_type === "screenshot_collect") {
|
||
return Response.json(
|
||
{ error: "截图回收任务不需要填写发布链接" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
const platform = current.platform === "抖音" ? "抖音" : "小红书";
|
||
const publishUrl = extractPublishUrl(
|
||
String(body.publishUrl ?? "").trim(),
|
||
platform,
|
||
);
|
||
if (!publishUrl) {
|
||
return Response.json(
|
||
{ error: `请填写包含${platform}作品链接的发布内容` },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
if (current.publish_url === publishUrl) {
|
||
return Response.json(await getDashboardData());
|
||
}
|
||
let collectionDays: number[] = [];
|
||
try {
|
||
const parsed = JSON.parse(current.collection_days || "[]");
|
||
if (Array.isArray(parsed)) {
|
||
collectionDays = [...new Set(parsed.map(Number))]
|
||
.filter(
|
||
(day) =>
|
||
Number.isInteger(day) && day >= 1 && day <= 7,
|
||
)
|
||
.sort((a, b) => a - b);
|
||
}
|
||
} catch {
|
||
collectionDays = [];
|
||
}
|
||
const isScheduled = Boolean(
|
||
current.collection_start_date && collectionDays.length > 0,
|
||
);
|
||
const statements = [
|
||
db
|
||
.prepare(
|
||
`UPDATE distributions SET
|
||
publish_url = ?,
|
||
publish_time = CURRENT_TIMESTAMP,
|
||
status = 'published',
|
||
d2_likes = NULL,
|
||
d2_comments = NULL,
|
||
d2_collects = NULL,
|
||
d5_likes = NULL,
|
||
d5_comments = NULL,
|
||
d5_collects = NULL,
|
||
d7_likes = NULL,
|
||
d7_comments = NULL,
|
||
d7_collects = NULL,
|
||
latest_likes = NULL,
|
||
latest_comments = NULL,
|
||
latest_collects = NULL,
|
||
latest_shares = NULL,
|
||
collection_status = ?,
|
||
collection_status_description = ?,
|
||
collection_updated_at = NULL,
|
||
last_collection_day = NULL,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(
|
||
publishUrl,
|
||
isScheduled ? "scheduled" : "pending",
|
||
isScheduled
|
||
? `管理员已更新链接,等待${collectionDays.length}个采集日`
|
||
: "管理员已更新链接,等待设置采集计划",
|
||
distributionId,
|
||
),
|
||
db
|
||
.prepare("DELETE FROM collection_runs WHERE distribution_id = ?")
|
||
.bind(distributionId),
|
||
];
|
||
if (!current.publish_url) {
|
||
statements.push(
|
||
db
|
||
.prepare(
|
||
`UPDATE partners SET completed_total = completed_total + 1
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(current.partner_id),
|
||
);
|
||
}
|
||
await db.batch(statements);
|
||
if (isScheduled && current.collection_start_date) {
|
||
await createCollectionRunTasks(
|
||
db,
|
||
current.task_id,
|
||
current.collection_start_date,
|
||
collectionDays,
|
||
);
|
||
runInBackground(
|
||
runDueScheduledCollections(
|
||
db,
|
||
Date.now(),
|
||
resolveCollectionMcpConfig(
|
||
env as unknown as CollectionMcpBindings,
|
||
),
|
||
"catchup",
|
||
current.task_id,
|
||
).catch(() => undefined),
|
||
"collection catchup after publish URL update",
|
||
);
|
||
}
|
||
runInBackground(
|
||
enrichDistributionAccount(
|
||
db,
|
||
distributionId,
|
||
publishUrl,
|
||
current.account_nickname,
|
||
resolveCollectionMcpConfig(
|
||
env as unknown as CollectionMcpBindings,
|
||
),
|
||
).catch(() => undefined),
|
||
"account enrichment after publish URL update",
|
||
);
|
||
} else if (body.action === "save_collection_schedule") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
const startDate = String(body.startDate ?? "").trim();
|
||
const days = Array.isArray(body.days)
|
||
? [...new Set(body.days.map(Number))]
|
||
.filter(
|
||
(day) =>
|
||
Number.isInteger(day) && day >= 1 && day <= 7,
|
||
)
|
||
.sort((a, b) => a - b)
|
||
: [];
|
||
if (!taskId || !/^\d{4}-\d{2}-\d{2}$/.test(startDate)) {
|
||
return Response.json(
|
||
{ error: "请选择有效的开始采集日期" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
if (days.length === 0) {
|
||
return Response.json(
|
||
{ error: "请至少选择一个自动采集日" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
const task = await db
|
||
.prepare("SELECT id, task_type FROM tasks WHERE id = ?")
|
||
.bind(taskId)
|
||
.first<{ id: string; task_type?: string | null }>();
|
||
if (!task) {
|
||
return Response.json({ error: "任务不存在" }, { status: 404 });
|
||
}
|
||
if (task.task_type === "screenshot_collect") {
|
||
return Response.json(
|
||
{ error: "截图回收任务不需要设置数据采集计划" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
await db.batch([
|
||
db
|
||
.prepare(
|
||
`UPDATE tasks
|
||
SET collection_start_date = ?,
|
||
collection_days = ?,
|
||
collection_schedule_updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(startDate, JSON.stringify(days), taskId),
|
||
db
|
||
.prepare(
|
||
`UPDATE distributions
|
||
SET collection_status = CASE
|
||
WHEN latest_likes IS NULL THEN 'scheduled'
|
||
ELSE collection_status
|
||
END,
|
||
collection_status_description = CASE
|
||
WHEN latest_likes IS NULL THEN ?
|
||
ELSE collection_status_description
|
||
END,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE task_id = ?
|
||
AND publish_url IS NOT NULL
|
||
AND publish_url != ''`,
|
||
)
|
||
.bind(
|
||
`已安排${days.length}个采集日,每日09:00执行`,
|
||
taskId,
|
||
),
|
||
]);
|
||
await createCollectionRunTasks(db, taskId, startDate, days);
|
||
const catchup = runDueScheduledCollections(
|
||
db,
|
||
Date.now(),
|
||
resolveCollectionMcpConfig(
|
||
env as unknown as CollectionMcpBindings,
|
||
),
|
||
"catchup",
|
||
taskId,
|
||
).catch(() => undefined);
|
||
runInBackground(catchup, "collection catchup");
|
||
} else if (body.action === "run_due_collections") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
await runDueScheduledCollections(
|
||
db,
|
||
Date.now(),
|
||
resolveCollectionMcpConfig(
|
||
env as unknown as CollectionMcpBindings,
|
||
),
|
||
"catchup",
|
||
);
|
||
} else if (
|
||
body.action === "collect_now" ||
|
||
body.action === "collect"
|
||
) {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const distributionId = String(body.distributionId ?? "");
|
||
const rawDay =
|
||
body.day === null || body.day === undefined
|
||
? null
|
||
: numberValue(body.day);
|
||
const day =
|
||
rawDay !== null && Number.isInteger(rawDay) && rawDay >= 1 && rawDay <= 7
|
||
? rawDay
|
||
: null;
|
||
if (body.action === "collect" && day === null) {
|
||
return Response.json({ error: "采集周期无效" }, { status: 400 });
|
||
}
|
||
const distribution = await db
|
||
.prepare(
|
||
`SELECT t.task_type
|
||
FROM distributions d
|
||
JOIN tasks t ON t.id = d.task_id
|
||
WHERE d.id = ?`,
|
||
)
|
||
.bind(distributionId)
|
||
.first<{ task_type?: string | null }>();
|
||
if (!distribution) {
|
||
return Response.json({ error: "笔记记录不存在" }, { status: 404 });
|
||
}
|
||
if (distribution.task_type === "screenshot_collect") {
|
||
return Response.json(
|
||
{ error: "截图回收任务不支持公开数据采集" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
await collectDistributionMetrics(
|
||
db,
|
||
distributionId,
|
||
shanghaiDateFromTimestamp(Date.now()),
|
||
day,
|
||
"manual",
|
||
resolveCollectionMcpConfig(
|
||
env as unknown as CollectionMcpBindings,
|
||
),
|
||
);
|
||
} else if (body.action === "retry_failed_collections") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
if (!taskId) {
|
||
return Response.json({ error: "请选择需要补采的任务" }, { status: 400 });
|
||
}
|
||
const task = await db
|
||
.prepare("SELECT task_type FROM tasks WHERE id = ?")
|
||
.bind(taskId)
|
||
.first<{ task_type?: string | null }>();
|
||
if (!task) {
|
||
return Response.json({ error: "任务不存在" }, { status: 404 });
|
||
}
|
||
if (task.task_type === "screenshot_collect") {
|
||
return Response.json(
|
||
{ error: "截图回收任务没有需要补采的公开数据" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
await retryFailedCollections(
|
||
db,
|
||
taskId,
|
||
resolveCollectionMcpConfig(
|
||
env as unknown as CollectionMcpBindings,
|
||
),
|
||
);
|
||
} else if (body.action === "backfill_account_profiles") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const backfill = backfillAccountProfiles(
|
||
db,
|
||
resolveCollectionMcpConfig(
|
||
env as unknown as CollectionMcpBindings,
|
||
),
|
||
Math.max(1, Math.min(25, numberValue(body.limit, 10))),
|
||
).catch(() => undefined);
|
||
runInBackground(backfill, "account profile backfill");
|
||
} else if (body.action === "set_public_account_ids") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const items = Array.isArray(body.items) ? body.items.slice(0, 50) : [];
|
||
const normalized = items
|
||
.map((item) => {
|
||
const record =
|
||
item && typeof item === "object"
|
||
? (item as Record<string, unknown>)
|
||
: {};
|
||
return {
|
||
accountId: String(record.accountId ?? "").trim().slice(0, 80),
|
||
publicAccountId: String(record.publicAccountId ?? "")
|
||
.trim()
|
||
.slice(0, 80),
|
||
};
|
||
})
|
||
.filter(
|
||
(item) =>
|
||
item.accountId &&
|
||
/^[\p{L}\p{N}._-]{2,80}$/u.test(item.publicAccountId),
|
||
);
|
||
if (normalized.length === 0) {
|
||
return Response.json(
|
||
{ error: "没有可回填的账号号值" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
await db.batch(
|
||
normalized.map((item) =>
|
||
db
|
||
.prepare(
|
||
`UPDATE accounts
|
||
SET public_account_id = ?,
|
||
last_seen_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(item.publicAccountId, item.accountId),
|
||
),
|
||
);
|
||
} else if (body.action === "manual_metrics") {
|
||
const forbidden = await distributionOwnerGate(body.distributionId);
|
||
if (forbidden) return forbidden;
|
||
const distributionId = String(body.distributionId ?? "");
|
||
const exposure = Math.max(0, numberValue(body.exposure));
|
||
const views = Math.max(0, numberValue(body.views));
|
||
await db
|
||
.prepare(
|
||
`UPDATE distributions SET
|
||
exposure = ?,
|
||
views = ?,
|
||
ocr_status = 'manual',
|
||
status = CASE WHEN d7_likes IS NOT NULL THEN 'complete' ELSE status END,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(exposure, views, distributionId)
|
||
.run();
|
||
} else if (body.action === "bind_wecom_external_id") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const partnerId = String(body.partnerId ?? "").trim().slice(0, 80);
|
||
const externalId = String(body.wecomExternalUserId ?? "")
|
||
.trim()
|
||
.slice(0, 128);
|
||
const wecomName = String(body.wecomName ?? "").trim().slice(0, 120);
|
||
if (!partnerId) {
|
||
return Response.json(
|
||
{ error: "缺少 partnerId" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
if (wecomName) {
|
||
await db
|
||
.prepare(
|
||
"UPDATE partners SET wecom_external_user_id = ?, wecom_name = ? WHERE id = ?",
|
||
)
|
||
.bind(externalId || null, wecomName, partnerId)
|
||
.run();
|
||
} else {
|
||
await db
|
||
.prepare(
|
||
"UPDATE partners SET wecom_external_user_id = ? WHERE id = ?",
|
||
)
|
||
.bind(externalId || null, partnerId)
|
||
.run();
|
||
}
|
||
return Response.json({
|
||
partnerId,
|
||
wecomExternalUserId: externalId || null,
|
||
wecomName,
|
||
});
|
||
} else if (body.action === "partner_bind_account") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const partnerId = String(body.partnerId ?? "").trim();
|
||
const accountId = String(body.accountId ?? "").trim();
|
||
if (!partnerId) {
|
||
return Response.json({ error: "缺少 partnerId" }, { status: 400 });
|
||
}
|
||
const partner = await db
|
||
.prepare("SELECT id, name, partner_type, account_id FROM partners WHERE id = ?")
|
||
.bind(partnerId)
|
||
.first<{ id: string; name: string; partner_type: string | null; account_id: string | null }>();
|
||
if (!partner) return Response.json({ error: "合作方不存在" }, { status: 404 });
|
||
if ((partner.partner_type ?? "external_koc") !== "external_koc") {
|
||
return Response.json({ error: "群主合作方不绑定 KOC 资源" }, { status: 400 });
|
||
}
|
||
if (!accountId) {
|
||
await db
|
||
.prepare("UPDATE partners SET account_id = '' WHERE id = ?")
|
||
.bind(partnerId)
|
||
.run();
|
||
return Response.json({ partnerId, accountId: "" });
|
||
}
|
||
const account = await db
|
||
.prepare("SELECT id, nickname FROM accounts WHERE id = ?")
|
||
.bind(accountId)
|
||
.first<{ id: string; nickname: string }>();
|
||
if (!account) return Response.json({ error: "KOC 资源不存在" }, { status: 404 });
|
||
const holder = await db
|
||
.prepare(
|
||
"SELECT id, name FROM partners WHERE account_id = ? AND id != ? LIMIT 1",
|
||
)
|
||
.bind(accountId, partnerId)
|
||
.first<{ id: string; name: string }>();
|
||
if (holder) {
|
||
return Response.json(
|
||
{ error: `该 KOC 资源已绑定合作方「${holder.name}」,请先在对方条目上解绑` },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
await db
|
||
.prepare("UPDATE partners SET account_id = ? WHERE id = ?")
|
||
.bind(accountId, partnerId)
|
||
.run();
|
||
return Response.json({ partnerId, accountId, nickname: account.nickname });
|
||
} else if (body.action === "partner_create_from_account") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const accountId = String(body.accountId ?? "").trim();
|
||
if (!accountId) {
|
||
return Response.json({ error: "缺少 accountId" }, { status: 400 });
|
||
}
|
||
const account = await db
|
||
.prepare("SELECT id, nickname, current_contact FROM accounts WHERE id = ?")
|
||
.bind(accountId)
|
||
.first<{ id: string; nickname: string; current_contact: string }>();
|
||
if (!account) return Response.json({ error: "KOC 资源不存在" }, { status: 404 });
|
||
const holder = await db
|
||
.prepare("SELECT id, name FROM partners WHERE account_id = ? LIMIT 1")
|
||
.bind(accountId)
|
||
.first<{ id: string; name: string }>();
|
||
if (holder) {
|
||
return Response.json(
|
||
{ error: `该 KOC 已是合作方「${holder.name}」,无需重复创建` },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
const partnerId = `partner-koc-${crypto.randomUUID().slice(0, 12)}`;
|
||
await db
|
||
.prepare(
|
||
`INSERT INTO partners
|
||
(id, name, wecom_name, owner, claimed_total, completed_total,
|
||
partner_type, wecom_userid, account_id, created_at)
|
||
VALUES (?, ?, '', '运营组', 0, 0, 'external_koc', '', ?, ?)`,
|
||
)
|
||
.bind(partnerId, account.nickname, accountId, new Date().toISOString())
|
||
.run();
|
||
return Response.json({
|
||
partnerId,
|
||
accountId,
|
||
nickname: account.nickname,
|
||
contact: account.current_contact ?? "",
|
||
});
|
||
} else if (body.action === "send_test_wecom") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const wecomConfig = resolveWecomConfig(
|
||
env as unknown as WecomBindings,
|
||
);
|
||
const partnerId = String(body.partnerId ?? "").trim();
|
||
let partnerExternalId: string | null = null;
|
||
if (partnerId) {
|
||
const row = await db
|
||
.prepare(
|
||
"SELECT wecom_external_user_id FROM partners WHERE id = ?",
|
||
)
|
||
.bind(partnerId)
|
||
.first<{ wecom_external_user_id: string | null }>();
|
||
partnerExternalId = row?.wecom_external_user_id ?? null;
|
||
}
|
||
const testContent = `[KOC LOOP 测试] 群机器人连通性正常,时间 ${new Date().toISOString()}`;
|
||
let robotStatus: "ok" | "skipped" = "skipped";
|
||
if (wecomConfig.robotWebhook) {
|
||
await sendWecomRobotMessage(testContent, wecomConfig);
|
||
robotStatus = "ok";
|
||
}
|
||
let appStatus: "ok" | "skipped" | "failed" = "skipped";
|
||
if (
|
||
partnerExternalId &&
|
||
wecomConfig.corpId &&
|
||
wecomConfig.agentId &&
|
||
wecomConfig.secret
|
||
) {
|
||
const result = await sendWecomAppMessage(
|
||
[partnerExternalId],
|
||
testContent,
|
||
wecomConfig,
|
||
);
|
||
appStatus = result.failed > 0 ? "failed" : "ok";
|
||
}
|
||
return Response.json({
|
||
robot: robotStatus,
|
||
app: appStatus,
|
||
});
|
||
} else if (body.action === "wecom_status") {
|
||
const wecomConfig = resolveWecomConfig(
|
||
env as unknown as WecomBindings,
|
||
);
|
||
return Response.json({
|
||
robotConfigured: Boolean(wecomConfig.robotWebhook),
|
||
appConfigured: Boolean(
|
||
wecomConfig.corpId && wecomConfig.agentId && wecomConfig.secret,
|
||
),
|
||
dueDays: wecomConfig.dueDays,
|
||
});
|
||
} else if (body.action === "trigger_wecom_due_soon") {
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
const forbidden = await taskOwnerGate(taskId || undefined);
|
||
if (forbidden) return forbidden;
|
||
const wecomConfig = resolveWecomConfig(
|
||
env as unknown as WecomBindings,
|
||
);
|
||
const summary = await runDueSoonWecomNotifications(
|
||
db,
|
||
wecomConfig,
|
||
undefined,
|
||
undefined,
|
||
taskId || undefined,
|
||
);
|
||
return Response.json(summary);
|
||
} else if (body.action === "trigger_wecom_overdue_escalation") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const wecomConfig = resolveWecomConfig(
|
||
env as unknown as WecomBindings,
|
||
);
|
||
const escalation = await runOverdueEscalation(db, wecomConfig);
|
||
const autoRelease = await runOverdueAutoRelease(db, wecomConfig);
|
||
return Response.json({ escalation, autoRelease });
|
||
} else if (body.action === "wecom_list_external_contacts") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const wecomConfig = resolveWecomConfig(
|
||
env as unknown as WecomBindings,
|
||
);
|
||
const contacts = await listExternalContacts(wecomConfig);
|
||
return Response.json({ contacts });
|
||
} else if (body.action === "wecom_list_internal_contacts") {
|
||
const wecomConfig = resolveWecomConfig(
|
||
env as unknown as WecomBindings,
|
||
);
|
||
const contacts = await listInternalContacts(wecomConfig);
|
||
return Response.json({ contacts });
|
||
} else if (body.action === "wecom_sync_group_chats") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const wecomConfig = resolveWecomConfig(
|
||
env as unknown as WecomBindings,
|
||
);
|
||
const groups = await syncGroupChats(db, wecomConfig);
|
||
return Response.json({ groups, syncedCount: groups.length });
|
||
} else if (body.action === "wecom_push_task_to_groups") {
|
||
const forbidden = await taskOwnerGate(body.taskId);
|
||
if (forbidden) return forbidden;
|
||
const wecomConfig = resolveWecomConfig(
|
||
env as unknown as WecomBindings,
|
||
);
|
||
const summary = await pushTaskToGroupChats(
|
||
db,
|
||
{
|
||
taskId: String(body.taskId ?? ""),
|
||
chatIds: body.chatIds,
|
||
text: body.text,
|
||
},
|
||
wecomConfig,
|
||
);
|
||
const failCount = summary.results.reduce(
|
||
(total, item) => total + item.failList.length,
|
||
0,
|
||
);
|
||
const groupCount = summary.results.reduce(
|
||
(total, item) => total + item.chatCount,
|
||
0,
|
||
);
|
||
return Response.json({
|
||
results: summary.results,
|
||
groupCount,
|
||
failCount,
|
||
hint: "群发任务已创建,群主需在企微客户端「群发助手」点击发送后,消息才会送达客户群",
|
||
});
|
||
} else if (body.action === "wecom_task_group_pushes") {
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
const forbidden = await taskOwnerGate(taskId || undefined);
|
||
if (forbidden) return forbidden;
|
||
const pushes = await listGroupPushes(db, taskId || undefined);
|
||
return Response.json({ pushes });
|
||
} else if (body.action === "project_list") {
|
||
return Response.json({
|
||
projects: await listProjects(db, ownerScopeUserid(scope)),
|
||
});
|
||
} else if (body.action === "project_detail") {
|
||
const forbidden = await projectOwnerGate(body.projectId);
|
||
if (forbidden) return forbidden;
|
||
return Response.json(await getProjectDetail(db, String(body.projectId ?? "")));
|
||
} else if (body.action === "project_save") {
|
||
const projectId = String(body.id ?? "").trim();
|
||
// 项目负责人只能更新自己负责的项目,且归属 ownerUserid 强制保留原值
|
||
if (scope.kind === "owner" && !projectId) return managerForbidden();
|
||
const forbidden = await projectOwnerGate(projectId);
|
||
if (forbidden) return forbidden;
|
||
let nextOwnerUserid = body.ownerUserid;
|
||
if (scope.kind === "owner") {
|
||
const current = await db
|
||
.prepare("SELECT owner_userid FROM projects WHERE id = ?")
|
||
.bind(projectId)
|
||
.first<{ owner_userid: string }>();
|
||
nextOwnerUserid = current?.owner_userid ?? "";
|
||
}
|
||
const saved = await saveProject(
|
||
db,
|
||
{
|
||
id: body.id,
|
||
name: body.name,
|
||
brand: body.brand,
|
||
mainGroupMemberIds: body.mainGroupMemberIds,
|
||
ownerUserid: nextOwnerUserid,
|
||
note: body.note,
|
||
},
|
||
resolveWecomConfig(env as unknown as WecomBindings),
|
||
);
|
||
return Response.json(saved);
|
||
} else if (body.action === "project_delete") {
|
||
const forbidden = await projectOwnerGate(body.projectId);
|
||
if (forbidden) return forbidden;
|
||
await deleteProject(db, String(body.projectId ?? ""));
|
||
return Response.json({ ok: true });
|
||
} else if (body.action === "project_ensure_main_chat") {
|
||
const forbidden = await projectOwnerGate(body.projectId);
|
||
if (forbidden) return forbidden;
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
String(body.projectId ?? ""),
|
||
resolveWecomConfig(env as unknown as WecomBindings),
|
||
);
|
||
return Response.json(result);
|
||
} else if (body.action === "task_set_owner") {
|
||
const forbidden = await taskProjectOwnerGate(body.taskId);
|
||
if (forbidden) return forbidden;
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
const ownerUserid = String(body.ownerUserid ?? "").trim();
|
||
if (!taskId) {
|
||
return Response.json({ error: "缺少 taskId" }, { status: 400 });
|
||
}
|
||
if (!ownerUserid) {
|
||
return Response.json({ error: "请选择任务负责人" }, { status: 400 });
|
||
}
|
||
const existing = await db
|
||
.prepare("SELECT id FROM tasks WHERE id = ?")
|
||
.bind(taskId)
|
||
.first<{ id: string }>();
|
||
if (!existing) {
|
||
return Response.json({ error: "任务不存在" }, { status: 404 });
|
||
}
|
||
const assignment = await assignTaskOwner(
|
||
db,
|
||
{ taskId, ownerUserid },
|
||
resolveWecomConfig(env as unknown as WecomBindings),
|
||
);
|
||
const task = await db
|
||
.prepare("SELECT owner_userid, task_chat_id FROM tasks WHERE id = ?")
|
||
.bind(taskId)
|
||
.first<{ owner_userid: string; task_chat_id: string }>();
|
||
return Response.json({ ok: true, assignment, task });
|
||
} else if (body.action === "task_update") {
|
||
const forbidden = await taskProjectOwnerGate(body.taskId);
|
||
if (forbidden) return forbidden;
|
||
if (scope.kind === "owner") {
|
||
// 迁移目标也必须是本人负责的项目;置空(脱离项目)同样拒绝
|
||
const nextProjectId = String(body.projectId ?? "").trim();
|
||
const projectForbiddenResponse = nextProjectId
|
||
? await projectOwnerGate(nextProjectId)
|
||
: projectForbidden();
|
||
if (projectForbiddenResponse) return projectForbiddenResponse;
|
||
}
|
||
const platform = body.platform === "抖音" || body.platform === "小红书"
|
||
? (body.platform as "小红书" | "抖音")
|
||
: undefined;
|
||
const contentFormat = body.contentFormat === "video" || body.contentFormat === "image_text"
|
||
? (body.contentFormat as "image_text" | "video")
|
||
: undefined;
|
||
const update = await updateTask({
|
||
taskId: String(body.taskId ?? ""),
|
||
name: String(body.name ?? ""),
|
||
brand: String(body.brand ?? ""),
|
||
dueAt: String(body.dueAt ?? ""),
|
||
projectId: String(body.projectId ?? ""),
|
||
platform,
|
||
contentFormat,
|
||
recoveryEnabled:
|
||
body.recoveryEnabled === undefined
|
||
? undefined
|
||
: Boolean(body.recoveryEnabled),
|
||
recoveryDueAt: String(body.recoveryDueAt ?? ""),
|
||
recoveryInstructions: String(body.recoveryInstructions ?? ""),
|
||
recoveryExampleImageKey: String(body.recoveryExampleImageKey ?? ""),
|
||
});
|
||
ownerWarning = await assignOwnerQuietly(update.taskId, body.ownerUserid);
|
||
} else if (body.action === "partner_save") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
return Response.json({
|
||
partner: await saveGroupOwnerPartner(db, {
|
||
id: body.id,
|
||
name: body.name,
|
||
wecomUserid: body.wecomUserid,
|
||
}),
|
||
});
|
||
} else if (body.action === "partner_group_owners") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
return Response.json({ partners: await listGroupOwnerPartners(db) });
|
||
} else if (body.action === "project_push_task") {
|
||
const forbidden = await taskProjectOwnerGate(body.taskId);
|
||
if (forbidden) return forbidden;
|
||
const wecomConfig = resolveWecomConfig(env as unknown as WecomBindings);
|
||
const outcome = await pushProjectTask(
|
||
db,
|
||
{
|
||
taskId: body.taskId,
|
||
sendMainGroup: body.sendMainGroup,
|
||
sendGroupChats: body.sendGroupChats,
|
||
text: body.text,
|
||
},
|
||
wecomConfig,
|
||
);
|
||
const appFailed = outcome.appResults.filter((item) => !item.ok);
|
||
return Response.json({
|
||
...outcome,
|
||
appSent: outcome.appResults.length - appFailed.length,
|
||
appFailedCount: appFailed.length,
|
||
hint: "任务群消息已直接发送至任务内部群(负责人与领取人即时可见)",
|
||
});
|
||
} else if (body.action === "refresh_account_fields") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
const status = startAccountFieldRefresh(
|
||
db,
|
||
resolveCollectionMcpConfig(env as unknown as CollectionMcpBindings),
|
||
body.fields,
|
||
);
|
||
return Response.json({ refresh: status });
|
||
} else if (body.action === "account_field_refresh_status") {
|
||
const forbidden = managerGate();
|
||
if (forbidden) return forbidden;
|
||
return Response.json({ refresh: getAccountFieldRefreshStatus() });
|
||
} else if (body.action === "task_group_links") {
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
const forbidden = await taskOwnerGate(taskId);
|
||
if (forbidden) return forbidden;
|
||
const task = await db
|
||
.prepare(
|
||
"SELECT id, name, share_token, task_chat_id, owner_userid FROM tasks WHERE id = ?",
|
||
)
|
||
.bind(taskId)
|
||
.first<{
|
||
id: string;
|
||
name: string;
|
||
share_token: string | null;
|
||
task_chat_id: string;
|
||
owner_userid: string;
|
||
}>();
|
||
if (!task) return Response.json({ error: "任务不存在" }, { status: 404 });
|
||
const portalUrl = String(env.KOC_PORTAL_URL ?? "").replace(/\/$/, "");
|
||
const ownerUserid = String(task.owner_userid ?? "").trim();
|
||
let owners: {
|
||
partnerId: string;
|
||
name: string;
|
||
wecomUserid: string;
|
||
isInternal: boolean;
|
||
link: string;
|
||
}[] = [];
|
||
if (task.share_token && ownerUserid) {
|
||
try {
|
||
const partner = await findOrCreateGroupOwnerPartner(db, ownerUserid);
|
||
owners = [
|
||
{
|
||
partnerId: partner.id,
|
||
name: partner.name,
|
||
wecomUserid: partner.wecom_userid,
|
||
isInternal: Boolean(partner.wecom_userid),
|
||
link: `${portalUrl}/?task=${task.share_token}&owner=${partner.id}`,
|
||
},
|
||
];
|
||
} catch {
|
||
owners = [];
|
||
}
|
||
}
|
||
return Response.json({
|
||
taskName: task.name,
|
||
taskChatId: task.task_chat_id ?? "",
|
||
claimUrl: task.share_token
|
||
? `${portalUrl}/?task=${task.share_token}`
|
||
: "",
|
||
owners,
|
||
});
|
||
} else if (body.action === "task_customer_chats") {
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
const forbidden = await taskOwnerGate(taskId, { strict: true });
|
||
if (forbidden) return forbidden;
|
||
const task = await db
|
||
.prepare("SELECT id, customer_chat_id FROM tasks WHERE id = ?")
|
||
.bind(taskId)
|
||
.first<{ id: string; customer_chat_id: string }>();
|
||
if (!task) return Response.json({ error: "任务不存在" }, { status: 404 });
|
||
const wecomConfig = resolveWecomConfig(env as unknown as WecomBindings);
|
||
// 列表来自全量群缓存(所有群主的外部客户群),缓存为空或要求刷新时全量同步
|
||
let rows = body.sync === true ? [] : await listGroupChatRows(db);
|
||
if (rows.length === 0) {
|
||
rows = await syncGroupChats(db, wecomConfig);
|
||
}
|
||
return Response.json({
|
||
chats: rows.map((row) => ({
|
||
chatId: row.chat_id,
|
||
name: row.name,
|
||
ownerUserId: row.owner_user_id,
|
||
memberCount: row.member_count,
|
||
status: row.status,
|
||
})),
|
||
boundChatId: String(task.customer_chat_id ?? ""),
|
||
});
|
||
} else if (body.action === "task_bind_customer_chat") {
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
const forbidden = await taskOwnerGate(taskId, { strict: true });
|
||
if (forbidden) return forbidden;
|
||
const task = await db
|
||
.prepare("SELECT id, customer_chat_id FROM tasks WHERE id = ?")
|
||
.bind(taskId)
|
||
.first<{ id: string; customer_chat_id: string }>();
|
||
if (!task) return Response.json({ error: "任务不存在" }, { status: 404 });
|
||
const chatId = String(body.chatId ?? "").trim();
|
||
const wecomConfig = resolveWecomConfig(env as unknown as WecomBindings);
|
||
if (chatId) {
|
||
const chatName = String(body.chatName ?? "").trim();
|
||
const result = await bindTaskCustomerChat(
|
||
db,
|
||
{ taskId, chatId, chatName },
|
||
wecomConfig,
|
||
);
|
||
return Response.json({ task: result });
|
||
}
|
||
await unbindTaskCustomerChat(db, taskId, wecomConfig);
|
||
return Response.json({
|
||
task: {
|
||
customerChatId: "",
|
||
customerChatName: "",
|
||
joinConfigId: "",
|
||
joinQrCode: "",
|
||
},
|
||
});
|
||
} else if (body.action === "task_group_push") {
|
||
const taskId = String(body.taskId ?? "").trim();
|
||
const task = await db
|
||
.prepare(
|
||
"SELECT id, name, share_token, owner_userid FROM tasks WHERE id = ?",
|
||
)
|
||
.bind(taskId)
|
||
.first<{
|
||
id: string;
|
||
name: string;
|
||
share_token: string | null;
|
||
owner_userid: string;
|
||
}>();
|
||
if (!task) return Response.json({ error: "任务不存在" }, { status: 404 });
|
||
const forbidden = await taskOwnerGate(taskId);
|
||
if (forbidden) return forbidden;
|
||
if (!task.share_token) {
|
||
return Response.json({ error: "任务缺少领取链接" }, { status: 400 });
|
||
}
|
||
const ownerUserid = String(task.owner_userid ?? "").trim();
|
||
if (!ownerUserid) {
|
||
return Response.json(
|
||
{ error: "该任务未分配负责人,请先在任务中心分配" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
const owner = await findOrCreateGroupOwnerPartner(db, ownerUserid);
|
||
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[] = [];
|
||
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 });
|
||
} else {
|
||
return Response.json({ error: "不支持的操作" }, { status: 400 });
|
||
}
|
||
|
||
const dashboard = await getDashboardData(ownerScopeUserid(scope));
|
||
return Response.json(
|
||
ownerWarning ? { ...dashboard, ownerWarning } : dashboard,
|
||
);
|
||
} catch (error) {
|
||
return Response.json(
|
||
{ error: error instanceof Error ? error.message : "操作失败" },
|
||
{
|
||
status:
|
||
error instanceof FeishuSourceError ||
|
||
error instanceof DistributionReleaseError ||
|
||
error instanceof WecomClientError ||
|
||
error instanceof ProjectError
|
||
? error.status
|
||
: 500,
|
||
},
|
||
);
|
||
}
|
||
}
|