diff --git a/apps/api/src/app/container.ts b/apps/api/src/app/container.ts index f9df2bc..f7c415d 100644 --- a/apps/api/src/app/container.ts +++ b/apps/api/src/app/container.ts @@ -271,8 +271,21 @@ export async function createContainer(config: AppConfig, overrides: ContainerOve const { staff } = await org.listStaff(); return staff.filter((s) => s.positionId === positionId).map((s) => ({ id: s.id })); }, - // 治理台收件人 = 能进运营中心的活跃员工(任务在那儿管;ADR-012 权限只到端) - governanceStaff: async () => (await org.staffWithEnd('ops')).map((s) => ({ id: s.id })), + /* + * 治理台收件人 = 能进运营中心的活跃员工(任务在那儿管;ADR-012 权限只到端)。 + * **空集要出声**:全新安装、还没人以 admin 登录过时这里就是空的,而调用方是死信通知与 + * 无主入站兜底 —— 收件人为空就等于把「出事了」这件事无声丢掉(铁规矩 8:界面不撒谎)。 + */ + governanceStaff: async () => { + const recipients = await org.staffWithEnd('ops'); + if (recipients.length === 0) { + process.stderr.write( + '[org] 没有能进运营中心的活跃员工 —— 死信与无主入站的通知这一次没有收件人。' + + '去运营中心的「角色 → 端」把某个 realm 角色配上 ops,或让持该角色的人先登录一次。\n', + ); + } + return recipients.map((s) => ({ id: s.id })); + }, }, clock: systemClock, ids, diff --git a/db/migrations/0032_keycloak_identity.sql b/db/migrations/0032_keycloak_identity.sql index f0e8ed9..0425682 100644 --- a/db/migrations/0032_keycloak_identity.sql +++ b/db/migrations/0032_keycloak_identity.sql @@ -25,3 +25,11 @@ create table if not exists sys.role_access ( -- 他们名下的运行数据由 `reset-runs` 清(迁移不碰运行表)。 delete from sys.work_group; delete from sys.staff; + +-- 身份没了,指着身份的**配置**也不能留: +-- `sys.node_owner.staff_id` 是运营配的节点负责人,它排在轮值**之前** —— 留着就是把单派给一个不存在的人; +-- `sys.trigger.confirm_staff_id` 是起单确认人,留下 stale id + confirm_required=true, +-- 正是「命中的单没人点头、全进死信」那个状态(那也是 setTriggerConfirm 不允许人手工造出来的状态)。 +-- 这两张是**配置**表,`reset-runs` 按设计不碰(见 reset-runs.ts 的注释),所以只能在这里收拾。 +delete from sys.node_owner; +update sys.trigger set confirm_required = false, confirm_staff_id = null where confirm_staff_id is not null;