From 8dc71db9d873dfbbc89a100bd4bb875db4bb1e08 Mon Sep 17 00:00:00 2001 From: xcs Date: Tue, 25 Aug 2026 17:57:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=84=E5=AE=A1=E4=B8=A4=E6=9D=A1=20Importan?= =?UTF-8?q?t:=E8=BF=81=E7=A7=BB=E8=BF=9E=E5=B8=A6=E6=B8=85=E6=8E=89?= =?UTF-8?q?=E6=8C=87=E5=90=91=E8=BA=AB=E4=BB=BD=E7=9A=84=E4=B8=A4=E5=BC=A0?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=A1=A8;=E6=B2=BB=E7=90=86=E5=8F=B0?= =?UTF-8?q?=E6=94=B6=E4=BB=B6=E4=BA=BA=E4=B8=BA=E7=A9=BA=E6=97=B6=E5=87=BA?= =?UTF-8?q?=E5=A3=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 节点负责人(排在轮值之前)与触发器确认人都是无 FK 的裸 staff id,而 reset-runs 按设计 不碰配置表 —— 切换后留着就是「把单派给不存在的人」与「命中的单没人点头全进死信」。 governanceStaff 在全新安装(还没人以 admin 登录过)时是空集,而调用方是死信通知: 收件人为空原本会静默丢掉「出事了」这件事,现在留一条说清怎么办的话。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw --- apps/api/src/app/container.ts | 17 +++++++++++++++-- db/migrations/0032_keycloak_identity.sql | 8 ++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) 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;