feat: add leader AgentBus task summaries

This commit is contained in:
inman committed 2026-09-07 13:01:01 +08:00
1 parent f4664997a8
commit 1a3ab63700
18 files changed
+2158 -19

No files matched your search

+289 -2
View File
@@ -44,6 +44,9 @@ let automationSettingsError = '';
let automationSettingsSyncInFlight = null;
let channelList = [];
let channelSettingsBusy = false;
let leaderSummarySubscriptions = [];
let leaderSummarySettingsBusy = false;
let leaderSummaryEditingChannelId = '';
let parserRoutingRows = [];
let parserRoutingBusy = false;
let accountList = [];
@@ -802,6 +805,7 @@ async function syncChannels() {
channelList = Array.isArray(result.channels) ? result.channels : [];
renderChannelList();
renderChannelOwnerOptions();
renderLeaderSummaryChannelOptions();
return channelList;
}
@@ -858,6 +862,7 @@ async function updateChannelEnabled(channelId, enabled) {
body: { enabled }
});
await syncChannels();
await syncLeaderSummarySubscriptions().catch(() => {});
} finally {
channelSettingsBusy = false;
}
@@ -872,6 +877,7 @@ async function updateChannelOwner(channelId, ownerUserId) {
body: { owner_user_id: ownerUserId }
});
await syncChannels();
await syncLeaderSummarySubscriptions().catch(() => {});
renderChannelOwnerOptions();
} finally {
channelSettingsBusy = false;
@@ -923,7 +929,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');
@@ -935,6 +941,7 @@ async function deleteChannel(channelId) {
method: 'DELETE'
});
channelList = channelList.filter((item) => item.id !== channelId);
await syncLeaderSummarySubscriptions().catch(() => {});
} finally {
channelSettingsBusy = false;
renderChannelList();
@@ -942,6 +949,241 @@ 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) => (
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);
}
function renderLeaderSummarySubscriptions() {
const container = $('#leaderSummaryList');
if (!container) return;
container.replaceChildren();
if (!leaderSummarySubscriptions.length) {
container.append(el('p', 'muted channel-empty', '尚未配置组长摘要抄送。设置保存后仍默认关闭,启用前必须核对主动投递目标。'));
return;
}
for (const subscription of leaderSummarySubscriptions) {
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 || '未知组长'));
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',
subscription.target_verified ? 'channel-key-state' : 'channel-error',
subscription.target_verified
? `目标已核对 · 收件指纹 ${subscription.recipient_fingerprint || '—'} · 会话指纹 ${subscription.conversation_fingerprint || '—'}`
: '主动投递目标尚未核对'
));
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);
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 : [];
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 '组长';
@@ -1176,6 +1418,7 @@ async function syncAccounts() {
accountTaskTypes = Array.isArray(result.task_types) ? result.task_types : [];
renderAccounts();
renderChannelOwnerOptions();
renderLeaderSummaryChannelOptions();
}
async function createAccountFromForm() {
@@ -5045,6 +5288,9 @@ async function refreshBackgroundState() {
try {
const operations = [pingAi(), pingBridge()];
if (isAdministrator()) operations.push(syncAutomationSettings({ background: true }));
if (IS_CHANNELS_PAGE && isAdministrator()) {
operations.push(syncChannels(), syncLeaderSummarySubscriptions());
}
if (IS_TASK_PAGE) operations.push(syncRemoteTasks());
await Promise.allSettled(operations);
} finally {
@@ -5083,7 +5329,7 @@ function confirmTaskHardDelete(tasks) {
? `任务 ${selectedTasks[0].task_id}`
: `所选 ${selectedTasks.length} 个任务`;
return window.confirm(
`确认永久强制删除${scope}?\n\n任务、原始输入、生命周期、执行记录、附件和回执都会被物理删除,无法恢复。此操作不受“正在处理”或“等待 ERP 执行”状态限制。\n\n如果 ERP 已经开始写入,删除平台记录不会撤销 ERP 中已经发生的操作;系统会向任务所属账号的在线插件发送停止与清理指令,不会误发给当前管理员插件。`
`确认永久强制删除${scope}?\n\n任务、原始输入、生命周期、执行记录、附件和回执都会被物理删除,无法恢复。此操作不受“正在处理”或“等待 ERP 执行”状态限制。\n\n如果 ERP 已经开始写入,删除平台记录不会撤销 ERP 中已经发生的操作;系统会向任务所属账号的在线插件发送停止与清理指令,不会误发给当前管理员插件。已经交给 AgentBus 并到达组长微信的任务摘要也无法撤回。`
);
}
@@ -5556,6 +5802,10 @@ async function initializeSession() {
const message = $('#channelMessage');
if (message) message.textContent = `渠道读取失败:${error.message || String(error)}`;
});
await syncLeaderSummarySubscriptions().catch((error) => {
const message = $('#leaderSummaryMessage');
if (message) message.textContent = `组长摘要设置读取失败:${error.message || String(error)}`;
});
}
if (IS_ACCOUNTS_PAGE && isAdministrator()) {
await syncAccounts().catch((error) => {
@@ -5727,6 +5977,9 @@ document.addEventListener('DOMContentLoaded', async () => {
taskAttachmentDrafts.clear();
accountList = [];
accountTaskTypes = [];
channelList = [];
leaderSummarySubscriptions = [];
leaderSummaryEditingChannelId = '';
accountAuthorizationTargetId = '';
accountAuthorizationDraft = new Set();
historySelectedTaskIds.clear();
@@ -6019,6 +6272,40 @@ 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';
+29 -2
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=20260902-account-routing-hard-delete-2">
<link rel="stylesheet" href="styles.css?v=20260907-leader-summary-1">
</head>
<body>
<main class="app-shell">
@@ -277,6 +277,33 @@
<div id="channelList" class="channel-list"></div>
<p id="channelMessage" class="channel-message" role="status"></p>
</section>
<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>
</div>
</div>
<div class="leader-summary-safety" role="note">
默认关闭;只处理保存设置后的新结果,不补发历史任务。收件地址、会话 ID 和摘要正文均加密保存且不回显。AgentBus 主动投递可能极少量重复,已经到达微信的消息无法撤回。
</div>
<form id="leaderSummaryForm" class="leader-summary-form" novalidate>
<label>组长 AgentBus 渠道<select id="leaderSummaryChannelId" required><option value="">请选择组长渠道</option></select></label>
<label>AgentBus 收件地址<input id="leaderSummaryRecipientAddress" maxlength="500" autocomplete="off" placeholder="首次配置或变更时填写"></label>
<label>微信会话 ID<input id="leaderSummaryConversationId" maxlength="500" autocomplete="off" placeholder="首次配置或变更时填写"></label>
<fieldset class="leader-summary-options">
<legend>抄送范围</legend>
<label><input id="leaderSummaryIncludeManual" type="checkbox" checked>人工提交</label>
<label><input id="leaderSummaryIncludeAgentbus" type="checkbox" checked>AgentBus 提交</label>
<label><input id="leaderSummaryTargetVerified" type="checkbox">已核对主动投递目标</label>
<label><input id="leaderSummaryEnabled" type="checkbox">启用抄送</label>
</fieldset>
<button id="leaderSummarySave" type="submit">保存设置</button>
</form>
<div id="leaderSummaryList" class="channel-list leader-summary-list"></div>
<p id="leaderSummaryMessage" class="channel-message" role="status"></p>
</section>
</section>
<section id="parserRoutingPage" class="parser-routing-page" hidden>
@@ -409,6 +436,6 @@
</section>
</main>
<script src="app.js?v=20260902-account-routing-hard-delete-2"></script>
<script src="app.js?v=20260907-leader-summary-1"></script>
</body>
</html>
+84
View File
@@ -634,6 +634,80 @@ textarea {
font-size: 11px;
}
.leader-summary-panel {
min-height: 0 !important;
grid-template-rows: auto auto auto minmax(120px, auto) auto !important;
}
.leader-summary-safety {
padding: 9px 11px;
border: 1px solid #d6c89d;
border-radius: 7px;
background: #fffaf0;
color: #725b18;
font-size: 11px;
line-height: 1.55;
}
.leader-summary-form {
display: grid;
grid-template-columns: minmax(150px, 0.8fr) minmax(180px, 1fr) minmax(180px, 1fr) minmax(230px, 1.2fr) auto;
align-items: end;
gap: 8px;
}
.leader-summary-form > label,
.leader-summary-options {
display: grid;
gap: 5px;
color: var(--muted);
font-size: 11px;
font-weight: 700;
}
.leader-summary-form > label input,
.leader-summary-form > label select {
min-height: 34px;
padding: 6px 8px;
font-size: 12px;
}
.leader-summary-form > button {
min-height: 34px;
white-space: nowrap;
}
.leader-summary-options {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin: 0;
padding: 5px 8px 7px;
border: 1px solid var(--line);
border-radius: 7px;
}
.leader-summary-options legend {
padding: 0 4px;
}
.leader-summary-options label {
display: flex;
align-items: center;
gap: 5px;
color: var(--ink);
font-size: 11px;
font-weight: 500;
}
.leader-summary-options input {
width: 14px;
height: 14px;
margin: 0;
}
.leader-summary-list .channel-row-main p {
overflow-wrap: anywhere;
}
.account-authorization-summary {
color: var(--ink) !important;
}
@@ -1986,10 +2060,18 @@ textarea {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.leader-summary-form {
grid-template-columns: minmax(0, 1fr);
}
.channel-form button {
width: 100%;
}
.leader-summary-form > button {
width: 100%;
}
.channel-row {
align-items: flex-start;
flex-direction: column;
@@ -3037,6 +3119,8 @@ textarea {
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1fr);
align-content: start;
gap: 12px;
overflow: auto;
}