feat: isolate administrators from task data plane
This commit is contained in:
1 parent
03d01315a9
commit
fd39347603
13 files changed
+644
-112
No files matched your search
@@ -4,7 +4,7 @@
|
||||
|
||||
操作台首页只展示按创建时间倒序排列的最近 10 条任务;任务数量超过 10 条时,点击“查看更多”进入 `/history` 历史任务目录。历史目录仍使用同一登录会话和任务详情面板,支持按任务编号/摘要搜索、按状态筛选、分页,以及补充信息、确认提交、插件回查和彻底删除等现有操作。历史目录不新增归档状态,服务端持久化任务仍是唯一事实源。
|
||||
|
||||
操作台不会在页面刷新、插件重连或状态轮询时自动重交 ERP 任务。只有管理员的首次明确确认会申请服务端唯一执行权;领取成功后才向插件下发。领取后的投递超时或 ERP 回执不确定都会进入“待回查”,禁止自动重试。
|
||||
操作台不会在页面刷新、插件重连或状态轮询时自动重交 ERP 任务。只有任务所属员工账号的首次明确确认会申请服务端唯一执行权;领取成功后才向该账号绑定的插件下发。领取后的投递超时或 ERP 回执不确定都会进入“待回查”,禁止自动重试。管理员只使用账号、渠道、解析策略和审计等管理功能,不进入任务数据面。
|
||||
|
||||
生产控制平面说明见 [../control-plane/README.md](../control-plane/README.md)。
|
||||
|
||||
|
||||
+129
-36
@@ -163,12 +163,16 @@ function isTeamLeader() {
|
||||
return authUser?.role === 'team_lead';
|
||||
}
|
||||
|
||||
function canUseTaskDataPlane(user = authUser) {
|
||||
return user?.role === 'team_lead' || user?.role === 'user';
|
||||
}
|
||||
|
||||
function canViewOperationsDashboard() {
|
||||
return isAdministrator() || isTeamLeader();
|
||||
return isTeamLeader();
|
||||
}
|
||||
|
||||
function connectionIdForUser(user) {
|
||||
if (!user?.id) return '';
|
||||
if (!user?.id || !canUseTaskDataPlane(user)) return '';
|
||||
const storageKey = `liansyn_platform_browser_connection_id:${user.id}`;
|
||||
let connectionId = localStorage.getItem(storageKey) || '';
|
||||
if (!/^platform-browser:[a-f0-9-]{16,80}$/i.test(connectionId)) {
|
||||
@@ -237,6 +241,11 @@ async function encodeRosterAttachment(file) {
|
||||
}
|
||||
|
||||
async function apiRequest(path, options = {}) {
|
||||
if (authUser && !canUseTaskDataPlane() && isTaskDataPlaneApiPath(path)) {
|
||||
const accessError = new Error('管理员账号仅用于平台管理,不能访问业务任务。');
|
||||
accessError.code = 'task_access_forbidden';
|
||||
throw accessError;
|
||||
}
|
||||
const method = String(options.method || 'GET').toUpperCase();
|
||||
const headers = new Headers(options.headers || {});
|
||||
const timeoutMs = Number(options.timeoutMs) || requestTimeoutForPath(path);
|
||||
@@ -273,6 +282,60 @@ async function apiRequest(path, options = {}) {
|
||||
return payload || {};
|
||||
}
|
||||
|
||||
function isTaskDataPlaneApiPath(path) {
|
||||
const pathname = String(path || '').split(/[?#]/, 1)[0];
|
||||
return pathname === '/api/messages'
|
||||
|| pathname === '/api/connections/heartbeat'
|
||||
|| pathname === '/api/events'
|
||||
|| pathname === '/api/tasks'
|
||||
|| pathname.startsWith('/api/tasks/')
|
||||
|| pathname.startsWith('/api/parser-decisions/')
|
||||
|| pathname === '/api/operations-dashboard'
|
||||
|| pathname.startsWith('/api/operations-dashboard/');
|
||||
}
|
||||
|
||||
function hideAuthenticatedNavigation() {
|
||||
for (const selector of [
|
||||
'#workbenchNav',
|
||||
'#historyNav',
|
||||
'#operationsDashboardNav',
|
||||
'#channelsNav',
|
||||
'#parserRoutingNav',
|
||||
'#accountsNav',
|
||||
'#auditNav',
|
||||
'#bridgeState'
|
||||
]) {
|
||||
const element = $(selector);
|
||||
if (element) element.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function clearTaskDataPlaneClientState() {
|
||||
currentTaskId = '';
|
||||
taskStore = [];
|
||||
runtimeTaskStore.clear();
|
||||
remoteTaskStore.clear();
|
||||
taskDetailStore.clear();
|
||||
taskDetailRequests.clear();
|
||||
taskInputHistoryStore.clear();
|
||||
taskInputHistoryRequests.clear();
|
||||
localTaskOverlayStore.clear();
|
||||
extensionResultPersistQueues.clear();
|
||||
pendingExtensionResults.clear();
|
||||
persistedExtensionResultVersions.clear();
|
||||
taskArchiveStates.clear();
|
||||
taskDeleteStates.clear();
|
||||
taskReplyStates.clear();
|
||||
taskReplyDrafts.clear();
|
||||
taskAttachmentDrafts.clear();
|
||||
historySelectedTaskIds.clear();
|
||||
locallyDeletedTaskIds.clear();
|
||||
sessionStorage.removeItem('liansyn_platform_current_task_id');
|
||||
stopPolling();
|
||||
if (eventStream) eventStream.close();
|
||||
eventStream = null;
|
||||
}
|
||||
|
||||
function showLoginPanel(message = '') {
|
||||
authUser = null;
|
||||
browserConnectionId = '';
|
||||
@@ -314,6 +377,7 @@ function showLoginPanel(message = '') {
|
||||
if (automationButton) automationButton.hidden = true;
|
||||
if (logoutButton) logoutButton.hidden = true;
|
||||
if (changePasswordButton) changePasswordButton.hidden = true;
|
||||
hideAuthenticatedNavigation();
|
||||
const submitButton = $('#loginForm button[type="submit"]');
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
const error = $('#loginError');
|
||||
@@ -354,6 +418,7 @@ function showAuthChecking() {
|
||||
if (automationButton) automationButton.hidden = true;
|
||||
if (logoutButton) logoutButton.hidden = true;
|
||||
if (changePasswordButton) changePasswordButton.hidden = true;
|
||||
hideAuthenticatedNavigation();
|
||||
const submitButton = $('#loginForm button[type="submit"]');
|
||||
if (submitButton) submitButton.disabled = true;
|
||||
const error = $('#loginError');
|
||||
@@ -362,14 +427,22 @@ function showAuthChecking() {
|
||||
|
||||
function showAuthenticatedApp(user) {
|
||||
authUser = user;
|
||||
browserConnectionId = connectionIdForUser(user);
|
||||
browserConnectionId = canUseTaskDataPlane() ? connectionIdForUser(user) : '';
|
||||
if (isAdministrator()) {
|
||||
clearTaskDataPlaneClientState();
|
||||
if (user?.id) localStorage.removeItem(`liansyn_platform_browser_connection_id:${user.id}`);
|
||||
}
|
||||
if (isAdministrator() && (IS_TASK_PAGE || IS_OPERATIONS_DASHBOARD_PAGE)) {
|
||||
window.location.replace('/accounts');
|
||||
return false;
|
||||
}
|
||||
if (!isAdministrator() && IS_ADMIN_PAGE) {
|
||||
window.location.replace('/');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (IS_OPERATIONS_DASHBOARD_PAGE && !canViewOperationsDashboard()) {
|
||||
window.location.replace('/');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const panel = $('#loginPanel');
|
||||
const workbench = $('#workbench');
|
||||
@@ -381,11 +454,12 @@ function showAuthenticatedApp(user) {
|
||||
const passwordPanel = $('#passwordChangePanel');
|
||||
const authState = $('#authState');
|
||||
const automationButton = $('#automationToggleButton');
|
||||
const bridgeState = $('#bridgeState');
|
||||
const logoutButton = $('#logoutButton');
|
||||
const changePasswordButton = $('#changePasswordButton');
|
||||
if (panel) panel.hidden = true;
|
||||
if (passwordPanel) passwordPanel.hidden = true;
|
||||
if (workbench) workbench.hidden = IS_MANAGEMENT_PAGE;
|
||||
if (workbench) workbench.hidden = IS_MANAGEMENT_PAGE || !canUseTaskDataPlane();
|
||||
if (channelsPage) channelsPage.hidden = !IS_CHANNELS_PAGE || !isAdministrator();
|
||||
if (parserRoutingPage) parserRoutingPage.hidden = !IS_PARSER_ROUTING_PAGE || !isAdministrator();
|
||||
if (accountsPage) accountsPage.hidden = !IS_ACCOUNTS_PAGE || !isAdministrator();
|
||||
@@ -405,12 +479,18 @@ function showAuthenticatedApp(user) {
|
||||
}
|
||||
const operationsDashboardNav = $('#operationsDashboardNav');
|
||||
if (operationsDashboardNav) operationsDashboardNav.hidden = !canViewOperationsDashboard();
|
||||
for (const selector of ['#workbenchNav', '#historyNav']) {
|
||||
const nav = $(selector);
|
||||
if (nav) nav.hidden = !canUseTaskDataPlane();
|
||||
}
|
||||
if (bridgeState) bridgeState.hidden = !canUseTaskDataPlane();
|
||||
if (automationButton) {
|
||||
automationButton.hidden = !isAdministrator() || IS_MANAGEMENT_PAGE;
|
||||
renderAutomationToggle();
|
||||
}
|
||||
const submitButton = $('#loginForm button[type="submit"]');
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function showPasswordChangePanel() {
|
||||
@@ -1140,7 +1220,7 @@ function renderAccounts() {
|
||||
revoke.dataset.accountAction = 'revoke-sessions';
|
||||
revoke.dataset.accountId = account.id;
|
||||
revoke.disabled = accountSettingsBusy;
|
||||
const authorizations = el('button', 'secondary-button', account.role === 'admin' ? '全部任务' : '任务权限');
|
||||
const authorizations = el('button', 'secondary-button', account.role === 'admin' ? '不参与任务' : '任务权限');
|
||||
authorizations.type = 'button';
|
||||
authorizations.dataset.accountAction = 'business-authorizations';
|
||||
authorizations.dataset.accountId = account.id;
|
||||
@@ -1280,7 +1360,7 @@ async function createAccountFromForm() {
|
||||
$('#accountErpAccount').value = '';
|
||||
if (message) {
|
||||
message.textContent = result.account?.role === 'admin'
|
||||
? '管理员账号已创建;该角色固定拥有全部任务权限,初始密码不会再次显示。'
|
||||
? '管理员账号已创建;该角色仅能使用平台管理功能,不具备任何任务权限。'
|
||||
: '账号已创建,当前默认不能执行任何业务;请点击“任务权限”完成授权。';
|
||||
}
|
||||
} finally {
|
||||
@@ -1859,7 +1939,7 @@ async function refreshCsrfToken() {
|
||||
}
|
||||
|
||||
async function syncRemoteTasks() {
|
||||
if (!authUser) return;
|
||||
if (!canUseTaskDataPlane()) return;
|
||||
if (remoteSyncInProgress) {
|
||||
syncRequested = true;
|
||||
return;
|
||||
@@ -1907,12 +1987,12 @@ async function syncRemoteTasks() {
|
||||
if (currentTaskId) sessionStorage.setItem('liansyn_platform_current_task_id', currentTaskId);
|
||||
else sessionStorage.removeItem('liansyn_platform_current_task_id');
|
||||
renderTaskCards();
|
||||
} while (syncRequested && authUser);
|
||||
} while (syncRequested && canUseTaskDataPlane());
|
||||
await syncRuntimeTasks();
|
||||
await autoDispatchReadyTasks();
|
||||
} finally {
|
||||
remoteSyncInProgress = false;
|
||||
if (syncRequested && authUser) {
|
||||
if (syncRequested && canUseTaskDataPlane()) {
|
||||
syncRequested = false;
|
||||
queueMicrotask(() => syncRemoteTasks().catch((error) => {
|
||||
setOutput({ status: 'sync_error', message: error.message });
|
||||
@@ -1923,6 +2003,8 @@ async function syncRemoteTasks() {
|
||||
|
||||
function startRemoteEventStream() {
|
||||
if (eventStream) eventStream.close();
|
||||
eventStream = null;
|
||||
if (!canUseTaskDataPlane()) return;
|
||||
const lastEventId = runtimeTasks().filter(taskAssignedToCurrentAccount).reduce((highest, task) => (
|
||||
Math.max(highest, Number(task?.last_event_id || 0))
|
||||
), 0);
|
||||
@@ -1980,7 +2062,7 @@ function hasPollableRuntimeTasks() {
|
||||
}
|
||||
|
||||
async function syncRuntimeTasks() {
|
||||
if (!authUser) return;
|
||||
if (!canUseTaskDataPlane()) return;
|
||||
const result = await apiRequest('/api/tasks?status=active&limit=200&include_total=false&executable_by=me');
|
||||
const activeTasks = (Array.isArray(result.tasks) ? result.tasks : [])
|
||||
.map(mergeRemoteTask)
|
||||
@@ -2269,12 +2351,12 @@ function taskStatusText(task) {
|
||||
if (status === 'parse_blocked') return '拆解失败';
|
||||
if (status === 'dry_run') return '已规划,未写入 ERP';
|
||||
if (status === 'operation_blocked') return '业务未接入';
|
||||
if (status === 'execution_uncertain') return '待管理员核验,请勿重复提交';
|
||||
if (status === 'reconciliation_pending') return '待管理员核验,请勿重复提交';
|
||||
if (status === 'execution_uncertain') return '待当前账号只读核验,请勿重复提交';
|
||||
if (status === 'reconciliation_pending') return '待当前账号只读核验,请勿重复提交';
|
||||
if (status === 'cancelled') return '已取消';
|
||||
if (status === 'post_save_recovery_required') return '需要恢复导出';
|
||||
if (status === 'blocked') return '已阻断';
|
||||
if (status === 'saved_unverified') return '待管理员核验,请勿重复提交';
|
||||
if (status === 'saved_unverified') return '待当前账号只读核验,请勿重复提交';
|
||||
return stage || status;
|
||||
}
|
||||
|
||||
@@ -2306,9 +2388,9 @@ function taskStatusLabel(task) {
|
||||
waiting_extension: '等待插件',
|
||||
dry_run: '已规划',
|
||||
operation_blocked: '已阻断',
|
||||
execution_uncertain: '待管理员核验',
|
||||
reconciliation_pending: '待管理员核验',
|
||||
saved_unverified: '待管理员核验',
|
||||
execution_uncertain: '待人工核验',
|
||||
reconciliation_pending: '待人工核验',
|
||||
saved_unverified: '待人工核验',
|
||||
cancelled: '已取消',
|
||||
post_save_recovery_required: '待恢复'
|
||||
};
|
||||
@@ -2332,7 +2414,7 @@ function requiresManualConfirmation(task) {
|
||||
}
|
||||
|
||||
function taskAssignedToCurrentAccount(task) {
|
||||
return Boolean(authUser?.id && task?.assignee?.id === authUser.id);
|
||||
return Boolean(canUseTaskDataPlane() && authUser?.id && task?.assignee?.id === authUser.id);
|
||||
}
|
||||
|
||||
function extensionTaskBelongsToCurrentAccount(taskId) {
|
||||
@@ -2676,7 +2758,7 @@ function isAutomaticTask(task) {
|
||||
}
|
||||
|
||||
async function autoDispatchReadyTasks({ force = false } = {}) {
|
||||
if (!authUser || autoHandoffInProgress) return;
|
||||
if (!canUseTaskDataPlane() || autoHandoffInProgress) return;
|
||||
autoHandoffInProgress = true;
|
||||
try {
|
||||
const result = await apiRequest('/api/tasks?status=confirmed&limit=200&include_total=false&executable_by=me');
|
||||
@@ -3389,7 +3471,7 @@ function taskStageSnapshot(task) {
|
||||
business = {
|
||||
mode: 'needs',
|
||||
current: 'processing',
|
||||
currentLabel: '待管理员核验'
|
||||
currentLabel: '待人工核验'
|
||||
};
|
||||
} else if (['blocked', 'failed', 'cancelled', 'extension_error', 'batch_fallback_incomplete', 'live_submit_blocked', 'preflight_blocked'].includes(status)) {
|
||||
business = {
|
||||
@@ -3517,7 +3599,7 @@ function renderTaskStages(task) {
|
||||
|
||||
function parserEngineLabel(parser = {}) {
|
||||
if (parser.fallback_reason && parser.fallback_reason !== 'manual_ai_reparse') return 'AI 兜底';
|
||||
if (parser.fallback_reason === 'manual_ai_reparse') return '管理员 AI 重解析';
|
||||
if (parser.fallback_reason === 'manual_ai_reparse') return '人工 AI 重解析';
|
||||
return parser.authoritative_engine === 'program' ? '程序解析'
|
||||
: parser.authoritative_engine === 'ai' ? 'AI 解析' : '尚未确定';
|
||||
}
|
||||
@@ -3689,7 +3771,7 @@ function renderTaskInputAudit(task) {
|
||||
const item = el('article', 'detail-block');
|
||||
const sourceLabel = message.source === 'manual' ? '人工输入'
|
||||
: message.source === 'agentbus' ? 'AgentBus'
|
||||
: message.source === 'reparse' ? '管理员重解析' : '历史/系统';
|
||||
: message.source === 'reparse' ? '人工重解析' : '历史/系统';
|
||||
item.append(el(
|
||||
'p',
|
||||
'muted',
|
||||
@@ -4622,6 +4704,9 @@ function makeRequestId() {
|
||||
}
|
||||
|
||||
function sendToExtension(type, payload = {}, timeoutMs = 2500) {
|
||||
if (!canUseTaskDataPlane()) {
|
||||
return Promise.reject(new Error('管理员账号不能连接或调用任务执行插件。'));
|
||||
}
|
||||
const requestId = makeRequestId();
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -4643,6 +4728,7 @@ window.addEventListener('message', (event) => {
|
||||
if (event.source !== window) return;
|
||||
const message = event.data || {};
|
||||
if (message.source !== EXTENSION_SOURCE) return;
|
||||
if (!canUseTaskDataPlane()) return;
|
||||
if (message.type === 'BRIDGE_READY') {
|
||||
applyBridgePayload(message.payload || { ok: true });
|
||||
return;
|
||||
@@ -4999,6 +5085,7 @@ async function confirmAndSubmitToErpPlugin(task) {
|
||||
}
|
||||
|
||||
async function pingBridge() {
|
||||
if (!canUseTaskDataPlane()) return false;
|
||||
try {
|
||||
const result = await sendToExtension('PING', {
|
||||
expected_erp_account: authUser?.erp_account || ''
|
||||
@@ -5067,6 +5154,10 @@ async function createTask() {
|
||||
showLoginPanel('请先登录。');
|
||||
return;
|
||||
}
|
||||
if (!canUseTaskDataPlane()) {
|
||||
window.location.replace('/accounts');
|
||||
return;
|
||||
}
|
||||
if (taskCreateInProgress) return;
|
||||
const rawText = $('#rawInstruction').value;
|
||||
const fileInput = $('#rosterAttachment');
|
||||
@@ -5117,12 +5208,13 @@ async function refreshBackgroundState() {
|
||||
if (!authUser || backgroundRefreshInProgress) return;
|
||||
backgroundRefreshInProgress = true;
|
||||
try {
|
||||
const operations = [pingAi(), pingBridge()];
|
||||
const operations = [pingAi()];
|
||||
if (canUseTaskDataPlane()) operations.push(pingBridge());
|
||||
if (isAdministrator()) operations.push(syncAutomationSettings({ background: true }));
|
||||
if (IS_CHANNELS_PAGE && isAdministrator()) {
|
||||
operations.push(syncChannels(), syncLeaderSummarySubscriptions());
|
||||
}
|
||||
if (IS_TASK_PAGE) operations.push(syncRemoteTasks());
|
||||
if (IS_TASK_PAGE && canUseTaskDataPlane()) operations.push(syncRemoteTasks());
|
||||
await Promise.allSettled(operations);
|
||||
} finally {
|
||||
backgroundRefreshInProgress = false;
|
||||
@@ -5160,7 +5252,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 中已经发生的操作;系统只会向任务所属账号的在线插件发送停止与清理指令。已经交给 AgentBus 并到达组长微信的任务摘要也无法撤回。`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5540,7 +5632,7 @@ async function resumePrewriteTaskExecution(task) {
|
||||
}
|
||||
|
||||
async function pollAllTaskResults() {
|
||||
if (!bridgeConnected || pollInProgress) return;
|
||||
if (!canUseTaskDataPlane() || !bridgeConnected || pollInProgress) return;
|
||||
pollInProgress = true;
|
||||
try {
|
||||
const activeTasks = runtimeTasks().filter((task) => taskAssignedToCurrentAccount(task) && isTaskPollable(task));
|
||||
@@ -5567,6 +5659,7 @@ function stopPolling() {
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
if (!canUseTaskDataPlane()) return;
|
||||
pollAllTaskResults().catch((error) => {
|
||||
setTaskState('轮询失败');
|
||||
setOutput({ status: 'poll_error', message: error.message });
|
||||
@@ -5616,7 +5709,7 @@ async function initializeSession() {
|
||||
return false;
|
||||
}
|
||||
|
||||
showAuthenticatedApp(me.user);
|
||||
if (!showAuthenticatedApp(me.user)) return false;
|
||||
if (isAdministrator()) {
|
||||
try {
|
||||
await syncAutomationSettings();
|
||||
@@ -5656,7 +5749,7 @@ async function initializeSession() {
|
||||
if (message) message.textContent = operationsDashboardSafeError(error);
|
||||
});
|
||||
}
|
||||
if (IS_TASK_PAGE) {
|
||||
if (IS_TASK_PAGE && canUseTaskDataPlane()) {
|
||||
try {
|
||||
await syncRemoteTasks();
|
||||
} catch (error) {
|
||||
@@ -5720,7 +5813,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.message || '登录失败。');
|
||||
csrfToken = result.csrf_token || '';
|
||||
showAuthenticatedApp(result.user);
|
||||
if (!showAuthenticatedApp(result.user)) return;
|
||||
$('#loginPassword').value = '';
|
||||
if (isAdministrator()) {
|
||||
await syncAutomationSettings().catch(() => setTaskState('全自动化设置读取失败'));
|
||||
@@ -5735,12 +5828,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
}
|
||||
if (IS_ACCOUNTS_PAGE && isAdministrator()) await syncAccounts();
|
||||
if (IS_AUDIT_PAGE && isAdministrator()) await syncAuditEvents();
|
||||
if (IS_TASK_PAGE) {
|
||||
if (IS_TASK_PAGE && canUseTaskDataPlane()) {
|
||||
await syncRemoteTasks();
|
||||
startRemoteEventStream();
|
||||
}
|
||||
await pingAi();
|
||||
await pingBridge();
|
||||
if (canUseTaskDataPlane()) await pingBridge();
|
||||
} catch (error) {
|
||||
if (errorNode) errorNode.textContent = error.message || String(error);
|
||||
} finally {
|
||||
@@ -6227,9 +6320,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
void copyTaskLifecycle(button);
|
||||
});
|
||||
if (await initializeSession()) {
|
||||
if (IS_TASK_PAGE) renderTaskCards();
|
||||
if (IS_TASK_PAGE && canUseTaskDataPlane()) renderTaskCards();
|
||||
pingAi().catch(() => {});
|
||||
pingBridge().catch(() => {});
|
||||
if (canUseTaskDataPlane()) pingBridge().catch(() => {});
|
||||
// The control plane caches this health probe; keep the browser refresh
|
||||
// interval conservative so status checks cannot pressure the Agent API.
|
||||
setInterval(() => {
|
||||
@@ -6246,7 +6339,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
window.addEventListener('focus', () => {
|
||||
refreshBackgroundState().catch(() => {});
|
||||
});
|
||||
if (IS_TASK_PAGE && currentTaskId) renderTaskDetail();
|
||||
if (IS_TASK_PAGE && hasPollableRuntimeTasks()) startPolling();
|
||||
if (IS_TASK_PAGE && canUseTaskDataPlane() && currentTaskId) renderTaskDetail();
|
||||
if (IS_TASK_PAGE && canUseTaskDataPlane() && hasPollableRuntimeTasks()) startPolling();
|
||||
}
|
||||
});
|
||||
@@ -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-leader-summary-2">
|
||||
<link rel="stylesheet" href="styles.css?v=20260907-admin-task-isolation-1">
|
||||
</head>
|
||||
<body>
|
||||
<main class="app-shell">
|
||||
@@ -14,8 +14,8 @@
|
||||
<img class="brand-logo" src="assets/ltjt-platform-logo.png" alt="LianSyn-platform">
|
||||
</div>
|
||||
<nav class="top-nav" aria-label="主菜单">
|
||||
<a id="workbenchNav" class="top-nav-item is-active" aria-current="page" href="/">AI操作台</a>
|
||||
<a id="historyNav" class="top-nav-item" href="/history">历史任务</a>
|
||||
<a id="workbenchNav" class="top-nav-item is-active" aria-current="page" href="/" hidden>AI操作台</a>
|
||||
<a id="historyNav" class="top-nav-item" href="/history" hidden>历史任务</a>
|
||||
<a id="operationsDashboardNav" class="top-nav-item" href="/operations-dashboard" hidden>平台运行看板</a>
|
||||
<a id="channelsNav" class="top-nav-item" href="/channels" hidden>AgentBus 渠道</a>
|
||||
<a id="parserRoutingNav" class="top-nav-item" href="/parser-routing" hidden>解析策略</a>
|
||||
@@ -32,7 +32,7 @@
|
||||
<span class="status-icon-label">AI状态</span>
|
||||
<span class="status-icon-value">未连接</span>
|
||||
</button>
|
||||
<button id="bridgeState" type="button" class="status-icon state-bad" title="点击查看插件状态详情" aria-expanded="false" aria-controls="statusDetailsPopover">
|
||||
<button id="bridgeState" type="button" class="status-icon state-bad" title="点击查看插件状态详情" aria-expanded="false" aria-controls="statusDetailsPopover" hidden>
|
||||
<span class="status-icon-glyph" aria-hidden="true">⌁</span>
|
||||
<span class="status-icon-label">插件状态</span>
|
||||
<span class="status-icon-value">未连接</span>
|
||||
@@ -91,7 +91,7 @@
|
||||
<div>
|
||||
<p class="eyebrow">ACCOUNT DIRECTORY</p>
|
||||
<h2>平台账号管理</h2>
|
||||
<p class="muted">员工账号必须绑定唯一 ERP 账号;管理员只负责管理和查看,不绑定员工 ERP 执行身份。</p>
|
||||
<p class="muted">员工账号必须绑定唯一 ERP 账号;管理员仅负责平台配置,不进入任务控制台、不查看或执行业务任务。</p>
|
||||
</div>
|
||||
</div>
|
||||
<form id="accountForm" class="channel-form" novalidate>
|
||||
@@ -423,6 +423,6 @@
|
||||
</section>
|
||||
|
||||
</main>
|
||||
<script src="app.js?v=20260907-leader-summary-2"></script>
|
||||
<script src="app.js?v=20260907-admin-task-isolation-1"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user