feat: redesign leadership operations dashboard
This commit is contained in:
1 parent
08725b2de9
commit
823d1cb630
7 files changed
+1320
-366
No files matched your search
@@ -1836,17 +1836,50 @@ function operationsDashboardOutcomeKind(statusValue: unknown): OperationsDashboa
|
||||
|
||||
function operationsDashboardOutcomeLabel(kind: OperationsDashboardOutcomeKind): string {
|
||||
if (kind === 'completed') return '已完成';
|
||||
if (kind === 'attention') return '待处理';
|
||||
if (kind === 'failed') return '异常';
|
||||
if (kind === 'attention') return '待跟进';
|
||||
if (kind === 'failed') return '未完成';
|
||||
if (kind === 'cancelled') return '已取消';
|
||||
return '处理中';
|
||||
return '进行中';
|
||||
}
|
||||
|
||||
function operationsDashboardBusiness(routeId: unknown): { id: string | null; label: string } {
|
||||
const route = businessRouteById(routeId);
|
||||
return route
|
||||
? { id: route.routeId, label: route.directive }
|
||||
: { id: null, label: '未识别业务' };
|
||||
: { id: null, label: '其他任务' };
|
||||
}
|
||||
|
||||
function operationsDashboardLeadershipResultText(
|
||||
kind: OperationsDashboardOutcomeKind,
|
||||
value: unknown
|
||||
): string {
|
||||
const result = text(value).trim();
|
||||
if (!result) {
|
||||
if (kind === 'completed') return '工作已经完成。';
|
||||
if (kind === 'attention') return '正在等待补充信息或确认。';
|
||||
if (kind === 'failed') return '本次工作未完成,需要检查后重新处理。';
|
||||
if (kind === 'cancelled') return '这项工作已取消。';
|
||||
return '正在处理中。';
|
||||
}
|
||||
const containsTechnicalLanguage = /(?:\b(?:agent|api|dom|frame|iframe|json|native|operation|parser|payload|plugin|projection|requery|schema|stack|trace|write)\b|[a-z][a-z0-9]*(?:_[a-z0-9]+)+|data\.[a-z]|插件|解析服务|技术上下文|错误码|接口|ERP)/i.test(result);
|
||||
if (!containsTechnicalLanguage) return result;
|
||||
if (/不确定|核验|请勿重复/.test(result)) return '当前结果需要人工确认,请勿重复提交。';
|
||||
if (/未找到|不存在|不匹配|mismatch/i.test(result)) return '未完成:没有找到符合指令条件的业务记录。';
|
||||
if (/登录|连接|网络|超时/i.test(result)) return '未完成:业务系统暂时无法完成处理,请稍后再试。';
|
||||
if (/补充|缺少|必填|日期|年份/.test(result)) return '待跟进:需要补充完整的业务信息。';
|
||||
if (/不支持|超出.*范围/.test(result)) return '未完成:这项要求暂不在当前业务处理范围内。';
|
||||
if (kind === 'completed') return '工作已经完成,结果已记录。';
|
||||
if (kind === 'attention') return '正在等待补充信息或确认。';
|
||||
if (kind === 'cancelled') return '这项工作已取消。';
|
||||
return '未完成:当前业务条件未满足,需要检查后重新处理。';
|
||||
}
|
||||
|
||||
function operationsDashboardLeadershipInstructionText(value: unknown): string {
|
||||
const instruction = text(value).trim();
|
||||
if (!instruction) return '未记录输入内容';
|
||||
const looksLikeTechnicalPayload = /^[\[{]/.test(instruction)
|
||||
|| /(?:"(?:action|operation|payload|schema|submit_mode)"\s*:|[a-z][a-z0-9]*(?:_[a-z0-9]+){2,}|data\.[a-z])/i.test(instruction);
|
||||
return looksLikeTechnicalPayload ? '该历史任务没有可展示的业务指令。' : instruction;
|
||||
}
|
||||
|
||||
function operationsDashboardSuccessText(receipt: PublicTaskSuccessReceipt): string {
|
||||
@@ -1876,11 +1909,11 @@ function operationsDashboardOutcome(row: Record<string, unknown>): {
|
||||
resultText = operationsDashboardSuccessText(receipt);
|
||||
recordedAt = receipt.recorded_at;
|
||||
} else if (status === 'dry_run') {
|
||||
resultText = '业务方案已完成,未写入 ERP。';
|
||||
resultText = '业务方案已完成,尚未正式提交。';
|
||||
}
|
||||
} else if (kind === 'failed') {
|
||||
const error = publicErrorSummary(row, null);
|
||||
resultText = error?.message ? `本次指令未完成:${error.message}` : '本次指令未完成。';
|
||||
resultText = error?.message ? `本次工作未完成:${error.message}` : '本次工作未完成。';
|
||||
if (error) recordedAt = error.recorded_at;
|
||||
} else if (kind === 'attention') {
|
||||
if (status === 'awaiting_user_input') {
|
||||
@@ -1890,19 +1923,20 @@ function operationsDashboardOutcome(row: Record<string, unknown>): {
|
||||
} else if (status === 'awaiting_confirmation') {
|
||||
resultText = '业务方案已生成,等待提交确认。';
|
||||
} else {
|
||||
resultText = '业务结果待核验,请勿重复提交。';
|
||||
resultText = '当前结果需要人工确认,请勿重复提交。';
|
||||
}
|
||||
} else if (kind === 'cancelled') {
|
||||
resultText = '指令已取消,未形成业务结果。';
|
||||
resultText = '这项工作已取消。';
|
||||
} else {
|
||||
resultText = '正在处理,尚未形成业务结果。';
|
||||
}
|
||||
if (!resultText) {
|
||||
if (kind === 'completed') resultText = '业务处理已完成,未记录可展示的结果摘要。';
|
||||
if (kind === 'completed') resultText = '工作已经完成。';
|
||||
else if (kind === 'attention') resultText = '等待业务人员补充或确认。';
|
||||
else if (kind === 'cancelled') resultText = '任务已取消。';
|
||||
else if (kind === 'cancelled') resultText = '这项工作已取消。';
|
||||
else resultText = '正在处理,尚未形成业务结果。';
|
||||
}
|
||||
resultText = operationsDashboardLeadershipResultText(kind, resultText);
|
||||
return {
|
||||
kind,
|
||||
label: operationsDashboardOutcomeLabel(kind),
|
||||
@@ -1921,10 +1955,10 @@ function operationsDashboardTaskProjection(
|
||||
config: AppConfig,
|
||||
row: Record<string, unknown>
|
||||
): OperationsDashboardTaskProjection {
|
||||
const instruction = decryptText(
|
||||
const instruction = operationsDashboardLeadershipInstructionText(decryptText(
|
||||
config,
|
||||
row.original_text_ciphertext as string | null | undefined
|
||||
).trim();
|
||||
));
|
||||
const outcome = operationsDashboardOutcome(row);
|
||||
const business = operationsDashboardBusiness(row.business_route_id);
|
||||
const instructionPreview = instruction.replace(/\s+/g, ' ').trim();
|
||||
@@ -4592,7 +4626,7 @@ export class TaskService {
|
||||
const pool = getPool(this.config);
|
||||
const businessOptions = [
|
||||
...BUSINESS_ROUTES.map((route) => ({ key: route.routeId, label: route.directive })),
|
||||
{ key: 'unclassified', label: '未识别业务' }
|
||||
{ key: 'unclassified', label: '其他任务' }
|
||||
];
|
||||
const actorQuery = () => pool.query(
|
||||
`SELECT id, username, role, is_active
|
||||
@@ -4655,7 +4689,9 @@ export class TaskService {
|
||||
const values = searchValuesByTask.get(key) || [];
|
||||
values.push(
|
||||
text(row.actor_username),
|
||||
decryptText(this.config, row.content_ciphertext as string | null | undefined)
|
||||
operationsDashboardLeadershipInstructionText(
|
||||
decryptText(this.config, row.content_ciphertext as string | null | undefined)
|
||||
)
|
||||
);
|
||||
searchValuesByTask.set(key, values);
|
||||
}
|
||||
@@ -4836,7 +4872,7 @@ export class TaskService {
|
||||
));
|
||||
const businesses = (businessResult.rows as Record<string, unknown>[]).map((row) => {
|
||||
const key = text(row.dimension_key);
|
||||
return breakdown(row, key === 'unclassified' ? '未识别业务' : operationsDashboardBusiness(key).label);
|
||||
return breakdown(row, key === 'unclassified' ? '其他任务' : operationsDashboardBusiness(key).label);
|
||||
});
|
||||
const tasks = (listResult.rows as Record<string, unknown>[]).map((row) => (
|
||||
operationsDashboardTaskProjection(this.config, row).task
|
||||
@@ -4891,7 +4927,7 @@ export class TaskService {
|
||||
.filter((message) => message.source !== 'reparse')
|
||||
.map((message) => ({
|
||||
turn_no: message.turn_no,
|
||||
content: message.content,
|
||||
content: operationsDashboardLeadershipInstructionText(message.content),
|
||||
actor: message.actor,
|
||||
created_at: message.created_at
|
||||
} satisfies PublicOperationsDashboardInstruction));
|
||||
|
||||
@@ -134,6 +134,12 @@ test('operations dashboard is leadership-gated, business-facing, searchable, and
|
||||
assert.match(tasks, /operations_dashboard_search_scope_too_large/);
|
||||
assert.match(tasks, /content_ciphertext[\s\S]+decryptText\(this\.config, row\.content_ciphertext/);
|
||||
assert.match(tasks, /operationsDashboardSearchMatches[\s\S]+projection\.instruction[\s\S]+projection\.result/);
|
||||
assert.match(tasks, /function operationsDashboardLeadershipResultText/);
|
||||
assert.match(tasks, /function operationsDashboardLeadershipInstructionText/);
|
||||
assert.match(tasks, /待跟进/);
|
||||
assert.match(tasks, /未完成/);
|
||||
assert.match(tasks, /containsTechnicalLanguage/);
|
||||
assert.match(tasks, /当前结果需要人工确认/);
|
||||
assert.match(tasks, /timezone\('Asia\/Shanghai', t\.created_at\)/);
|
||||
assert.match(tasks, /days,[\s\S]+businesses,[\s\S]+business_options/);
|
||||
assert.match(tasks, /instructionPreview\.length > 360/);
|
||||
@@ -187,7 +193,7 @@ test('ordinary task access is enforced across reads, mutations, artifacts, event
|
||||
assert.match(server, /tasks\.eventsSince\(session\.user\.organizationId, since, contextFor\(session, request\)\)/);
|
||||
});
|
||||
|
||||
test('operator UI exposes role-aware accounts, business drill-through, original input history, and reversible archive', async () => {
|
||||
test('operator UI exposes role-aware accounts, executive drill-through, original input, final output, and reversible archive', async () => {
|
||||
const [app, index, retention] = await Promise.all([
|
||||
source('../../LianSyn-platform/app.js'),
|
||||
source('../../LianSyn-platform/index.html'),
|
||||
@@ -206,8 +212,14 @@ test('operator UI exposes role-aware accounts, business drill-through, original
|
||||
assert.match(index, /id="operationsDashboardBusiness"/);
|
||||
assert.match(index, /id="operationsDashboardDays"/);
|
||||
assert.match(index, /id="operationsDashboardBusinesses"/);
|
||||
assert.match(index, /姓名、指令内容、完成结果、团号或订单号/);
|
||||
assert.match(index, /平台运行全景/);
|
||||
assert.match(index, /任务规模、人员贡献、业务分布/);
|
||||
assert.match(index, /输入人员、原始指令或最终结果/);
|
||||
assert.match(index, /id="operationsDashboardRangePresets"/);
|
||||
assert.match(index, /id="operationsDashboardRefresh"/);
|
||||
assert.match(index, /从原始输入到最终输出/);
|
||||
assert.match(index, /id="operationsDashboardDetail"/);
|
||||
assert.doesNotMatch(index, /OPERATIONS OVERVIEW|BUSINESS TRACE|指令操作历史/);
|
||||
assert.match(index, /id="historyArchiveInput"/);
|
||||
assert.match(app, /authUser\?\.role === 'admin'/);
|
||||
assert.match(app, /crypto\.randomUUID/);
|
||||
@@ -223,15 +235,29 @@ test('operator UI exposes role-aware accounts, business drill-through, original
|
||||
assert.match(app, /data-operations-status|dataset\.operationsStatus/);
|
||||
assert.match(app, /data-operations-day|dataset\.operationsDay/);
|
||||
assert.match(app, /data-operations-business|dataset\.operationsBusiness/);
|
||||
assert.match(app, /`指令:\$\{task\.instruction_preview/);
|
||||
assert.match(app, /`结果:\$\{task\.result_summary/);
|
||||
assert.match(app, /function operationsDashboardReadableResult/);
|
||||
assert.match(app, /function operationsDashboardReadableInput/);
|
||||
assert.match(app, /原始输入/);
|
||||
assert.match(app, /最终输出/);
|
||||
const dashboardTaskRenderer = app.slice(
|
||||
app.indexOf('function renderOperationsDashboardTasks()'),
|
||||
app.indexOf('function renderOperationsDashboard()')
|
||||
);
|
||||
assert.match(dashboardTaskRenderer, /task\.creator\.username/);
|
||||
assert.match(dashboardTaskRenderer, /task\.created_at/);
|
||||
assert.match(dashboardTaskRenderer, /task\.business_route_label/);
|
||||
assert.match(dashboardTaskRenderer, /task\.instruction_preview/);
|
||||
assert.match(dashboardTaskRenderer, /operationsDashboardReadableInput\(task\.instruction_preview/);
|
||||
assert.match(dashboardTaskRenderer, /operationsDashboardReadableResult\(task\.result_summary/);
|
||||
assert.doesNotMatch(dashboardTaskRenderer, /记录号|task\.task_id\}`/);
|
||||
const dashboardDetailRenderer = app.slice(
|
||||
app.indexOf('function renderOperationsDashboardDetail()'),
|
||||
app.indexOf('async function syncOperationsDashboard()')
|
||||
);
|
||||
assert.match(dashboardDetailRenderer, /操作人/);
|
||||
assert.match(dashboardDetailRenderer, /指令内容/);
|
||||
assert.match(dashboardDetailRenderer, /完成结果/);
|
||||
assert.match(dashboardDetailRenderer, /人员/);
|
||||
assert.match(dashboardDetailRenderer, /原始输入/);
|
||||
assert.match(dashboardDetailRenderer, /最终输出/);
|
||||
assert.match(dashboardDetailRenderer, /operationsDashboardReadableResult/);
|
||||
assert.doesNotMatch(dashboardDetailRenderer, /任务生命周期|处理结果与技术上下文|renderTaskLifecycle|JSON\.stringify|task\.stage|parse_response|operation:/);
|
||||
assert.match(app, /\/api\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/archive/);
|
||||
assert.match(app, /\/api\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/restore/);
|
||||
|
||||
Reference in new issue
Block a user