forked from bai/curriculum-project-hub
feat: let Feishu onboarding create projects in folders
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user