feat: refactor copilot module (#14537)

This commit is contained in:
DarkSky
2026-03-02 13:57:55 +08:00
committed by GitHub
parent 60acd81d4b
commit c5d622531c
92 changed files with 5759 additions and 2170 deletions
@@ -411,6 +411,9 @@ declare global {
interface AISessionService {
createSession: (options: AICreateSessionOptions) => Promise<string>;
createSessionWithHistory: (
options: AICreateSessionOptions
) => Promise<CopilotChatHistoryFragment | undefined>;
getSession: (
workspaceId: string,
sessionId: string
@@ -185,6 +185,9 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
@state()
private accessor hasMore = true;
@state()
private accessor selectedSessionId: string | undefined;
private accessor currentOffset = 0;
private readonly pageSize = 10;
@@ -267,9 +270,16 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
override connectedCallback() {
super.connectedCallback();
this.selectedSessionId = this.session?.sessionId ?? undefined;
this.getRecentSessions().catch(console.error);
}
protected override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('session')) {
this.selectedSessionId = this.session?.sessionId ?? undefined;
}
}
override firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this.disposables.add(() => {
@@ -294,9 +304,10 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
class="ai-session-item"
@click=${(e: MouseEvent) => {
e.stopPropagation();
this.selectedSessionId = session.sessionId;
this.onSessionClick(session.sessionId);
}}
aria-selected=${this.session?.sessionId === session.sessionId}
aria-selected=${this.selectedSessionId === session.sessionId}
data-session-id=${session.sessionId}
>
<div class="ai-session-title">
@@ -332,6 +343,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
class="ai-session-doc"
@click=${(e: MouseEvent) => {
e.stopPropagation();
this.selectedSessionId = sessionId;
this.onDocClick(docId, sessionId);
}}
>
@@ -152,6 +152,7 @@ export class AIProvider {
}>(),
// downstream can emit this slot to notify ai presets that user info has been updated
userInfo: new Subject<AIUserInfo | null>(),
sessionReady: new BehaviorSubject<boolean>(false),
previewPanelOpenChange: new Subject<boolean>(),
/* eslint-enable rxjs/finnish */
};
@@ -344,6 +345,7 @@ export class AIProvider {
} else if (id === 'session') {
AIProvider.instance.session =
action as BlockSuitePresets.AISessionService;
AIProvider.instance.slots.sessionReady.next(true);
} else if (id === 'context') {
AIProvider.instance.context =
action as BlockSuitePresets.AIContextService;
@@ -11,6 +11,7 @@ import {
createCopilotContextMutation,
createCopilotMessageMutation,
createCopilotSessionMutation,
createCopilotSessionWithHistoryMutation,
forkCopilotSessionMutation,
getCopilotHistoriesQuery,
getCopilotHistoryIdsQuery,
@@ -41,7 +42,6 @@ import {
} from './error';
export enum Endpoint {
Stream = 'stream',
StreamObject = 'stream-object',
Workflow = 'workflow',
Images = 'images',
@@ -96,7 +96,6 @@ export class CopilotClient {
readonly gql: <Query extends GraphQLQuery>(
options: QueryOptions<Query>
) => Promise<QueryResponse<Query>>,
readonly fetcher: (input: string, init?: RequestInit) => Promise<Response>,
readonly eventSource: (
url: string,
eventSourceInitDict?: EventSourceInit
@@ -119,6 +118,20 @@ export class CopilotClient {
}
}
async createSessionWithHistory(
options: OptionsField<typeof createCopilotSessionWithHistoryMutation>
) {
try {
const res = await this.gql({
query: createCopilotSessionWithHistoryMutation,
variables: { options },
});
return res.createCopilotSessionWithHistory;
} catch (err) {
throw resolveError(err);
}
}
async updateSession(
options: OptionsField<typeof updateCopilotSessionMutation>
) {
@@ -150,7 +163,11 @@ export class CopilotClient {
}
async createMessage(
options: OptionsField<typeof createCopilotMessageMutation>
options: OptionsField<typeof createCopilotMessageMutation>,
requestOptions?: Pick<
RequestOptions<typeof createCopilotMessageMutation>,
'timeout' | 'signal'
>
) {
try {
const res = await this.gql({
@@ -158,6 +175,8 @@ export class CopilotClient {
variables: {
options,
},
timeout: requestOptions?.timeout,
signal: requestOptions?.signal,
});
return res.createCopilotMessage;
} catch (err) {
@@ -442,35 +461,6 @@ export class CopilotClient {
return { files, docs };
}
async chatText({
sessionId,
messageId,
reasoning,
modelId,
toolsConfig,
signal,
}: {
sessionId: string;
messageId?: string;
reasoning?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
signal?: AbortSignal;
}) {
let url = `/api/copilot/chat/${sessionId}`;
const queryString = this.paramsToQueryString({
messageId,
reasoning,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
}
const response = await this.fetcher(url.toString(), { signal });
return response.text();
}
// Text or image to text
chatTextStream(
{
@@ -486,7 +476,7 @@ export class CopilotClient {
modelId?: string;
toolsConfig?: AIToolsConfig;
},
endpoint = Endpoint.Stream
endpoint = Endpoint.StreamObject
) {
let url = `/api/copilot/chat/${sessionId}/${endpoint}`;
const queryString = this.paramsToQueryString({
@@ -3,7 +3,7 @@ import { partition } from 'lodash-es';
import { AIProvider } from './ai-provider';
import { type CopilotClient, Endpoint } from './copilot-client';
import { delay, toTextStream } from './event-source';
import { toTextStream } from './event-source';
const TIMEOUT = 50000;
@@ -67,6 +67,8 @@ interface CreateMessageOptions {
content?: string;
attachments?: (string | Blob | File)[];
params?: Record<string, any>;
timeout?: number;
signal?: AbortSignal;
}
async function createMessage({
@@ -75,6 +77,8 @@ async function createMessage({
content,
attachments,
params,
timeout,
signal,
}: CreateMessageOptions): Promise<string> {
const hasAttachments = attachments && attachments.length > 0;
const options: Parameters<CopilotClient['createMessage']>[0] = {
@@ -102,7 +106,7 @@ async function createMessage({
).filter(Boolean) as File[];
}
return await client.createMessage(options);
return await client.createMessage(options, { timeout, signal });
}
export function textToText({
@@ -115,7 +119,7 @@ export function textToText({
signal,
timeout = TIMEOUT,
retry = false,
endpoint = Endpoint.Stream,
endpoint = Endpoint.StreamObject,
postfix,
reasoning,
modelId,
@@ -133,6 +137,8 @@ export function textToText({
content,
attachments,
params,
timeout,
signal,
});
}
const eventSource = client.chatTextStream(
@@ -147,65 +153,105 @@ export function textToText({
);
AIProvider.LAST_ACTION_SESSIONID = sessionId;
if (signal) {
if (signal.aborted) {
eventSource.close();
return;
let onAbort: (() => void) | undefined;
try {
if (signal) {
if (signal.aborted) {
eventSource.close();
return;
}
onAbort = () => {
eventSource.close();
};
signal.addEventListener('abort', onAbort, { once: true });
}
signal.onabort = () => {
eventSource.close();
};
}
if (postfix) {
const messages: string[] = [];
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
messages.push(event.data);
if (postfix) {
const messages: string[] = [];
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
messages.push(event.data);
}
}
yield postfix(messages.join(''));
} else {
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
yield event.data;
}
}
}
yield postfix(messages.join(''));
} else {
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
yield event.data;
}
} finally {
eventSource.close();
if (signal && onAbort) {
signal.removeEventListener('abort', onAbort);
}
}
},
};
} else {
return Promise.race([
timeout
? delay(timeout).then(() => {
throw new Error('Timeout');
})
: null,
(async function () {
if (!retry) {
messageId = await createMessage({
client,
sessionId,
content,
attachments,
params,
});
}
AIProvider.LAST_ACTION_SESSIONID = sessionId;
return client.chatText({
return (async function () {
if (!retry) {
messageId = await createMessage({
client,
sessionId,
content,
attachments,
params,
timeout,
signal,
});
}
const eventSource = client.chatTextStream(
{
sessionId,
messageId,
reasoning,
modelId,
});
})(),
]);
toolsConfig,
},
endpoint
);
AIProvider.LAST_ACTION_SESSIONID = sessionId;
let onAbort: (() => void) | undefined;
try {
if (signal) {
if (signal.aborted) {
eventSource.close();
return '';
}
onAbort = () => {
eventSource.close();
};
signal.addEventListener('abort', onAbort, { once: true });
}
const messages: string[] = [];
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
messages.push(event.data);
}
}
const result = messages.join('');
return postfix ? postfix(result) : result;
} finally {
eventSource.close();
if (signal && onAbort) {
signal.removeEventListener('abort', onAbort);
}
}
})();
}
}
@@ -232,6 +278,8 @@ export function toImage({
content,
attachments,
params,
timeout,
signal,
});
}
const eventSource = client.imagesStream(
@@ -582,6 +582,21 @@ Could you make a new website based on these notes and send back just the html fi
AIProvider.provide('session', {
createSession,
createSessionWithHistory: async options => {
if (!options.sessionId && !options.retry) {
return client.createSessionWithHistory({
workspaceId: options.workspaceId,
docId: options.docId,
promptName: options.promptName,
pinned: options.pinned,
reuseLatestChat: options.reuseLatestChat,
});
}
const sessionId = await createSession(options);
if (!sessionId) return undefined;
return client.getSession(options.workspaceId, sessionId);
},
getSession: async (workspaceId: string, sessionId: string) => {
return client.getSession(workspaceId, sessionId);
},
@@ -823,7 +838,7 @@ Could you make a new website based on these notes and send back just the html fi
regular: string;
};
}[];
} = await client.fetcher(url.toString()).then(res => res.json());
} = await fetch(url.toString()).then((res: Response) => res.json());
if (!result.results) return [];
return result.results.map(r => {
const url = new URL(r.urls.regular);