feat(server): realtime notification & task status (#14934)

#### PR Dependency Tree


* **PR #14934** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Full realtime platform added: live notifications, comments, embedding
progress, and transcription task updates via realtime subscriptions.

* **Chores**
* Frontend switched from polling/GraphQL queries to realtime channels;
legacy query fields marked deprecated and client libs updated to use
realtime APIs.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/14934)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #14934** 👈
  * **PR #14936**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
DarkSky
2026-05-10 23:21:50 +08:00
committed by GitHub
parent 417d31cabe
commit 8cf00738c2
70 changed files with 2378 additions and 283 deletions
@@ -9,6 +9,7 @@ import {
OnJob,
sniffMime,
} from '../../../base';
import type { RealtimePublisher } from '../../../core/realtime';
import { Models } from '../../../models';
import { CopilotAccessPolicy } from '../access';
import { PromptService } from '../prompt';
@@ -32,6 +33,10 @@ const TRANSCRIPT_ACTION_ID = 'transcript.audio.gemini';
const TRANSCRIPT_ACTION_VERSION = 'v1';
const TRANSCRIPT_STRATEGY = 'gemini';
export function transcriptTaskRoom(workspaceId: string, taskId: string) {
return `copilot:transcript:${workspaceId}:${taskId}`;
}
export type TranscriptionJob = {
id: string;
status: AiJobStatus;
@@ -62,7 +67,8 @@ export class CopilotTranscriptionService {
private readonly tasks: TaskPolicy,
private readonly prompts: PromptService,
private readonly actionBridge: ActionRuntimeBridge,
@Optional() private readonly access?: CopilotAccessPolicy
@Optional() private readonly access?: CopilotAccessPolicy,
@Optional() private readonly realtime?: RealtimePublisher
) {}
private parseTaskPayload(payload: unknown): TranscriptionPayloadV2 {
@@ -252,6 +258,7 @@ export class CopilotTranscriptionService {
modelId: model,
});
await this.models.copilotTranscriptTask.markRunning(task.id);
this.publishTaskChanged(workspaceId, task.id, AiJobStatus.running);
return { id: task.id, status: AiJobStatus.running, infos };
}
@@ -294,6 +301,7 @@ export class CopilotTranscriptionService {
retryOf: task.actionRunId ?? undefined,
});
await this.models.copilotTranscriptTask.markRunning(taskId);
this.publishTaskChanged(workspaceId, taskId, AiJobStatus.running);
return {
id: taskId,
status: AiJobStatus.running,
@@ -389,6 +397,11 @@ export class CopilotTranscriptionService {
},
onRunCreated: async ({ runId }) => {
await this.models.copilotTranscriptTask.markRunning(taskId, runId);
this.publishTaskChanged(
task.workspaceId,
taskId,
AiJobStatus.running
);
},
prepareStructuredRoutes: {
stepId: 'transcribe',
@@ -425,6 +438,7 @@ export class CopilotTranscriptionService {
protectedResult: parsedResult,
errorCode: null,
});
this.publishTaskChanged(task.workspaceId, taskId, AiJobStatus.finished);
} catch (error) {
await this.models.copilotTranscriptTask.complete(taskId, {
status: 'failed',
@@ -434,7 +448,27 @@ export class CopilotTranscriptionService {
errorCode:
error instanceof Error ? error.message : 'transcript_task_failed',
});
this.publishTaskChanged(
task.workspaceId,
taskId,
AiJobStatus.failed,
error instanceof Error ? error.message : 'transcript_task_failed'
);
throw error;
}
}
private publishTaskChanged(
workspaceId: string,
taskId: string,
status: AiJobStatus,
error?: string
) {
this.realtime?.publish(
'copilot.transcript.task.changed',
{ workspaceId, taskId },
{ taskId, status, error },
{ room: transcriptTaskRoom(workspaceId, taskId) }
);
}
}