feat(工作台): 细化计划进展工时证据
关键改动: - 支持需求覆盖和调研方向开始工作记录 - 日报按计划记录开始时间和进度下次开始时间计算证据 - 更新版本编辑校验、项目展示和 workflow 说明 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -85,8 +85,11 @@ export function getWorkspaceDailyReport({
|
||||
};
|
||||
});
|
||||
|
||||
const dailyActivities = activities
|
||||
.filter((activity) => activity.actorId === userId && activity.date === date)
|
||||
const userActivities = activities
|
||||
.filter((activity) => activity.actorId === userId)
|
||||
.sort((a, b) => a.occurredAt.localeCompare(b.occurredAt));
|
||||
const dailyActivities = userActivities
|
||||
.filter((activity) => activity.date === date)
|
||||
.sort((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
||||
const activityKeys = new Set(dailyActivities.map(getActivityKey));
|
||||
const evidenceSourceIds = new Set([
|
||||
@@ -115,13 +118,11 @@ export function getWorkspaceDailyReport({
|
||||
});
|
||||
}
|
||||
|
||||
const touchedSourceIds = new Set([
|
||||
...reportActivities.map((activity) => activity.sourceId),
|
||||
...items.map((item) => item.taskId),
|
||||
]);
|
||||
const touchedWorkItemIds = buildTouchedWorkItemIds(reportActivities, items, workItems);
|
||||
const worklogTaskIds = new Set(items.map((item) => item.taskId));
|
||||
const activityHours = calcActivityHoursForDate(
|
||||
Array.from(new Set(reportActivities.map((activity) => activity.sourceId))),
|
||||
reportActivities,
|
||||
userActivities,
|
||||
workItemMap,
|
||||
worklogTaskIds,
|
||||
date,
|
||||
@@ -129,7 +130,7 @@ export function getWorkspaceDailyReport({
|
||||
);
|
||||
|
||||
const needsProgressItems = workItems
|
||||
.filter((item) => shouldRequireProgress(item, date, touchedSourceIds))
|
||||
.filter((item) => shouldRequireProgress(item, date, touchedWorkItemIds))
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
@@ -171,9 +172,46 @@ function getRaw(item: WorkItem): Record<string, unknown> {
|
||||
return ((item.raw ?? {}) as unknown) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function shouldRequireProgress(item: WorkItem, date: string, touchedSourceIds: Set<string>): boolean {
|
||||
function buildTouchedWorkItemIds(
|
||||
activities: WorkActivity[],
|
||||
items: WorkspaceDailyReportItem[],
|
||||
workItems: WorkItem[],
|
||||
): Set<string> {
|
||||
const touched = new Set([
|
||||
...activities.map((activity) => activity.sourceId),
|
||||
...items.map((item) => item.taskId),
|
||||
]);
|
||||
const touchedRequirementIds = new Set(
|
||||
activities
|
||||
.map(getPlanProgressRequirementId)
|
||||
.filter((requirementId): requirementId is string => Boolean(requirementId)),
|
||||
);
|
||||
|
||||
if (touchedRequirementIds.size === 0) return touched;
|
||||
|
||||
for (const item of workItems) {
|
||||
const requirementId = getWorkItemRequirementId(item);
|
||||
if (requirementId && touchedRequirementIds.has(requirementId)) {
|
||||
touched.add(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
return touched;
|
||||
}
|
||||
|
||||
function getPlanProgressRequirementId(activity: WorkActivity): string | undefined {
|
||||
if (activity.sourceType !== 'version_plan' || activity.action !== 'version_plan_requirement_progress') return undefined;
|
||||
return asString(activity.metadata?.requirementId);
|
||||
}
|
||||
|
||||
function getWorkItemRequirementId(item: WorkItem): string | undefined {
|
||||
const raw = getRaw(item);
|
||||
return asString(raw.requirementId) ?? asString(item.extra?.requirementId);
|
||||
}
|
||||
|
||||
function shouldRequireProgress(item: WorkItem, date: string, touchedWorkItemIds: Set<string>): boolean {
|
||||
if (item.completed) return false;
|
||||
if (touchedSourceIds.has(item.id)) return false;
|
||||
if (touchedWorkItemIds.has(item.id)) return false;
|
||||
if (!isInProgressStatus(item.status)) return false;
|
||||
|
||||
const startedAt = getStartedAt(item);
|
||||
@@ -299,18 +337,108 @@ function getTestCaseTerminalFallback(
|
||||
}
|
||||
|
||||
function calcActivityHoursForDate(
|
||||
sourceIds: string[],
|
||||
activities: WorkActivity[],
|
||||
allUserActivities: WorkActivity[],
|
||||
workItemMap: Map<string, WorkItem>,
|
||||
excludedSourceIds: Set<string>,
|
||||
date: string,
|
||||
now: Date,
|
||||
): number {
|
||||
const intervals = sourceIds
|
||||
const recordScopedSourceIds = new Set<string>();
|
||||
const intervals = activities
|
||||
.filter((activity) => !excludedSourceIds.has(activity.sourceId))
|
||||
.map((activity) => {
|
||||
if (!isRecordScopedProgressActivity(activity)) return undefined;
|
||||
recordScopedSourceIds.add(activity.sourceId);
|
||||
const workStartedAt = getRecordScopedWorkStartedAt(activity, allUserActivities, workItemMap, date);
|
||||
if (!workStartedAt) return undefined;
|
||||
return getActivityIntervalForDate(workStartedAt, activity.occurredAt, date);
|
||||
})
|
||||
.filter(isTimeInterval);
|
||||
|
||||
const sourceIds = Array.from(new Set(activities.map((activity) => activity.sourceId)));
|
||||
const fallbackIntervals = sourceIds
|
||||
.filter((sourceId) => !excludedSourceIds.has(sourceId))
|
||||
.filter((sourceId) => !recordScopedSourceIds.has(sourceId))
|
||||
.map((sourceId) => getWorkItemIntervalForDate(workItemMap.get(sourceId), date, now))
|
||||
.filter(isTimeInterval);
|
||||
|
||||
return calcMergedIntervalHours(intervals);
|
||||
return calcMergedIntervalHours([...intervals, ...fallbackIntervals]);
|
||||
}
|
||||
|
||||
function isPlanRecordProgressActivity(activity: WorkActivity): boolean {
|
||||
return activity.sourceType === 'version_plan'
|
||||
&& (activity.action === 'version_plan_requirement_progress'
|
||||
|| activity.action === 'version_plan_research_direction_progress');
|
||||
}
|
||||
|
||||
function isProgressNoteRecordActivity(activity: WorkActivity): boolean {
|
||||
return activity.action === 'progress_note_added' && Boolean(asString(activity.metadata?.nextStartAt));
|
||||
}
|
||||
|
||||
function isRecordScopedProgressActivity(activity: WorkActivity): boolean {
|
||||
return isPlanRecordProgressActivity(activity) || isProgressNoteRecordActivity(activity);
|
||||
}
|
||||
|
||||
function getRecordScopedWorkStartedAt(
|
||||
activity: WorkActivity,
|
||||
allUserActivities: WorkActivity[],
|
||||
workItemMap: Map<string, WorkItem>,
|
||||
date: string,
|
||||
): string | undefined {
|
||||
if (isPlanRecordProgressActivity(activity)) {
|
||||
return asString(activity.metadata?.workStartedAt)
|
||||
?? getPreviousRecordNextStartAt(activity, allUserActivities)
|
||||
?? getSameDayWorkItemStartAt(workItemMap.get(activity.sourceId), date);
|
||||
}
|
||||
|
||||
if (isProgressNoteRecordActivity(activity)) {
|
||||
return getPreviousRecordNextStartAt(activity, allUserActivities)
|
||||
?? getSameDayWorkItemStartAt(workItemMap.get(activity.sourceId), date);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getPreviousRecordNextStartAt(activity: WorkActivity, allUserActivities: WorkActivity[]): string | undefined {
|
||||
const currentTime = new Date(activity.occurredAt).getTime();
|
||||
if (!Number.isFinite(currentTime)) return undefined;
|
||||
|
||||
return [...allUserActivities]
|
||||
.filter((item) => item.sourceId === activity.sourceId && item.occurredAt < activity.occurredAt)
|
||||
.sort((a, b) => b.occurredAt.localeCompare(a.occurredAt))
|
||||
.map((item) => asString(item.metadata?.nextStartAt))
|
||||
.find((nextStartAt) => {
|
||||
if (!nextStartAt) return false;
|
||||
const startTime = new Date(nextStartAt).getTime();
|
||||
return Number.isFinite(startTime) && startTime < currentTime;
|
||||
});
|
||||
}
|
||||
|
||||
function getSameDayWorkItemStartAt(item: WorkItem | undefined, date: string): string | undefined {
|
||||
if (!item) return undefined;
|
||||
const startAt = getStartedAt(item);
|
||||
if (!startAt || !isWithinLocalDate(startAt, date)) return undefined;
|
||||
return startAt;
|
||||
}
|
||||
|
||||
function getActivityIntervalForDate(
|
||||
startAt: string,
|
||||
occurredAt: string,
|
||||
date: string,
|
||||
): { start: number; end: number } | undefined {
|
||||
const day = getLocalDateBounds(date);
|
||||
if (!day) return undefined;
|
||||
|
||||
const start = new Date(startAt).getTime();
|
||||
const end = new Date(occurredAt).getTime();
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return undefined;
|
||||
|
||||
const overlapStart = Math.max(start, day.start.getTime());
|
||||
const overlapEnd = Math.min(end, day.end.getTime());
|
||||
if (overlapEnd <= overlapStart) return undefined;
|
||||
|
||||
return { start: overlapStart, end: overlapEnd };
|
||||
}
|
||||
|
||||
function getWorkItemIntervalForDate(item: WorkItem | undefined, date: string, now: Date): { start: number; end: number } | undefined {
|
||||
|
||||
Reference in New Issue
Block a user