LEADER SUMMARY COPY
组长任务摘要抄送
-把同组织内其他非管理员员工的稳定任务结果,经组长自己的 AgentBus 渠道主动投递到指定微信会话。人工任务与 AgentBus 任务均可纳入。
+身份为组长且已有可用 AgentBus 渠道时,系统自动把同组织内其他非管理员员工的稳定任务结果抄送到组长微信。人工任务与 AgentBus 任务都会纳入。
diff --git a/.project-docs/30-worklog/tasks/20260907-auto-leader-summary-routing-5e8c1a73.md b/.project-docs/30-worklog/tasks/20260907-auto-leader-summary-routing-5e8c1a73.md new file mode 100644 index 0000000..bc5792b --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260907-auto-leader-summary-routing-5e8c1a73.md @@ -0,0 +1,56 @@ +# Task: Auto-enable leader summaries from AgentBus routing + +## Identity + +- Task ID: 20260907-auto-leader-summary-routing-5e8c1a73 +- Mode: Feature +- Branch: codex/20260907-auto-leader-summary-routing-5e8c1a73-auto-leader-summary-routing +- Worktree: /Users/inmanx/Documents/lwltAPI-auto-leader-summary-routing-5e8c1a73 +- Base commit: 8fb066f3453dde079b3b58f67c95cd57de60923d +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Replace administrator-managed leader summary subscriptions with role-driven automatic activation for active `team_lead` accounts. +- Resolve the proactive AgentBus recipient and conversation from the leader-owned channel, preferring the latest valid inbound route and falling back to the channel external user reference. +- Keep encrypted route snapshots, durable low-priority delivery, future-only projection, source coverage, privacy filtering, and role/channel fail-closed behavior. +- Reduce the channels page to read-only automatic-delivery status and remove the manual mutation API and form controls. +- Update active component documentation and contract tests. No database migration or production send is in scope. + +## Intent And Constraints + +- User explicitly superseded the accepted default-off/manual-target product behavior: an active team lead with an enabled owned AgentBus channel should receive summaries automatically. +- Include both manual and AgentBus tasks assigned to other non-administrator employees in the same organization; never backfill history. +- A channel disable, ownership change, inactive account, or role change must stop delivery and cancel unsent rows rather than reroute them. +- Never expose or log plaintext routing targets, task instructions, traveler/customer data, attachments, or technical errors in leader summaries. +- Preserve the existing schema version 020 tables so the fix can deploy without another database change. + +## Outcome + +- Active team-lead accounts now receive an automatic organization-wide summary subscription whenever their owned AgentBus channel is enabled and has a safe route. +- Route resolution prefers the latest accepted inbound route whose task is assigned to the current channel owner, falls back to the channel external user reference, and learns exact future routes after successful AgentBus ingestion. +- Route/policy changes reset the future-only boundary and revision; invalid roles, accounts, ownership, or disabled channels cancel unsent rows and fail closed. +- Channel owner changes clear the previous external user reference, preventing a new owner from inheriting the former owner's proactive route. +- The manual subscription mutation API and all recipient/conversation/source/verification/enable controls were removed. `/channels` now displays automatic status only. +- Existing migration 020 remains sufficient; no database migration or production data change was introduced. + +## Verification + +- `node --run check:repo` — passed (10 tests). +- `node --run check` — passed. +- `node --run test:control-plane` — passed (176 tests). +- `node --run test:legacy` — passed (270 tests). +- `node --run build` — passed. +- `node --check LianSyn-platform/app.js` — passed. +- Targeted AgentBus and leader-summary contract suite — passed (28 tests). +- Disposable PostgreSQL 16.14 validation applied migrations 001–020, confirmed first reconciliation creates one enabled encrypted automatic row, the second reconciliation is idempotent, an employee terminal result projects and claims on the leader's exact inbound route, and a leader-to-user role change disables the row and cancels its pending delivery. The temporary database was stopped and moved to Trash. +- No real AgentBus/WeChat message, production database mutation, deployment, or restart was performed. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +- Revise accepted decision `AUTH-003-leader-task-summary-notifications.md` during Integration: activation and routing are automatic consequences of an active `team_lead` role plus its owned AgentBus channel, rather than an administrator-managed default-off subscription. diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index f145664..9046554 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -45,8 +45,6 @@ let automationSettingsSyncInFlight = null; let channelList = []; let channelSettingsBusy = false; let leaderSummarySubscriptions = []; -let leaderSummarySettingsBusy = false; -let leaderSummaryEditingChannelId = ''; let parserRoutingRows = []; let parserRoutingBusy = false; let accountList = []; @@ -805,7 +803,7 @@ async function syncChannels() { channelList = Array.isArray(result.channels) ? result.channels : []; renderChannelList(); renderChannelOwnerOptions(); - renderLeaderSummaryChannelOptions(); + renderLeaderSummarySubscriptions(); return channelList; } @@ -847,6 +845,7 @@ async function createChannelFromForm() { $('#channelBotAddress').value = ''; renderChannelList(); renderChannelOwnerOptions(); + renderLeaderSummarySubscriptions(); if (message) message.textContent = '渠道已保存,连接状态会在服务端异步更新。'; } finally { channelSettingsBusy = false; @@ -929,7 +928,7 @@ async function deleteChannel(channelId) { if (!channel || channel.deletable === false) return; const confirmed = window.confirm( `确认删除 AgentBus 渠道“${channel.display_name || '未命名渠道'}”?\n\n` - + '删除后连接会立即停止,服务端保存的 key、持久化 AgentBus 回执记录(包括未发送回执)会被移除;组长摘要设置及其待发送记录也会被移除;历史任务不会被删除,已经到达微信的摘要无法撤回。此操作不可撤销。' + + '删除后连接会立即停止,服务端保存的 key、持久化 AgentBus 回执记录(包括未发送回执)会被移除;组长自动摘要及其待发送记录也会被移除;历史任务不会被删除,已经到达微信的摘要无法撤回。此操作不可撤销。' ); if (!confirmed) return; const message = $('#channelMessage'); @@ -952,115 +951,58 @@ async function deleteChannel(channelId) { function leaderSummarySubscriptionForChannel(channelId) { const channel = channelList.find((item) => item.id === channelId); return leaderSummarySubscriptions.find((item) => ( - item.channel_id === channelId - || (channel?.owner_user_id && item.leader_user_id === channel.owner_user_id) - )); -} - -function populateLeaderSummaryForm(channelId) { - leaderSummaryEditingChannelId = channelId || ''; - const subscription = leaderSummarySubscriptionForChannel(channelId); - const recipient = $('#leaderSummaryRecipientAddress'); - const conversation = $('#leaderSummaryConversationId'); - if (recipient) recipient.value = ''; - if (conversation) conversation.value = ''; - if ($('#leaderSummaryIncludeManual')) { - $('#leaderSummaryIncludeManual').checked = subscription?.include_manual !== false; - } - if ($('#leaderSummaryIncludeAgentbus')) { - $('#leaderSummaryIncludeAgentbus').checked = subscription?.include_agentbus !== false; - } - if ($('#leaderSummaryTargetVerified')) { - $('#leaderSummaryTargetVerified').checked = subscription?.target_verified === true; - } - if ($('#leaderSummaryEnabled')) { - $('#leaderSummaryEnabled').checked = subscription?.enabled === true; - } - const hint = $('#leaderSummaryMessage'); - if (hint && subscription) { - hint.textContent = '已有目标不会回显;地址和会话 ID 留空保存会继续使用原目标。输入任一新值会要求重新核对。'; - } else if (hint && channelId) { - hint.textContent = '首次配置必须同时填写 AgentBus 收件地址和微信会话 ID,保存后不会回显。'; - } -} - -function renderLeaderSummaryChannelOptions() { - const select = $('#leaderSummaryChannelId'); - if (!select) return; - const subscribedChannelIds = new Set(leaderSummarySubscriptions.map((item) => item.channel_id)); - const channels = channelList.filter((channel) => ( - channel.owner_user_id - && (channel.owner_role === 'team_lead' || subscribedChannelIds.has(channel.id)) - )); - const preferred = String(select.value || leaderSummaryEditingChannelId || ''); - select.replaceChildren(new Option('请选择组长渠道', '')); - for (const channel of channels) { - const state = channel.enabled ? channelStatusLabel(channel.status) : '已停用'; - select.append(new Option( - `${channel.owner_username || '身份已变化'} · ${channel.display_name || '未命名渠道'} · ${state}`, - channel.id - )); - } - const next = channels.some((channel) => channel.id === preferred) - ? preferred - : leaderSummarySubscriptions.find((item) => channels.some((channel) => channel.id === item.channel_id))?.channel_id - || channels[0]?.id - || ''; - select.value = next; - select.disabled = leaderSummarySettingsBusy || !channels.length; - $('#leaderSummarySave').disabled = leaderSummarySettingsBusy || !next; - if (leaderSummaryEditingChannelId !== next) populateLeaderSummaryForm(next); + channel?.owner_user_id && item.leader_user_id === channel.owner_user_id + )) || leaderSummarySubscriptions.find((item) => item.channel_id === channelId); } function renderLeaderSummarySubscriptions() { const container = $('#leaderSummaryList'); if (!container) return; container.replaceChildren(); - if (!leaderSummarySubscriptions.length) { - container.append(el('p', 'muted channel-empty', '尚未配置组长摘要抄送。设置保存后仍默认关闭,启用前必须核对主动投递目标。')); + const leaderChannels = channelList.filter((channel) => ( + channel.owner_user_id && channel.owner_role === 'team_lead' + )); + if (!leaderChannels.length) { + container.append(el('p', 'muted channel-empty', '当前没有绑定 AgentBus 渠道的有效组长账号。账号设为组长并绑定渠道后,摘要抄送会自动生效。')); return; } - for (const subscription of leaderSummarySubscriptions) { + for (const channel of leaderChannels) { + const subscription = leaderSummarySubscriptionForChannel(channel.id); const row = el('article', 'channel-row'); const main = el('div', 'channel-row-main'); const heading = el('div', 'channel-row-heading'); - const stateClass = !subscription.enabled - ? 'state-warn' - : subscription.eligible ? 'state-ok' : 'state-bad'; - const stateLabel = !subscription.enabled - ? '已关闭' - : subscription.eligible ? '已启用' : '已暂停'; - heading.append(el('strong', '', subscription.leader_username || '未知组长')); + const routeReady = subscription?.route_ready === true || subscription?.target_verified === true; + const active = subscription?.enabled === true && subscription?.eligible === true; + let stateClass = active ? 'state-ok' : 'state-warn'; + let stateLabel = active ? '自动推送' : '自动同步中'; + if (!channel.enabled) stateLabel = '渠道已停用'; + else if (!channel.routing_ready) { + stateClass = 'state-bad'; + stateLabel = '渠道未就绪'; + } else if (!routeReady) stateLabel = '等待路由'; + heading.append(el('strong', '', channel.owner_username || subscription?.leader_username || '未知组长')); heading.append(el('span', `state ${stateClass}`, stateLabel)); main.append(heading); - const sources = [ - subscription.include_manual ? '人工任务' : '', - subscription.include_agentbus ? 'AgentBus 任务' : '' - ].filter(Boolean).join('、'); - main.append(el('p', 'muted', `渠道:${subscription.channel_name || '未命名渠道'} · 范围:同组织其他非管理员员工 · 来源:${sources || '未选择'}`)); + main.append(el('p', 'muted', `渠道:${channel.display_name || subscription?.channel_name || '未命名渠道'} · 范围:同组织其他非管理员员工 · 来源:人工任务、AgentBus 任务`)); main.append(el( 'p', - subscription.target_verified ? 'channel-key-state' : 'channel-error', - subscription.target_verified - ? `目标已核对 · 收件指纹 ${subscription.recipient_fingerprint || '—'} · 会话指纹 ${subscription.conversation_fingerprint || '—'}` - : '主动投递目标尚未核对' + active ? 'channel-key-state' : channel.routing_ready ? 'muted' : 'channel-error', + active + ? '组长身份和 AgentBus 路由已自动识别,无需维护收件地址或微信会话 ID。' + : !channel.enabled + ? '重新启用该组长渠道后,系统会自动恢复摘要抄送;停用期间不会发送。' + : !channel.routing_ready + ? '请先补齐该渠道的平台账号与 ERP 账号绑定。' + : !routeReady && !channel.external_user_ref + ? '渠道未设置外部用户标识。组长从微信向该 AgentBus 渠道发送首条有效消息后,系统会自动学习路由并启用。' + : 'AgentBus 路由正在自动同步,通常会在几秒内生效。' )); - main.append(el('p', 'muted', `当前队列:待发 ${subscription.pending_count || 0} · 失败待重试 ${subscription.failed_count || 0} · 累计已交给 AgentBus ${subscription.delivered_count || 0}`)); - main.append(el('p', subscription.eligible || !subscription.enabled ? 'muted' : 'channel-error', subscription.eligibility_message || '')); - main.append(el('p', 'muted', `本轮起点:${formatDateTime(subscription.starts_at)} · 最近投递:${formatDateTime(subscription.last_delivered_at)}`)); - const actions = el('div', 'channel-row-actions'); - const edit = el('button', 'secondary-button', '编辑'); - edit.type = 'button'; - edit.dataset.leaderSummaryAction = 'edit'; - edit.dataset.leaderSummaryChannelId = subscription.channel_id; - edit.disabled = leaderSummarySettingsBusy; - const toggle = el('button', subscription.enabled ? 'danger-button' : 'secondary-button', subscription.enabled ? '关闭' : '启用'); - toggle.type = 'button'; - toggle.dataset.leaderSummaryAction = 'toggle'; - toggle.dataset.leaderSummaryId = subscription.id; - toggle.disabled = leaderSummarySettingsBusy; - actions.append(edit, toggle); - row.append(main, actions); + if (subscription) { + main.append(el('p', 'muted', `当前队列:待发 ${subscription.pending_count || 0} · 失败待重试 ${subscription.failed_count || 0} · 累计已交给 AgentBus ${subscription.delivered_count || 0}`)); + main.append(el('p', subscription.eligible ? 'muted' : 'channel-error', subscription.eligibility_message || '')); + main.append(el('p', 'muted', `自动生效起点:${formatDateTime(subscription.starts_at)} · 最近投递:${formatDateTime(subscription.last_delivered_at)}`)); + } + row.append(main); container.append(row); } } @@ -1069,121 +1011,10 @@ async function syncLeaderSummarySubscriptions() { if (!isAdministrator()) return []; const result = await apiRequest('/api/settings/leader-summary-subscriptions'); leaderSummarySubscriptions = Array.isArray(result.subscriptions) ? result.subscriptions : []; - renderLeaderSummaryChannelOptions(); renderLeaderSummarySubscriptions(); return leaderSummarySubscriptions; } -function confirmLeaderSummaryEnable(subscription) { - return window.confirm( - `确认启用“${subscription?.leader_username || '所选组长'}”的任务摘要抄送?\n\n` - + '只会发送本次保存后的稳定结果,不补发历史;消息正文不含原始指令、客户/游客资料或技术错误。AgentBus 主动投递按至少一次处理,极少情况下可能重复,已经到达微信的消息无法撤回。' - ); -} - -async function saveLeaderSummaryFromForm() { - if (!isAdministrator() || leaderSummarySettingsBusy) return; - const channelId = String($('#leaderSummaryChannelId')?.value || ''); - const channel = channelList.find((item) => item.id === channelId); - const subscription = leaderSummarySubscriptionForChannel(channelId); - const leaderUserId = channel?.owner_user_id || subscription?.leader_user_id || ''; - const recipientAddress = normalizeText($('#leaderSummaryRecipientAddress')?.value).slice(0, 500); - const conversationId = normalizeText($('#leaderSummaryConversationId')?.value).slice(0, 500); - const includeManual = $('#leaderSummaryIncludeManual')?.checked === true; - const includeAgentbus = $('#leaderSummaryIncludeAgentbus')?.checked === true; - const targetVerified = $('#leaderSummaryTargetVerified')?.checked === true; - const enabled = $('#leaderSummaryEnabled')?.checked === true; - const message = $('#leaderSummaryMessage'); - if (!channelId || !leaderUserId) { - if (message) message.textContent = '请选择有效的组长 AgentBus 渠道。'; - return; - } - if (!includeManual && !includeAgentbus) { - if (message) message.textContent = '人工任务和 AgentBus 任务至少选择一种。'; - return; - } - if (!subscription && (!recipientAddress || !conversationId)) { - if (message) message.textContent = '首次配置必须填写 AgentBus 收件地址和微信会话 ID。'; - return; - } - if (enabled && !targetVerified) { - if (message) message.textContent = '启用前请先核对主动投递目标并勾选确认。'; - return; - } - if ((recipientAddress || conversationId) && targetVerified && !window.confirm( - '你正在保存新的主动投递目标。请确认 AgentBus 收件地址与微信会话 ID 已在当前桥接环境中核对无误;系统不会自动发送测试消息。' - )) return; - if (enabled && !subscription?.enabled && !confirmLeaderSummaryEnable(subscription || { - leader_username: channel.owner_username - })) return; - - leaderSummarySettingsBusy = true; - renderLeaderSummaryChannelOptions(); - renderLeaderSummarySubscriptions(); - if (message) message.textContent = '正在保存组长摘要设置…'; - try { - const body = { - channel_id: channelId, - include_manual: includeManual, - include_agentbus: includeAgentbus, - target_verified: targetVerified, - enabled, - ...(recipientAddress ? { recipient_address: recipientAddress } : {}), - ...(conversationId ? { conversation_id: conversationId } : {}), - ...(subscription ? { expected_revision: subscription.revision } : {}) - }; - await apiRequest(`/api/settings/leader-summary-subscriptions/${encodeURIComponent(leaderUserId)}`, { - method: 'PUT', - body - }); - leaderSummaryEditingChannelId = ''; - await syncLeaderSummarySubscriptions(); - const saved = leaderSummarySubscriptionForChannel(channelId); - if (message) { - message.textContent = saved?.enabled - ? '设置已启用;只会从本轮起点之后的稳定任务结果开始抄送。' - : '设置已保存但仍处于关闭状态,不会发送消息。'; - } - } finally { - leaderSummarySettingsBusy = false; - renderLeaderSummaryChannelOptions(); - renderLeaderSummarySubscriptions(); - } -} - -async function toggleLeaderSummarySubscription(subscriptionId) { - const subscription = leaderSummarySubscriptions.find((item) => item.id === subscriptionId); - if (!subscription || leaderSummarySettingsBusy) return; - const nextEnabled = !subscription.enabled; - if (nextEnabled && !confirmLeaderSummaryEnable(subscription)) return; - leaderSummarySettingsBusy = true; - renderLeaderSummaryChannelOptions(); - renderLeaderSummarySubscriptions(); - try { - await apiRequest(`/api/settings/leader-summary-subscriptions/${encodeURIComponent(subscription.leader_user_id)}`, { - method: 'PUT', - body: { - channel_id: subscription.channel_id, - include_manual: subscription.include_manual, - include_agentbus: subscription.include_agentbus, - target_verified: subscription.target_verified, - enabled: nextEnabled, - expected_revision: subscription.revision - } - }); - leaderSummaryEditingChannelId = ''; - await syncLeaderSummarySubscriptions(); - const message = $('#leaderSummaryMessage'); - if (message) message.textContent = nextEnabled - ? '摘要抄送已启用;不会补发此前的任务。' - : '摘要抄送已关闭;尚未发送的旧目标摘要已取消。'; - } finally { - leaderSummarySettingsBusy = false; - renderLeaderSummaryChannelOptions(); - renderLeaderSummarySubscriptions(); - } -} - function accountRoleLabel(role) { if (role === 'admin') return '管理员'; if (role === 'team_lead') return '组长'; @@ -1418,7 +1249,7 @@ async function syncAccounts() { accountTaskTypes = Array.isArray(result.task_types) ? result.task_types : []; renderAccounts(); renderChannelOwnerOptions(); - renderLeaderSummaryChannelOptions(); + renderLeaderSummarySubscriptions(); } async function createAccountFromForm() { @@ -5804,7 +5635,7 @@ async function initializeSession() { }); await syncLeaderSummarySubscriptions().catch((error) => { const message = $('#leaderSummaryMessage'); - if (message) message.textContent = `组长摘要设置读取失败:${error.message || String(error)}`; + if (message) message.textContent = `组长摘要状态读取失败:${error.message || String(error)}`; }); } if (IS_ACCOUNTS_PAGE && isAdministrator()) { @@ -5979,7 +5810,6 @@ document.addEventListener('DOMContentLoaded', async () => { accountTaskTypes = []; channelList = []; leaderSummarySubscriptions = []; - leaderSummaryEditingChannelId = ''; accountAuthorizationTargetId = ''; accountAuthorizationDraft = new Set(); historySelectedTaskIds.clear(); @@ -6272,40 +6102,6 @@ document.addEventListener('DOMContentLoaded', async () => { void syncChannels(); }); }); - $('#leaderSummaryForm')?.addEventListener('submit', (event) => { - event.preventDefault(); - void saveLeaderSummaryFromForm().catch((error) => { - const message = $('#leaderSummaryMessage'); - if (message) message.textContent = error.message || String(error); - }); - }); - $('#leaderSummaryChannelId')?.addEventListener('change', (event) => { - populateLeaderSummaryForm(event.target.value); - }); - for (const inputId of ['leaderSummaryRecipientAddress', 'leaderSummaryConversationId']) { - $(`#${inputId}`)?.addEventListener('input', () => { - const verified = $('#leaderSummaryTargetVerified'); - if (verified) verified.checked = false; - }); - } - $('#leaderSummaryList')?.addEventListener('click', (event) => { - const button = event.target.closest('[data-leader-summary-action]'); - if (!button) return; - if (button.dataset.leaderSummaryAction === 'edit') { - const channelId = button.dataset.leaderSummaryChannelId || ''; - const select = $('#leaderSummaryChannelId'); - if (select) select.value = channelId; - populateLeaderSummaryForm(channelId); - $('#leaderSummaryForm')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - return; - } - if (button.dataset.leaderSummaryAction === 'toggle') { - void toggleLeaderSummarySubscription(button.dataset.leaderSummaryId).catch((error) => { - const message = $('#leaderSummaryMessage'); - if (message) message.textContent = error.message || String(error); - }); - } - }); $('#accountRole')?.addEventListener('change', (event) => { const erpInput = $('#accountErpAccount'); const admin = event.target.value === 'admin'; diff --git a/LianSyn-platform/index.html b/LianSyn-platform/index.html index 14546f0..530d569 100644 --- a/LianSyn-platform/index.html +++ b/LianSyn-platform/index.html @@ -5,7 +5,7 @@
LEADER SUMMARY COPY
把同组织内其他非管理员员工的稳定任务结果,经组长自己的 AgentBus 渠道主动投递到指定微信会话。人工任务与 AgentBus 任务均可纳入。
+身份为组长且已有可用 AgentBus 渠道时,系统自动把同组织内其他非管理员员工的稳定任务结果抄送到组长微信。人工任务与 AgentBus 任务都会纳入。