merge: integrate leader summary webhook delivery

# Conflicts:
#	control-plane/README.md
#	control-plane/src/db.ts
#	control-plane/test/control-plane.test.ts
This commit is contained in:
inman committed 2026-09-09 17:25:23 +08:00
commit 295409bff0
21 files changed
+1260 -1269

No files matched your search

+44 -69
View File
@@ -44,7 +44,7 @@ let automationSettingsError = '';
let automationSettingsSyncInFlight = null;
let channelList = [];
let channelSettingsBusy = false;
let leaderSummarySubscriptions = [];
let leaderSummaryWebhookStatus = null;
let parserRoutingRows = [];
let parserRoutingBusy = false;
let accountList = [];
@@ -883,7 +883,6 @@ async function syncChannels() {
channelList = Array.isArray(result.channels) ? result.channels : [];
renderChannelList();
renderChannelOwnerOptions();
renderLeaderSummarySubscriptions();
return channelList;
}
@@ -925,7 +924,6 @@ async function createChannelFromForm() {
$('#channelBotAddress').value = '';
renderChannelList();
renderChannelOwnerOptions();
renderLeaderSummarySubscriptions();
if (message) message.textContent = '渠道已保存,连接状态会在服务端异步更新。';
} finally {
channelSettingsBusy = false;
@@ -941,7 +939,6 @@ async function updateChannelEnabled(channelId, enabled) {
body: { enabled }
});
await syncChannels();
await syncLeaderSummarySubscriptions().catch(() => {});
} finally {
channelSettingsBusy = false;
}
@@ -956,7 +953,6 @@ async function updateChannelOwner(channelId, ownerUserId) {
body: { owner_user_id: ownerUserId }
});
await syncChannels();
await syncLeaderSummarySubscriptions().catch(() => {});
renderChannelOwnerOptions();
} finally {
channelSettingsBusy = false;
@@ -1008,7 +1004,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 回执记录(包括未发送回执)会被移除;历史任务不会被删除。组长摘要已使用独立外部 Webhook,不受该渠道影响。此操作不可撤销。'
);
if (!confirmed) return;
const message = $('#channelMessage');
@@ -1020,7 +1016,6 @@ async function deleteChannel(channelId) {
method: 'DELETE'
});
channelList = channelList.filter((item) => item.id !== channelId);
await syncLeaderSummarySubscriptions().catch(() => {});
} finally {
channelSettingsBusy = false;
renderChannelList();
@@ -1028,71 +1023,52 @@ async function deleteChannel(channelId) {
if (message) message.textContent = `“${channel.display_name || '未命名渠道'}”已删除。`;
}
function leaderSummarySubscriptionForChannel(channelId) {
const channel = channelList.find((item) => item.id === channelId);
return leaderSummarySubscriptions.find((item) => (
channel?.owner_user_id && item.leader_user_id === channel.owner_user_id
)) || leaderSummarySubscriptions.find((item) => item.channel_id === channelId);
}
function renderLeaderSummarySubscriptions() {
function renderLeaderSummaryWebhookStatus() {
const container = $('#leaderSummaryList');
if (!container) return;
container.replaceChildren();
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 渠道的有效组长账号。账号设为组长并绑定渠道后,摘要抄送会自动生效。'));
const status = leaderSummaryWebhookStatus;
if (!status) {
container.append(el('p', 'muted channel-empty', '正在读取外部 Webhook 投递状态…'));
return;
}
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 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);
main.append(el('p', 'muted', `渠道:${channel.display_name || subscription?.channel_name || '未命名渠道'} · 范围:同组织其他非管理员员工 · 来源:人工任务、AgentBus 任务`));
main.append(el(
'p',
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 路由正在自动同步,通常会在几秒内生效。'
));
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);
const active = status.enabled === true && status.state === 'active';
const invalid = status.state === 'invalid_configuration';
const row = el('article', 'channel-row');
const main = el('div', 'channel-row-main');
const heading = el('div', 'channel-row-heading');
heading.append(el('strong', '', '固定微信群 Webhook'));
heading.append(el(
'span',
`state ${active ? 'state-ok' : status.configured || invalid ? 'state-warn' : 'state-bad'}`,
active ? '自动推送' : invalid ? '配置无效' : status.configured ? '配置同步中' : '尚未配置'
));
main.append(heading);
main.append(el('p', 'muted', `配置 ID:${status.webhook_config_id || '9999'} · 范围:同组织非管理员员工 · 来源:人工任务、AgentBus 任务`));
main.append(el(
'p',
active ? 'channel-key-state' : 'channel-error',
status.status_message || '外部 Webhook 状态未知。'
));
main.append(el(
'p',
'muted',
`当前队列:待发 ${status.pending_count || 0} · 明确拒绝 ${status.failed_count || 0} · 结果不确定 ${status.uncertain_count || 0} · 累计已受理 ${status.accepted_count || 0}`
));
if (status.endpoint_fingerprint) {
main.append(el('p', 'muted', `接口指纹:${status.endpoint_fingerprint} · token 与完整 URL 均不回显`));
}
main.append(el('p', 'muted', `自动生效起点:${formatDateTime(status.starts_at)} · 最近受理:${formatDateTime(status.last_accepted_at)}`));
row.append(main);
container.append(row);
}
async function syncLeaderSummarySubscriptions() {
if (!isAdministrator()) return [];
const result = await apiRequest('/api/settings/leader-summary-subscriptions');
leaderSummarySubscriptions = Array.isArray(result.subscriptions) ? result.subscriptions : [];
renderLeaderSummarySubscriptions();
return leaderSummarySubscriptions;
async function syncLeaderSummaryWebhookStatus() {
if (!isAdministrator()) return null;
const result = await apiRequest('/api/settings/leader-summary-webhook');
leaderSummaryWebhookStatus = result.status || null;
renderLeaderSummaryWebhookStatus();
return leaderSummaryWebhookStatus;
}
function accountRoleLabel(role) {
@@ -1329,7 +1305,6 @@ async function syncAccounts() {
accountTaskTypes = Array.isArray(result.task_types) ? result.task_types : [];
renderAccounts();
renderChannelOwnerOptions();
renderLeaderSummarySubscriptions();
}
async function createAccountFromForm() {
@@ -5213,7 +5188,7 @@ async function refreshBackgroundState() {
if (canUseTaskDataPlane()) operations.push(pingBridge());
if (isAdministrator()) operations.push(syncAutomationSettings({ background: true }));
if (IS_CHANNELS_PAGE && isAdministrator()) {
operations.push(syncChannels(), syncLeaderSummarySubscriptions());
operations.push(syncChannels(), syncLeaderSummaryWebhookStatus());
}
if (IS_TASK_PAGE && canUseTaskDataPlane()) operations.push(syncRemoteTasks());
await Promise.allSettled(operations);
@@ -5253,7 +5228,7 @@ function confirmTaskHardDelete(tasks) {
? `任务 ${selectedTasks[0].task_id}`
: `所选 ${selectedTasks.length} 个任务`;
return window.confirm(
`确认永久强制删除${scope}?\n\n任务、原始输入、生命周期、执行记录、附件和回执都会被物理删除,无法恢复。此操作不受“正在处理”或“等待 ERP 执行”状态限制。\n\n如果 ERP 已经开始写入,删除平台记录不会撤销 ERP 中已经发生的操作;系统只会向任务所属账号的在线插件发送停止与清理指令。已经交给 AgentBus 并到达组长微信的任务摘要也无法撤回。`
`确认永久强制删除${scope}?\n\n任务、原始输入、生命周期、执行记录、附件和回执都会被物理删除,无法恢复。此操作不受“正在处理”或“等待 ERP 执行”状态限制。\n\n如果 ERP 已经开始写入,删除平台记录不会撤销 ERP 中已经发生的操作;系统只会向任务所属账号的在线插件发送停止与清理指令。已经由外部 Webhook 受理并可能到达组长微信群的任务摘要也无法撤回。`
);
}
@@ -5727,7 +5702,7 @@ async function initializeSession() {
const message = $('#channelMessage');
if (message) message.textContent = `渠道读取失败:${error.message || String(error)}`;
});
await syncLeaderSummarySubscriptions().catch((error) => {
await syncLeaderSummaryWebhookStatus().catch((error) => {
const message = $('#leaderSummaryMessage');
if (message) message.textContent = `组长摘要状态读取失败:${error.message || String(error)}`;
});
@@ -5903,7 +5878,7 @@ document.addEventListener('DOMContentLoaded', async () => {
accountList = [];
accountTaskTypes = [];
channelList = [];
leaderSummarySubscriptions = [];
leaderSummaryWebhookStatus = null;
accountAuthorizationTargetId = '';
accountAuthorizationDraft = new Set();
historySelectedTaskIds.clear();
+6 -6
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#edf2f4">
<title>AI操作台 · LianSyn-platform</title>
<link rel="stylesheet" href="styles.css?v=20260907-admin-task-isolation-1">
<link rel="stylesheet" href="styles.css?v=20260908-leader-webhook-1">
</head>
<body>
<main class="app-shell">
@@ -280,13 +280,13 @@
<section id="leaderSummaryPanel" class="panel channel-panel leader-summary-panel">
<div class="panel-head">
<div>
<p class="eyebrow">LEADER SUMMARY COPY</p>
<h2>组长任务摘要抄送</h2>
<p class="muted">身份为组长且已有可用 AgentBus 渠道时,系统自动把同组织内其他非管理员员工的稳定任务结果抄送到组长微信。人工任务与 AgentBus 任务都会纳入。</p>
<p class="eyebrow">LEADER SUMMARY WEBHOOK</p>
<h2>组长任务摘要推送</h2>
<p class="muted">系统通过服务器配置的固定外部 Webhook,把同组织内非管理员员工的稳定任务结果推送到组长微信群。该链路不再依赖组长 AgentBus 账号或渠道。</p>
</div>
</div>
<div class="leader-summary-safety" role="note">
无需单独配置或启用。收件路由优先使用 AgentBus 已记录的组长会话,并可从渠道的外部用户标识自动建立;只处理自动启用后的新结果,不补发历史任务。路由和摘要正文均加密保存且不回显。AgentBus 主动投递可能极少量重复,已经到达微信的消息无法撤回。
部署环境必须同时提供完整 WEBHOOK_SEND_URL 与 32 位 WEBHOOK_EXTERNAL_TOKEN,业务请求固定使用 ID 9999。配置缺失或无效只停用本摘要推送,不影响员工任务、AgentBus 或 ERP 执行。只处理配置生效后的新结果,不补发历史任务;摘要正文加密保存,URL 与 token 不回显。接口成功仅表示“已受理”,不代表微信已送达;明确拒绝、超时、断网或 5xx 都不会自动重试,以免重复群通知。
</div>
<div id="leaderSummaryList" class="channel-list leader-summary-list"></div>
<p id="leaderSummaryMessage" class="channel-message" role="status"></p>
@@ -423,6 +423,6 @@
</section>
</main>
<script src="app.js?v=20260907-admin-task-isolation-1"></script>
<script src="app.js?v=20260908-leader-webhook-1"></script>
</body>
</html>