feat: let Feishu onboarding create projects in folders

This commit is contained in:
2026-07-10 00:35:18 +08:00
parent 519ea8144f
commit 87e3d3f990
3 changed files with 53 additions and 16 deletions
+32 -12
View File
@@ -4,31 +4,42 @@ export interface OnboardingProjectOption {
readonly folderName?: string | undefined;
}
export interface OnboardingFolderOption {
readonly folderId: string;
readonly name: string;
}
export interface ProjectOnboardingActionValue {
readonly action: "create_project_from_chat" | "bind_project";
readonly organization_id: string;
readonly project_id?: string | undefined;
readonly folder_id?: string | undefined;
}
export function buildUnboundChatOnboardingCard(params: {
readonly organizationId: string;
readonly organizationName: string;
readonly folders: readonly OnboardingFolderOption[];
readonly projects: readonly OnboardingProjectOption[];
readonly canCreateProject: boolean;
}): Record<string, unknown> {
const actions: unknown[] = [];
if (params.canCreateProject) {
actions.push({
tag: "button",
text: { tag: "plain_text", content: "新建并绑定项目" },
type: "primary",
value: {
project_onboarding: {
action: "create_project_from_chat",
organization_id: params.organizationId,
const folders = params.folders.length === 0 ? [{ folderId: undefined, name: "默认位置" }] : params.folders.slice(0, 3);
for (const folder of folders) {
actions.push({
tag: "button",
text: { tag: "plain_text", content: `新建到 ${buttonLabel(folder.name, 14)}` },
type: "primary",
value: {
project_onboarding: {
action: "create_project_from_chat",
organization_id: params.organizationId,
...(folder.folderId !== undefined ? { folder_id: folder.folderId } : {}),
},
},
},
});
});
}
}
for (const project of params.projects.slice(0, 5)) {
@@ -53,7 +64,7 @@ export function buildUnboundChatOnboardingCard(params: {
`这个飞书群还没有绑定项目。`,
``,
`组织: **${escapeMarkdown(params.organizationName)}**`,
`可以新建一个项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`,
`可以选择 folder 新建项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`,
].join("\n"),
},
];
@@ -101,16 +112,21 @@ export function projectOnboardingActionFromValue(value: unknown): ProjectOnboard
const rawAction = (action as { action?: unknown }).action;
const organizationId = (action as { organization_id?: unknown }).organization_id;
const projectId = (action as { project_id?: unknown }).project_id;
const folderId = (action as { folder_id?: unknown }).folder_id;
if ((rawAction !== "create_project_from_chat" && rawAction !== "bind_project") || typeof organizationId !== "string" || organizationId === "") {
return null;
}
if (rawAction === "bind_project" && (typeof projectId !== "string" || projectId === "")) {
return null;
}
if (folderId !== undefined && (typeof folderId !== "string" || folderId === "")) {
return null;
}
return {
action: rawAction,
organization_id: organizationId,
...(typeof projectId === "string" && projectId !== "" ? { project_id: projectId } : {}),
...(typeof folderId === "string" && folderId !== "" ? { folder_id: folderId } : {}),
};
}
@@ -126,7 +142,11 @@ function unwrapValue(value: unknown): unknown {
function buttonProjectLabel(project: OnboardingProjectOption): string {
const folderPrefix = project.folderName === undefined ? "" : `${project.folderName} / `;
const label = `${folderPrefix}${project.name}`;
return label.length <= 24 ? label : `${label.slice(0, 21)}...`;
return buttonLabel(label, 24);
}
function buttonLabel(label: string, maxLength: number): string {
return label.length <= maxLength ? label : `${label.slice(0, maxLength - 3)}...`;
}
function escapeMarkdown(value: string): string {
+14
View File
@@ -53,6 +53,7 @@ import {
buildProjectOnboardingResolvedCard,
buildUnboundChatOnboardingCard,
projectOnboardingActionFromValue,
type OnboardingFolderOption,
type OnboardingProjectOption,
type ProjectOnboardingActionValue,
} from "./projectOnboardingCard.js";
@@ -575,6 +576,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
chatId,
name,
workspaceRoot: deps.projectWorkspaceRoot,
folderId: action.folder_id,
});
await resolveProjectOnboardingCard(rt, messageId, {
title: "已创建并绑定项目",
@@ -860,6 +862,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
actorFeishuOpenId: senderOpenId,
isOrgAdmin: isOrgAdminRole(organization.role),
});
const folders = await listCreatableRootFolders(organization.organizationId);
await sendCard(
rt,
@@ -867,6 +870,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
buildUnboundChatOnboardingCard({
organizationId: organization.organizationId,
organizationName: organization.organizationName,
folders,
projects,
canCreateProject,
}),
@@ -956,6 +960,16 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
return allowed;
}
async function listCreatableRootFolders(organizationId: string): Promise<readonly OnboardingFolderOption[]> {
const folders = await deps.prisma.folder.findMany({
where: { organizationId, parentId: null, archivedAt: null },
select: { id: true, name: true },
orderBy: [{ sortKey: "asc" }, { name: "asc" }],
take: 3,
});
return folders.map((folder) => ({ folderId: folder.id, name: folder.name }));
}
return Object.assign(onMessage, { onCardAction, approvalManager });
}
+7 -4
View File
@@ -168,12 +168,15 @@ describe("trigger full lifecycle (integration)", () => {
expect(rt.sentCards).toHaveLength(1);
expect(rt.sentTexts.at(-1)).toContain("这个飞书群还没有绑定项目");
const values = cardActionValues(rt.sentCards[0]);
expect(values).toContainEqual({
project_onboarding: {
expect(values).toEqual(expect.arrayContaining([
expect.objectContaining({
project_onboarding: expect.objectContaining({
action: "create_project_from_chat",
organization_id: DEFAULT_ORG_ID,
},
});
folder_id: expect.any(String),
}),
}),
]));
expect(runAgentCalls).toHaveLength(0);
});