forked from bai/curriculum-project-hub
fix: guide Feishu users through onboarding
This commit is contained in:
@@ -85,6 +85,10 @@ interface TriggerDeps {
|
||||
readonly messageBatcherOptions?: MessageBatcherOptions | undefined;
|
||||
readonly triggerQueue?: TriggerQueue | undefined;
|
||||
readonly projectWorkspaceRoot: string;
|
||||
/** Public origin used to build the Silo Organization's Feishu OAuth entrypoint. */
|
||||
readonly publicBaseUrl: string;
|
||||
/** The single Organization this Alpha Silo is fail-closed to serve. */
|
||||
readonly siloOrganizationId: string;
|
||||
/** Alpha Silo policy: never acknowledge work into the process-local queue. */
|
||||
readonly rejectWhenBusy?: boolean | undefined;
|
||||
readonly resourceLimits?: {
|
||||
@@ -133,6 +137,9 @@ export interface TriggerHandler {
|
||||
export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
const projectWorkspaceRoot = deps.projectWorkspaceRoot.trim();
|
||||
if (projectWorkspaceRoot === "") throw new Error("projectWorkspaceRoot is required");
|
||||
const publicBaseUrl = parsePublicBaseUrl(deps.publicBaseUrl);
|
||||
const siloOrganizationId = deps.siloOrganizationId.trim();
|
||||
if (siloOrganizationId === "") throw new Error("siloOrganizationId is required");
|
||||
const authorizer = deps.authorizer ?? createPermissionAuthorizer(deps.prisma);
|
||||
const runAgent = deps.runAgent ?? defaultRunAgent;
|
||||
const approvalManager = new ApprovalManager();
|
||||
@@ -936,6 +943,15 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
await sendText(rt, chatId, "无法识别发送者,拒绝触发。", sendOptionsForTriggerMessage(msg));
|
||||
return;
|
||||
}
|
||||
const organizationResolution = await resolveSingleActiveOrganizationForFeishuUser(senderOpenId);
|
||||
if (organizationResolution.status === "error" && organizationResolution.reason !== "organization_unavailable") {
|
||||
deps.logger.info(
|
||||
{ projectId, senderOpenId, reason: organizationResolution.reason },
|
||||
"feishu trigger: actor onboarding required before project authorization",
|
||||
);
|
||||
await sendText(rt, chatId, organizationResolution.message, sendOptionsForTriggerMessage(msg));
|
||||
return;
|
||||
}
|
||||
const actor = { feishuOpenId: senderOpenId, chatId };
|
||||
const triggerDecision = await authorizer.can({
|
||||
actor,
|
||||
@@ -1083,17 +1099,33 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
readonly organizationName: string;
|
||||
readonly role: "OWNER" | "ADMIN" | "MEMBER";
|
||||
}
|
||||
| { readonly status: "error"; readonly message: string }
|
||||
| {
|
||||
readonly status: "error";
|
||||
readonly reason: "identity_missing" | "membership_missing" | "organization_unavailable";
|
||||
readonly message: string;
|
||||
}
|
||||
> {
|
||||
const siloOrganization = await deps.prisma.organization.findFirst({
|
||||
where: { id: siloOrganizationId, status: "ACTIVE" },
|
||||
select: { id: true, slug: true },
|
||||
});
|
||||
if (siloOrganization === null) {
|
||||
return {
|
||||
status: "error",
|
||||
reason: "organization_unavailable",
|
||||
message: "组织当前不可用,请联系 Educraft 运维人员。",
|
||||
};
|
||||
}
|
||||
const identity = await deps.prisma.feishuUserIdentity.findFirst({
|
||||
where: {
|
||||
openId: feishuOpenId,
|
||||
connection: { status: "ACTIVE", organization: { status: "ACTIVE" } },
|
||||
connection: { status: "ACTIVE", organizationId: siloOrganization.id },
|
||||
},
|
||||
select: {
|
||||
user: { select: { organizationMemberships: {
|
||||
where: {
|
||||
revokedAt: null,
|
||||
organizationId: siloOrganization.id,
|
||||
organization: { status: "ACTIVE" },
|
||||
},
|
||||
select: {
|
||||
@@ -1109,19 +1141,34 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
where: { feishuOpenId },
|
||||
select: {
|
||||
organizationMemberships: {
|
||||
where: { revokedAt: null, organization: { status: "ACTIVE" } },
|
||||
where: {
|
||||
revokedAt: null,
|
||||
organizationId: siloOrganization.id,
|
||||
organization: { status: "ACTIVE" },
|
||||
},
|
||||
select: { role: true, organization: { select: { id: true, name: true } } },
|
||||
orderBy: { createdAt: "asc" },
|
||||
},
|
||||
},
|
||||
})
|
||||
: null);
|
||||
if (user === null) return { status: "error", message: "请先登录并加入组织后,再绑定项目。" };
|
||||
if (user.organizationMemberships.length === 0) {
|
||||
return { status: "error", message: "你还不属于任何可用组织,请联系组织管理员。" };
|
||||
if (user === null) {
|
||||
const loginUrl = new URL(
|
||||
`/auth/feishu/${encodeURIComponent(siloOrganization.slug)}`,
|
||||
publicBaseUrl,
|
||||
).toString();
|
||||
return {
|
||||
status: "error",
|
||||
reason: "identity_missing",
|
||||
message: `请先通过飞书登录建立身份:${loginUrl}\n登录后仍需由组织管理员将你加入组织。`,
|
||||
};
|
||||
}
|
||||
if (user.organizationMemberships.length > 1) {
|
||||
return { status: "error", message: "你属于多个组织。请先从组织后台选择项目绑定,或等待多组织选择入口。" };
|
||||
if (user.organizationMemberships.length === 0) {
|
||||
return {
|
||||
status: "error",
|
||||
reason: "membership_missing",
|
||||
message: "你已完成飞书登录,但尚未加入该组织。请联系组织管理员为你开通成员权限。",
|
||||
};
|
||||
}
|
||||
const membership = user.organizationMemberships[0]!;
|
||||
return {
|
||||
@@ -1184,6 +1231,19 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
return Object.assign(onMessage, { onCardAction, approvalManager });
|
||||
}
|
||||
|
||||
function parsePublicBaseUrl(raw: string): URL {
|
||||
const value = raw.trim();
|
||||
if (value === "") throw new Error("publicBaseUrl is required");
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
throw new Error("publicBaseUrl must use http or https");
|
||||
}
|
||||
if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.pathname !== "/") {
|
||||
throw new Error("publicBaseUrl must be an origin without credentials, path, query, or fragment");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
interface SessionMetadata {
|
||||
readonly claudeSessionId?: string | undefined;
|
||||
}
|
||||
|
||||
@@ -160,6 +160,8 @@ export async function startHub(): Promise<void> {
|
||||
settings: runtimeSettings,
|
||||
logger: app.log,
|
||||
projectWorkspaceRoot,
|
||||
publicBaseUrl,
|
||||
siloOrganizationId: siloOrganization.id,
|
||||
rejectWhenBusy: true,
|
||||
resourceLimits: { maxFilesPerMessage, maxBytesPerFile },
|
||||
allowLegacyFeishuIdentity: false,
|
||||
|
||||
Reference in New Issue
Block a user