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
+23 -3
View File
@@ -4,32 +4,43 @@ export interface OnboardingProjectOption {
readonly folderName?: string | undefined; readonly folderName?: string | undefined;
} }
export interface OnboardingFolderOption {
readonly folderId: string;
readonly name: string;
}
export interface ProjectOnboardingActionValue { export interface ProjectOnboardingActionValue {
readonly action: "create_project_from_chat" | "bind_project"; readonly action: "create_project_from_chat" | "bind_project";
readonly organization_id: string; readonly organization_id: string;
readonly project_id?: string | undefined; readonly project_id?: string | undefined;
readonly folder_id?: string | undefined;
} }
export function buildUnboundChatOnboardingCard(params: { export function buildUnboundChatOnboardingCard(params: {
readonly organizationId: string; readonly organizationId: string;
readonly organizationName: string; readonly organizationName: string;
readonly folders: readonly OnboardingFolderOption[];
readonly projects: readonly OnboardingProjectOption[]; readonly projects: readonly OnboardingProjectOption[];
readonly canCreateProject: boolean; readonly canCreateProject: boolean;
}): Record<string, unknown> { }): Record<string, unknown> {
const actions: unknown[] = []; const actions: unknown[] = [];
if (params.canCreateProject) { if (params.canCreateProject) {
const folders = params.folders.length === 0 ? [{ folderId: undefined, name: "默认位置" }] : params.folders.slice(0, 3);
for (const folder of folders) {
actions.push({ actions.push({
tag: "button", tag: "button",
text: { tag: "plain_text", content: "新建并绑定项目" }, text: { tag: "plain_text", content: `新建到 ${buttonLabel(folder.name, 14)}` },
type: "primary", type: "primary",
value: { value: {
project_onboarding: { project_onboarding: {
action: "create_project_from_chat", action: "create_project_from_chat",
organization_id: params.organizationId, organization_id: params.organizationId,
...(folder.folderId !== undefined ? { folder_id: folder.folderId } : {}),
}, },
}, },
}); });
} }
}
for (const project of params.projects.slice(0, 5)) { for (const project of params.projects.slice(0, 5)) {
actions.push({ actions.push({
@@ -53,7 +64,7 @@ export function buildUnboundChatOnboardingCard(params: {
`这个飞书群还没有绑定项目。`, `这个飞书群还没有绑定项目。`,
``, ``,
`组织: **${escapeMarkdown(params.organizationName)}**`, `组织: **${escapeMarkdown(params.organizationName)}**`,
`可以新建一个项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`, `可以选择 folder 新建项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`,
].join("\n"), ].join("\n"),
}, },
]; ];
@@ -101,16 +112,21 @@ export function projectOnboardingActionFromValue(value: unknown): ProjectOnboard
const rawAction = (action as { action?: unknown }).action; const rawAction = (action as { action?: unknown }).action;
const organizationId = (action as { organization_id?: unknown }).organization_id; const organizationId = (action as { organization_id?: unknown }).organization_id;
const projectId = (action as { project_id?: unknown }).project_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 === "") { if ((rawAction !== "create_project_from_chat" && rawAction !== "bind_project") || typeof organizationId !== "string" || organizationId === "") {
return null; return null;
} }
if (rawAction === "bind_project" && (typeof projectId !== "string" || projectId === "")) { if (rawAction === "bind_project" && (typeof projectId !== "string" || projectId === "")) {
return null; return null;
} }
if (folderId !== undefined && (typeof folderId !== "string" || folderId === "")) {
return null;
}
return { return {
action: rawAction, action: rawAction,
organization_id: organizationId, organization_id: organizationId,
...(typeof projectId === "string" && projectId !== "" ? { project_id: projectId } : {}), ...(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 { function buttonProjectLabel(project: OnboardingProjectOption): string {
const folderPrefix = project.folderName === undefined ? "" : `${project.folderName} / `; const folderPrefix = project.folderName === undefined ? "" : `${project.folderName} / `;
const label = `${folderPrefix}${project.name}`; 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 { function escapeMarkdown(value: string): string {
+14
View File
@@ -53,6 +53,7 @@ import {
buildProjectOnboardingResolvedCard, buildProjectOnboardingResolvedCard,
buildUnboundChatOnboardingCard, buildUnboundChatOnboardingCard,
projectOnboardingActionFromValue, projectOnboardingActionFromValue,
type OnboardingFolderOption,
type OnboardingProjectOption, type OnboardingProjectOption,
type ProjectOnboardingActionValue, type ProjectOnboardingActionValue,
} from "./projectOnboardingCard.js"; } from "./projectOnboardingCard.js";
@@ -575,6 +576,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
chatId, chatId,
name, name,
workspaceRoot: deps.projectWorkspaceRoot, workspaceRoot: deps.projectWorkspaceRoot,
folderId: action.folder_id,
}); });
await resolveProjectOnboardingCard(rt, messageId, { await resolveProjectOnboardingCard(rt, messageId, {
title: "已创建并绑定项目", title: "已创建并绑定项目",
@@ -860,6 +862,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
actorFeishuOpenId: senderOpenId, actorFeishuOpenId: senderOpenId,
isOrgAdmin: isOrgAdminRole(organization.role), isOrgAdmin: isOrgAdminRole(organization.role),
}); });
const folders = await listCreatableRootFolders(organization.organizationId);
await sendCard( await sendCard(
rt, rt,
@@ -867,6 +870,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
buildUnboundChatOnboardingCard({ buildUnboundChatOnboardingCard({
organizationId: organization.organizationId, organizationId: organization.organizationId,
organizationName: organization.organizationName, organizationName: organization.organizationName,
folders,
projects, projects,
canCreateProject, canCreateProject,
}), }),
@@ -956,6 +960,16 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
return allowed; 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 }); 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.sentCards).toHaveLength(1);
expect(rt.sentTexts.at(-1)).toContain("这个飞书群还没有绑定项目"); expect(rt.sentTexts.at(-1)).toContain("这个飞书群还没有绑定项目");
const values = cardActionValues(rt.sentCards[0]); const values = cardActionValues(rt.sentCards[0]);
expect(values).toContainEqual({ expect(values).toEqual(expect.arrayContaining([
project_onboarding: { expect.objectContaining({
project_onboarding: expect.objectContaining({
action: "create_project_from_chat", action: "create_project_from_chat",
organization_id: DEFAULT_ORG_ID, organization_id: DEFAULT_ORG_ID,
}, folder_id: expect.any(String),
}); }),
}),
]));
expect(runAgentCalls).toHaveLength(0); expect(runAgentCalls).toHaveLength(0);
}); });