fix: move folder creation into project move flow

This commit is contained in:
2026-07-13 17:05:45 +08:00
parent 2f0e0f2cd7
commit 2211beb42c
4 changed files with 144 additions and 17 deletions
+59
View File
@@ -52,6 +52,65 @@ export async function browseFolderDestinations(
};
}
export async function createFolderAndMoveProject(
prisma: PrismaClient,
input: {
readonly organizationId: string;
readonly projectId: string;
readonly parentFolderId: string | null;
readonly name: string;
readonly actorUserId?: string | undefined;
},
): Promise<{ readonly folderId: string; readonly folderName: string }> {
const name = input.name.trim();
if (name === "") throw new Error("folder name is required");
if (name.length > 100) throw new Error("folder name must not exceed 100 characters");
return prisma.$transaction(async (tx) => {
await lockActiveProjectOrganization(tx, input.projectId);
const project = await tx.project.findFirst({
where: { id: input.projectId, organizationId: input.organizationId, archivedAt: null },
select: { id: true, folderId: true },
});
if (project === null) throw new Error("active project not found in Organization");
if (input.parentFolderId !== null) {
const parent = await tx.folder.findFirst({
where: {
id: input.parentFolderId,
organizationId: input.organizationId,
archivedAt: null,
kind: { not: "SYSTEM_INBOX" },
},
select: { id: true },
});
if (parent === null) throw new Error("active destination folder not found in Organization");
}
const folder = await tx.folder.create({
data: {
organizationId: input.organizationId,
parentId: input.parentFolderId,
name,
},
select: { id: true, name: true },
});
await tx.project.update({ where: { id: project.id }, data: { folderId: folder.id } });
await tx.auditEntry.create({
data: {
organizationId: input.organizationId,
projectId: project.id,
...(input.actorUserId !== undefined ? { actorUserId: input.actorUserId } : {}),
action: "folder.created_and_project_moved_from_feishu",
metadata: {
folderId: folder.id,
parentFolderId: input.parentFolderId,
previousFolderId: project.folderId,
name: folder.name,
},
},
});
return { folderId: folder.id, folderName: folder.name };
});
}
export async function loadProjectConsole(
prisma: PrismaClient,
input: { readonly projectId: string; readonly chatId: string },
+16 -5
View File
@@ -88,7 +88,6 @@ export function buildProjectManagementCard(params: {
readonly sdkSessionReady: boolean;
} | null | undefined;
readonly canManageProject?: boolean | undefined;
readonly canCreateFolder?: boolean | undefined;
}): Record<string, unknown> {
const elements: unknown[] = [{
tag: "markdown",
@@ -149,7 +148,6 @@ export function buildProjectManagementCard(params: {
renameProjectForm(params),
);
}
if (params.canCreateFolder === true) elements.push(createFolderForm(params));
return {
config: { wide_screen_mode: true },
header: {
@@ -202,6 +200,7 @@ export function buildMoveProjectCard(params: {
readonly parentFolderId: string | null;
readonly breadcrumb: string;
readonly childFolders: readonly { readonly id: string; readonly name: string }[];
readonly canCreateFolder: boolean;
}): Record<string, unknown> {
const navigation: unknown[] = [];
if (params.folderId !== null) {
@@ -232,6 +231,13 @@ export function buildMoveProjectCard(params: {
project_id: params.projectId,
...(params.folderId !== null ? { folder_id: params.folderId } : {}),
}, "primary")] });
if (params.canCreateFolder) {
elements.push(createFolderAndMoveForm({
organizationId: params.organizationId,
projectId: params.projectId,
parentFolderId: params.folderId,
}));
}
return {
config: { wide_screen_mode: true },
header: { title: { tag: "plain_text", content: "移动项目" }, template: "blue" },
@@ -433,7 +439,11 @@ function renameProjectForm(params: {
};
}
function createFolderForm(params: { readonly organizationId: string; readonly projectId: string }): unknown {
function createFolderAndMoveForm(params: {
readonly organizationId: string;
readonly projectId: string;
readonly parentFolderId: string | null;
}): unknown {
return {
tag: "form",
name: "folder_create_form",
@@ -443,12 +453,12 @@ function createFolderForm(params: { readonly organizationId: string; readonly pr
name: "folder_name",
required: true,
max_length: 100,
placeholder: { tag: "plain_text", content: "在当前目录下新建 Folder" },
placeholder: { tag: "plain_text", content: "在当前目标目录下新建 Folder" },
},
{
tag: "button",
name: "folder_create_submit",
text: { tag: "plain_text", content: "新建目录" },
text: { tag: "plain_text", content: "新建并移动" },
type: "default",
complex_interaction: true,
action_type: "form_submit",
@@ -456,6 +466,7 @@ function createFolderForm(params: { readonly organizationId: string; readonly pr
action: "create_folder",
organization_id: params.organizationId,
project_id: params.projectId,
...(params.parentFolderId !== null ? { folder_id: params.parentFolderId } : {}),
} },
},
],
+21 -10
View File
@@ -56,7 +56,6 @@ import { createSlashCommandRegistry, parseSlashInvocation } from "./slashCommand
import { cphHubMcpToolsForRole, roleToolsAllow } from "../agent/roleTools.js";
import {
bindFeishuChatToProject,
createFolder,
createProjectFromFeishuChat,
ensureOrganizationProjectSettings,
moveProjectToFolder,
@@ -75,6 +74,7 @@ import {
import {
archiveCurrentRoleSession,
browseFolderDestinations,
createFolderAndMoveProject,
listRoleSessionHistory,
loadProjectConsole,
resumeRoleSession,
@@ -221,7 +221,6 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
roles: visibleRoles,
currentSession: state.currentSession,
canManageProject: manageDecision.allowed || isOrgAdmin,
canCreateFolder: isOrgAdmin,
...(title !== undefined ? { title } : {}),
});
}
@@ -949,6 +948,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
organizationId: state.organizationId,
projectId,
projectName: state.projectName,
canCreateFolder: isOrgAdmin,
...page,
}));
return;
@@ -968,18 +968,20 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
if (!isOrgAdmin) throw new Error("creating an Organization folder requires OWNER or ADMIN");
const folderName = event.action.form_value?.["folder_name"];
if (typeof folderName !== "string") throw new Error("create folder form is missing folder_name");
const folder = await createFolder(deps.prisma, {
const folder = await createFolderAndMoveProject(deps.prisma, {
organizationId: state.organizationId,
name: folderName.slice(0, 100),
...(state.folderId !== null ? { parentId: state.folderId } : {}),
});
await writeAudit(deps.prisma, {
projectId,
action: "folder.created_from_feishu",
metadata: { folderId: folder.id, parentId: state.folderId, name: folder.name },
parentFolderId: action.folder_id ?? null,
name: folderName.slice(0, 100),
...(manageDecision.actorUserId !== undefined ? { actorUserId: manageDecision.actorUserId } : {}),
});
if (messageId === undefined) throw new Error("folder creation requires message id");
await patchCard(rt, messageId, await projectConsoleCard(projectId, chatId, operatorOpenId, "目录已创建"));
await patchCard(rt, messageId, await projectConsoleCard(
projectId,
chatId,
operatorOpenId,
`已新建目录“${folder.folderName}”并移动项目`,
));
return;
}
if (action.action === "rename_project") {
@@ -1359,6 +1361,15 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
return;
}
const key = messageBatchKey(chatId, senderOpenId, runContext.roleId);
for (const [pendingKey, pendingContext] of batchContexts) {
if (
pendingKey !== key &&
pendingContext.chatId === chatId &&
pendingContext.senderOpenId === senderOpenId
) {
await messageBatcher.flushNow(pendingKey);
}
}
if (!batchContexts.has(key)) {
batchContexts.set(key, runContext);
}