forked from EduCraft/curriculum-project-hub
feat: interrupt running agent via card button with confirm dialog
Add a ⛔ 中断 button to live streaming cards; Feishu's native confirm
dialog is the confirmation step, so no extra card round-trip is needed.
- builder.ts: render interrupt action (danger button + confirm) while the
run is live; new `interrupted` flag renders a 已中断 footer instead of
完成/失败 on the complete card.
- streaming-card.ts: finish(text, { interrupted }) threads the flag into
the final patch; overflow cards suppress the button.
- runner.ts: RunRequest gains abortController, passed to the SDK query;
abort surfaces as RunStatus "interrupted" (no error) in the catch.
- trigger.ts: activeRuns registry maps runId → AbortController; onCardAction
resolves the interrupt button, authorizes agent.cancel, aborts the run.
Interrupted runs persist as CANCELED (spec RunState.canceled, terminal →
lock released per ADR-0002).
- authorizer.ts: agent.cancel now consults PermissionSettings.agentCancel
(MANAGE_ONLY/DISABLED) — previously only agentTrigger was honored, but
spec/PermissionGrant deliberately splits these knobs ("谁能触发" ≠
"谁能取消"). Default role remains MANAGE (Capability.normalCancel).
Tests: runner abort unit test, trigger interrupt + denied integration
tests, three agent.cancel authorization tests (manage allowed, edit denied,
DISABLED policy denies even manage). 26 files / 175 tests pass.
This commit is contained in:
@@ -121,7 +121,11 @@ export class PrismaPermissionAuthorizer implements PermissionAuthorizer {
|
||||
|
||||
private async requiredRole(req: AuthorizationRequest): Promise<PermissionRole | "DISABLED"> {
|
||||
let required = defaultRequiredRole(req.action);
|
||||
if ((req.action === "agent.trigger" || req.action === "role.trigger") && req.resource.type === "PROJECT") {
|
||||
// ADR-0019 + PermissionGrant spec: agentTrigger and agentCancel are
|
||||
// distinct policy knobs ("谁能触发" ≠ "谁能取消"). Each can tighten the
|
||||
// default role to MANAGE_ONLY or DISABLE the action entirely. The default
|
||||
// role (trigger=EDIT, cancel=MANAGE) is the floor when no policy is set.
|
||||
if (req.resource.type === "PROJECT" && (req.action === "agent.trigger" || req.action === "role.trigger")) {
|
||||
const settings = await this.prisma.permissionSettings.findFirst({
|
||||
where: { resourceType: req.resource.type, resourceId: req.resource.id },
|
||||
select: { agentTrigger: true },
|
||||
@@ -129,6 +133,14 @@ export class PrismaPermissionAuthorizer implements PermissionAuthorizer {
|
||||
const policy = normalizePolicy(settings?.agentTrigger);
|
||||
if (policy === "DISABLED") return "DISABLED";
|
||||
if (policy === "MANAGE_ONLY") required = "MANAGE";
|
||||
} else if (req.resource.type === "PROJECT" && req.action === "agent.cancel") {
|
||||
const settings = await this.prisma.permissionSettings.findFirst({
|
||||
where: { resourceType: req.resource.type, resourceId: req.resource.id },
|
||||
select: { agentCancel: true },
|
||||
});
|
||||
const policy = normalizePolicy(settings?.agentCancel);
|
||||
if (policy === "DISABLED") return "DISABLED";
|
||||
if (policy === "MANAGE_ONLY") required = "MANAGE";
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user