feat: add Feishu project onboarding card

This commit is contained in:
2026-07-10 00:32:09 +08:00
parent 34e07e229f
commit 519ea8144f
5 changed files with 558 additions and 6 deletions
+134
View File
@@ -0,0 +1,134 @@
export interface OnboardingProjectOption {
readonly projectId: string;
readonly name: string;
readonly folderName?: string | undefined;
}
export interface ProjectOnboardingActionValue {
readonly action: "create_project_from_chat" | "bind_project";
readonly organization_id: string;
readonly project_id?: string | undefined;
}
export function buildUnboundChatOnboardingCard(params: {
readonly organizationId: string;
readonly organizationName: string;
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,
},
},
});
}
for (const project of params.projects.slice(0, 5)) {
actions.push({
tag: "button",
text: { tag: "plain_text", content: buttonProjectLabel(project) },
type: "default",
value: {
project_onboarding: {
action: "bind_project",
organization_id: params.organizationId,
project_id: project.projectId,
},
},
});
}
const elements: unknown[] = [
{
tag: "markdown",
content: [
`这个飞书群还没有绑定项目。`,
``,
`组织: **${escapeMarkdown(params.organizationName)}**`,
`可以新建一个项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`,
].join("\n"),
},
];
if (actions.length > 0) {
elements.push({ tag: "action", actions });
} else {
elements.push({
tag: "markdown",
content: "你当前没有可绑定项目,也没有新建项目权限。请联系组织管理员。",
});
}
return {
config: { wide_screen_mode: true },
header: {
title: { tag: "plain_text", content: "绑定项目" },
template: "blue",
},
elements,
};
}
export function buildProjectOnboardingResolvedCard(params: {
readonly title: string;
readonly body: string;
readonly template: "green" | "red";
}): Record<string, unknown> {
return {
config: { wide_screen_mode: true },
header: {
title: { tag: "plain_text", content: params.title },
template: params.template,
},
elements: [{ tag: "markdown", content: params.body }],
};
}
export function projectOnboardingActionFromValue(value: unknown): ProjectOnboardingActionValue | null {
const raw = unwrapValue(value);
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
if (!("project_onboarding" in raw)) return null;
const action = raw.project_onboarding;
if (typeof action !== "object" || action === null || Array.isArray(action)) return null;
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;
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;
}
return {
action: rawAction,
organization_id: organizationId,
...(typeof projectId === "string" && projectId !== "" ? { project_id: projectId } : {}),
};
}
function unwrapValue(value: unknown): unknown {
if (typeof value !== "string") return value;
try {
return JSON.parse(value);
} catch {
return value;
}
}
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)}...`;
}
function escapeMarkdown(value: string): string {
return value.replace(/([`*_{}[\]<>])/g, "\\$1");
}