feat: migrate legacy projects through binding search

This commit is contained in:
2026-07-11 23:22:07 +08:00
parent 53998d2651
commit 530fcdd2b7
11 changed files with 873 additions and 31 deletions
+9 -1
View File
@@ -22,6 +22,7 @@ export function buildUnboundChatOnboardingCard(params: {
readonly folders: readonly OnboardingFolderOption[];
readonly projects: readonly OnboardingProjectOption[];
readonly canCreateProject: boolean;
readonly searchQuery?: string | undefined;
}): Record<string, unknown> {
const actions: unknown[] = [];
if (params.canCreateProject) {
@@ -64,7 +65,14 @@ export function buildUnboundChatOnboardingCard(params: {
`这个飞书群还没有绑定项目。`,
``,
`组织: **${escapeMarkdown(params.organizationName)}**`,
`可以选择 folder 新建项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`,
...(params.searchQuery === undefined || params.searchQuery === ""
? [`可以选择 folder 新建项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`]
: [
`项目搜索: **${escapeMarkdown(params.searchQuery)}**`,
params.projects.length === 0
? `没有匹配的未绑定项目。请 @bot 后换一个项目关键词。`
: `请选择匹配项目绑定到本群;如未找到,请 @bot 后换一个更具体的关键词。`,
]),
].join("\n"),
},
];
+43 -26
View File
@@ -1070,10 +1070,12 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
const settings = await ensureOrganizationProjectSettings(deps.prisma, organization.organizationId);
const canCreateProject = settings.membersCanCreateProjects || isOrgAdminRole(organization.role);
const searchQuery = (extractPrompt(msg) ?? "").trim().slice(0, 100);
const projects = await listBindableProjectsForActor({
organizationId: organization.organizationId,
actorFeishuOpenId: senderOpenId,
isOrgAdmin: isOrgAdminRole(organization.role),
searchQuery,
});
const folders = await listCreatableRootFolders(organization.organizationId);
@@ -1086,6 +1088,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
folders,
projects,
canCreateProject,
searchQuery,
}),
sendOptionsForTriggerMessage(msg),
);
@@ -1182,37 +1185,51 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
readonly organizationId: string;
readonly actorFeishuOpenId: string;
readonly isOrgAdmin: boolean;
readonly searchQuery: string;
}): Promise<readonly OnboardingProjectOption[]> {
const candidates = await deps.prisma.project.findMany({
where: {
const allowed: OnboardingProjectOption[] = [];
let cursor: string | undefined;
while (allowed.length < 5) {
const candidates = await deps.prisma.project.findMany({
where: {
organizationId: input.organizationId,
archivedAt: null,
groupBindings: { none: { archivedAt: null } },
},
select: {
id: true,
name: true,
folder: { select: { name: true } },
},
orderBy: { updatedAt: "desc" },
take: 20,
});
const allowed: OnboardingProjectOption[] = [];
for (const project of candidates) {
if (!input.isOrgAdmin) {
const decision = await authorizer.can({
actor: { feishuOpenId: input.actorFeishuOpenId },
action: "collaborator.manage",
resource: { type: "PROJECT", id: project.id },
});
if (!decision.allowed) continue;
}
allowed.push({
projectId: project.id,
name: project.name,
...(project.folder?.name !== undefined ? { folderName: project.folder.name } : {}),
...(input.searchQuery === "" ? {} : {
OR: [
{ name: { contains: input.searchQuery, mode: "insensitive" } },
{ folder: { name: { contains: input.searchQuery, mode: "insensitive" } } },
],
}),
},
select: {
id: true,
name: true,
folder: { select: { name: true } },
},
orderBy: [{ updatedAt: "desc" }, { id: "desc" }],
take: 20,
...(cursor === undefined ? {} : { cursor: { id: cursor }, skip: 1 }),
});
if (allowed.length >= 5) break;
for (const project of candidates) {
if (!input.isOrgAdmin) {
const decision = await authorizer.can({
actor: { feishuOpenId: input.actorFeishuOpenId },
action: "collaborator.manage",
resource: { type: "PROJECT", id: project.id },
});
if (!decision.allowed) continue;
}
allowed.push({
projectId: project.id,
name: project.name,
...(project.folder?.name !== undefined ? { folderName: project.folder.name } : {}),
});
if (allowed.length >= 5) break;
}
if (candidates.length < 20) break;
cursor = candidates.at(-1)?.id;
if (cursor === undefined) break;
}
return allowed;
}