feat(server): refactor copilot (#14892)

#### PR Dependency Tree


* **PR #14892** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
DarkSky
2026-05-04 00:36:47 +08:00
committed by GitHub
parent fa8f1a096c
commit d64f368623
239 changed files with 35859 additions and 16777 deletions
@@ -1,12 +1,16 @@
import { randomBytes } from 'node:crypto';
import serverNativeModule from '@affine/server-native';
import type { ProviderMiddlewareConfig } from '../../plugins/copilot/config';
import {
CopilotChatOptions,
CopilotEmbeddingOptions,
CopilotImageOptions,
type CopilotProviderModel,
CopilotProviderType,
CopilotStructuredOptions,
ModelConditions,
ModelInputType,
ModelFullConditions,
ModelOutputType,
PromptMessage,
StreamObject,
@@ -15,130 +19,534 @@ import {
DEFAULT_DIMENSIONS,
OpenAIProvider,
} from '../../plugins/copilot/providers/openai';
import type { ProviderModelRuntimeContext } from '../../plugins/copilot/providers/provider-model-runtime';
import {
type CopilotProviderExecution,
createNativeExecutionDriverSpec,
type ProviderDriverSpec,
} from '../../plugins/copilot/providers/provider-runtime-contract';
import type { ProviderRuntimeContexts } from '../../plugins/copilot/runtime/provider-runtime-context';
import { sleep } from '../utils/utils';
export class MockCopilotProvider extends OpenAIProvider {
override readonly models = [
{
id: 'test',
capabilities: [
{
input: [ModelInputType.Text],
output: [ModelOutputType.Text, ModelOutputType.Object],
defaultForOutputType: true,
},
],
},
{
id: 'test-image',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Image],
defaultForOutputType: true,
},
],
},
{
id: 'gpt-5',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Object],
},
],
},
{
id: 'gpt-5-2025-08-07',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Object],
},
],
},
{
id: 'gpt-5-mini',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
{
id: 'gpt-5-nano',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
{
id: 'gpt-image-1',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Image],
defaultForOutputType: true,
},
],
},
{
id: 'gemini-2.5-flash',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
{
id: 'gemini-2.5-pro',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
{
id: 'gemini-3.1-pro-preview',
capabilities: [
{
input: [
ModelInputType.Text,
ModelInputType.Image,
ModelInputType.Audio,
],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
];
const LLM_STREAM_END_MARKER = '__AFFINE_LLM_STREAM_END__';
const MOCK_NATIVE_TEXT = 'generate text to text';
const MOCK_NATIVE_STREAM_TEXT = 'generate text to text stream';
override async text(
function mockUsage() {
return {
prompt_tokens: 1,
completion_tokens: 1,
total_tokens: 2,
};
}
function buildMockDispatchResponse(model: string, text: string) {
return {
id: 'mock-dispatch',
model,
message: {
role: 'assistant',
content: [{ type: 'text', text }],
},
usage: mockUsage(),
finish_reason: 'stop',
};
}
function buildMockStructuredValue(schema: any, key?: string): any {
if (!schema || typeof schema !== 'object') {
return key === 'title' ? 'Weekly Sync' : MOCK_NATIVE_TEXT;
}
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
return buildMockStructuredValue(schema.anyOf[0], key);
}
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
return buildMockStructuredValue(schema.oneOf[0], key);
}
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
return schema.enum[0];
}
switch (schema.type) {
case 'object': {
const properties =
schema.properties && typeof schema.properties === 'object'
? schema.properties
: {};
return Object.fromEntries(
Object.entries(properties).map(([key, value]) => [
key,
buildMockStructuredValue(value, key),
])
);
}
case 'array':
return [buildMockStructuredValue(schema.items, key)];
case 'boolean':
return true;
case 'number':
case 'integer':
switch (key) {
case 'durationMinutes':
return 45;
case 's':
return 30;
case 'e':
return 53;
default:
return 1;
}
case 'null':
return null;
case 'string':
default:
switch (key) {
case 'title':
return 'Weekly Sync';
case 'description':
return 'Send recap';
case 'owner':
return 'A';
case 'deadline':
return 'Friday';
case 'speaker':
case 'a':
return 'A';
case 'attendees':
return 'A';
case 'start':
return '00:00:42';
case 'end':
return '00:01:05';
case 'text':
case 'transcription':
case 't':
return 'Hello, everyone.';
case 'keyPoints':
return 'Reviewed launch status';
case 'decisions':
return 'Ship on Monday';
case 'openQuestions':
return 'Need final QA sign-off';
case 'blockers':
return 'Waiting on analytics';
case 'summary':
return 'Reviewed launch status';
default:
return MOCK_NATIVE_TEXT;
}
}
}
function parseFirstRoute(routesJson: string) {
const routes = JSON.parse(routesJson) as Array<{
provider_id?: string;
model?: string;
request?: {
model?: string;
operation?: string;
prompt?: string;
schema?: unknown;
};
}>;
return routes[0];
}
function buildMockStructuredResponse(model: string, schema: unknown) {
const output_json = buildMockStructuredValue(schema);
return {
id: 'mock-structured-dispatch',
model,
output_text: JSON.stringify(output_json),
output_json,
usage: mockUsage(),
finish_reason: 'stop',
};
}
function emitMockTextStream(
model: string,
callback: (error: Error | null, eventJson: string) => void
) {
callback(null, JSON.stringify({ type: 'message_start', model }));
for (const text of MOCK_NATIVE_STREAM_TEXT) {
callback(null, JSON.stringify({ type: 'text_delta', text }));
}
callback(
null,
JSON.stringify({
type: 'done',
finish_reason: 'stop',
usage: mockUsage(),
})
);
callback(null, LLM_STREAM_END_MARKER);
}
export function installMockCopilotRuntime() {
const native = serverNativeModule as Record<string, any>;
const original = {
llmDispatchPrepared: native.llmDispatchPrepared,
llmDispatchPreparedStream: native.llmDispatchPreparedStream,
llmRenderBuiltInPrompt: native.llmRenderBuiltInPrompt,
llmRenderBuiltInSessionPrompt: native.llmRenderBuiltInSessionPrompt,
llmValidateJsonSchema: native.llmValidateJsonSchema,
llmStructuredDispatch: native.llmStructuredDispatch,
llmStructuredDispatchPrepared: native.llmStructuredDispatchPrepared,
llmEmbeddingDispatch: native.llmEmbeddingDispatch,
llmEmbeddingDispatchPrepared: native.llmEmbeddingDispatchPrepared,
llmRerankDispatch: native.llmRerankDispatch,
llmRerankDispatchPrepared: native.llmRerankDispatchPrepared,
llmImageDispatchPrepared: native.llmImageDispatchPrepared,
runNativeActionRecipePreparedStream:
native.runNativeActionRecipePreparedStream,
};
native.llmDispatchPrepared = (routesJson: string) => {
const route = parseFirstRoute(routesJson);
return JSON.stringify({
provider_id: route?.provider_id ?? 'mock-provider',
response: buildMockDispatchResponse(
route?.request?.model ?? route?.model ?? 'test',
MOCK_NATIVE_TEXT
),
});
};
native.llmDispatchPreparedStream = (
routesJson: string,
callback: (error: Error | null, eventJson: string) => void
) => {
const route = parseFirstRoute(routesJson);
emitMockTextStream(
route?.request?.model ?? route?.model ?? 'test',
callback
);
return { abort() {} };
};
native.llmStructuredDispatch = (
_protocol: string,
_backendConfigJson: string,
requestJson: string
) => {
const request = JSON.parse(requestJson) as {
model?: string;
schema?: unknown;
};
return JSON.stringify(
buildMockStructuredResponse(request.model ?? 'test', request.schema)
);
};
native.llmStructuredDispatchPrepared = (routesJson: string) => {
const route = parseFirstRoute(routesJson);
return JSON.stringify({
provider_id: route?.provider_id ?? 'mock-provider',
response: buildMockStructuredResponse(
route?.request?.model ?? route?.model ?? 'test',
route?.request?.schema
),
});
};
native.llmValidateJsonSchema = (_schema: unknown, value: unknown) => value;
native.llmEmbeddingDispatch = (
_protocol: string,
_backendConfigJson: string,
requestJson: string
) => {
const request = JSON.parse(requestJson) as {
model?: string;
dimensions?: number;
};
const length = request.dimensions ?? DEFAULT_DIMENSIONS;
return JSON.stringify({
model: request.model ?? 'test',
embeddings: [
Array.from({ length }, (_value, index) => (index % 128) + 1),
],
usage: { prompt_tokens: 1, total_tokens: 1 },
});
};
native.llmEmbeddingDispatchPrepared = (routesJson: string) => {
const route = parseFirstRoute(routesJson);
const response = JSON.parse(
native.llmEmbeddingDispatch(
'',
'',
JSON.stringify(route?.request ?? { model: route?.model ?? 'test' })
)
) as Record<string, unknown>;
return JSON.stringify({
provider_id: route?.provider_id ?? 'mock-provider',
response,
});
};
native.llmRerankDispatch = (
_protocol: string,
_backendConfigJson: string,
requestJson: string
) => {
const request = JSON.parse(requestJson) as {
model?: string;
candidates?: unknown[];
};
const candidateCount = request.candidates?.length ?? 0;
return JSON.stringify({
model: request.model ?? 'test',
scores: Array.from(
{ length: candidateCount },
(_value, index) => candidateCount - index
),
});
};
native.llmRerankDispatchPrepared = (routesJson: string) => {
const route = parseFirstRoute(routesJson);
const response = JSON.parse(
native.llmRerankDispatch(
'',
'',
JSON.stringify(route?.request ?? { model: route?.model ?? 'test' })
)
) as Record<string, unknown>;
return JSON.stringify({
provider_id: route?.provider_id ?? 'mock-provider',
response,
});
};
native.llmImageDispatchPrepared = (routesJson: string) => {
const route = parseFirstRoute(routesJson);
const model = route?.request?.model ?? route?.model ?? 'test-image';
const images = [
{
url: `https://example.com/${model}.jpg`,
media_type: 'image/jpeg',
},
];
if (route?.request?.operation === 'edit' && route.request.prompt) {
images.push({
url: `https://example.com/generated/${encodeURIComponent(route.request.prompt)}.jpg`,
media_type: 'image/jpeg',
});
}
return JSON.stringify({
provider_id: route?.provider_id ?? 'mock-provider',
response: {
images,
},
});
};
native.runNativeActionRecipePreparedStream = (
input: {
recipeId: string;
recipeVersion?: string;
input?: Record<string, any>;
},
callback: (error: Error | null, eventJson: string) => void
) => {
const version = input.recipeVersion ?? 'v1';
const result = input.recipeId.startsWith('image.filter.')
? {
url: `https://example.com/${input.recipeId}.jpg`,
}
: MOCK_NATIVE_STREAM_TEXT;
const attachmentEvent = input.recipeId.startsWith('image.filter.')
? [
{
type: 'attachment',
actionId: input.recipeId,
actionVersion: version,
status: 'running',
attachment: result,
},
]
: [];
const events = [
{
type: 'action_start',
actionId: input.recipeId,
actionVersion: version,
status: 'running',
},
{
type: 'step_start',
actionId: input.recipeId,
actionVersion: version,
stepId: 'generate',
status: 'running',
},
...attachmentEvent,
{
type: 'step_end',
actionId: input.recipeId,
actionVersion: version,
stepId: 'generate',
status: 'running',
},
{
type: 'action_done',
actionId: input.recipeId,
actionVersion: version,
status: 'succeeded',
result,
trace: {
actionId: input.recipeId,
actionVersion: version,
status: 'succeeded',
lightweight: [
{ type: 'action_start', status: 'running' },
{ type: 'action_trace', status: 'succeeded' },
],
},
},
];
for (const event of events) {
callback(null, JSON.stringify(event));
}
callback(null, LLM_STREAM_END_MARKER);
return { abort() {} };
};
return () => {
Object.assign(native, original);
};
}
export class MockCopilotProvider extends OpenAIProvider {
private runtimeHostOverride?: ProviderRuntimeContexts;
protected override resolveModelRuntimeContext(): ProviderModelRuntimeContext {
const providerType = this.type as CopilotProviderType;
return {
type: providerType,
backendKind:
providerType === CopilotProviderType.Gemini
? 'gemini_api'
: 'openai_responses',
};
}
override getDriverSpec(): ProviderDriverSpec {
const spec = super.getDriverSpec();
return {
...spec,
image: {
prepareMessages: async messages => messages,
},
};
}
private resolveMockModelId(
cond: Pick<ModelFullConditions, 'modelId' | 'outputType'>
) {
if (cond.modelId === 'test') {
return 'gpt-5-mini';
}
if (cond.modelId === 'test-image') {
return 'gpt-image-1';
}
return cond.modelId;
}
private normalizeMockConditions(
cond: ModelFullConditions
): ModelFullConditions {
const modelId = this.resolveMockModelId(cond);
return modelId === cond.modelId ? cond : { ...cond, modelId };
}
protected override createDriverSpec(spec: ProviderDriverSpec) {
return createNativeExecutionDriverSpec(spec, {
createBackendConfig: spec.createBackendConfig,
mapError: spec.mapError,
checkParams: input => this.checkParams(input),
selectModel: (cond, execution) => this.selectModel(cond, execution),
getTools: this.getTools.bind(this),
getActiveProviderMiddleware: this.getActiveProviderMiddleware.bind(this),
});
}
override async match(
cond: ModelFullConditions = {},
execution?: CopilotProviderExecution
) {
return await super.match(this.normalizeMockConditions(cond), execution);
}
override resolveModel(
modelId: string,
execution?: CopilotProviderExecution
): CopilotProviderModel | undefined {
const resolvedModelId = this.resolveMockModelId({ modelId });
return resolvedModelId
? super.resolveModel(resolvedModelId, execution)
: undefined;
}
override selectModel(
cond: ModelFullConditions,
execution?: CopilotProviderExecution
): CopilotProviderModel {
return super.selectModel(this.normalizeMockConditions(cond), execution);
}
override checkParams(input: Parameters<OpenAIProvider['checkParams']>[0]) {
return super.checkParams({
...input,
cond: this.normalizeMockConditions(input.cond),
});
}
override getActiveProviderMiddleware(): ProviderMiddlewareConfig {
return {};
}
overrideRuntimeHost(runtimeHost: ProviderRuntimeContexts) {
if (!this.runtimeHostOverride) {
const runtimeHostOverride: ProviderRuntimeContexts = {
...runtimeHost,
run: {
...runtimeHost.run,
text: this.text.bind(this),
streamText: this.streamTextRuntime.bind(this),
streamObject: this.streamObjectRuntime.bind(this),
structured: this.structure.bind(this),
embedding: this.embedding.bind(this),
},
};
this.runtimeHostOverride = runtimeHostOverride;
}
return this.runtimeHostOverride;
}
private async *streamTextRuntime(
cond: ModelConditions,
messages: PromptMessage[],
options?: CopilotChatOptions
): AsyncIterableIterator<string> {
yield* this.streamText(cond, messages, options);
}
private async *streamObjectRuntime(
cond: ModelConditions,
messages: PromptMessage[],
options?: CopilotChatOptions
): AsyncIterableIterator<StreamObject> {
yield* this.streamObject(cond, messages, options);
}
async text(
cond: ModelConditions,
messages: PromptMessage[],
options: CopilotChatOptions = {}
@@ -147,19 +555,27 @@ export class MockCopilotProvider extends OpenAIProvider {
...cond,
outputType: ModelOutputType.Text,
};
await this.checkParams({ messages, cond: fullCond, options });
await this.checkParams({
messages,
cond: fullCond,
options,
});
// make some time gap for history test case
await sleep(100);
return 'generate text to text';
}
override async *streamText(
async *streamText(
cond: ModelConditions,
messages: PromptMessage[],
options: CopilotChatOptions = {}
): AsyncIterable<string> {
const fullCond = { ...cond, outputType: ModelOutputType.Text };
await this.checkParams({ messages, cond: fullCond, options });
await this.checkParams({
messages,
cond: fullCond,
options,
});
// make some time gap for history test case
await sleep(100);
@@ -173,70 +589,58 @@ export class MockCopilotProvider extends OpenAIProvider {
}
}
override async structure(
async structure(
cond: ModelConditions,
messages: PromptMessage[],
options: CopilotStructuredOptions = {}
): Promise<string> {
const fullCond = { ...cond, outputType: ModelOutputType.Structured };
await this.checkParams({ messages, cond: fullCond, options });
await this.checkParams({
messages,
cond: fullCond,
options,
});
// make some time gap for history test case
await sleep(100);
return 'generate text to text';
}
override async *streamImages(
cond: ModelConditions,
messages: PromptMessage[],
options: CopilotImageOptions = {}
) {
const fullCond = { ...cond, outputType: ModelOutputType.Image };
await this.checkParams({ messages, cond: fullCond, options });
// make some time gap for history test case
await sleep(100);
const { content: prompt } = [...messages].pop() || {};
if (!prompt) throw new Error('Prompt is required');
const imageUrls = [
`https://example.com/${cond.modelId || 'test'}.jpg`,
prompt,
];
for (const imageUrl of imageUrls) {
yield imageUrl;
if (options.signal?.aborted) {
break;
}
}
return;
}
// ====== text to embedding ======
override async embedding(
async embedding(
cond: ModelConditions,
messages: string | string[],
options: CopilotEmbeddingOptions = { dimensions: DEFAULT_DIMENSIONS }
): Promise<number[][]> {
messages = Array.isArray(messages) ? messages : [messages];
const fullCond = { ...cond, outputType: ModelOutputType.Embedding };
await this.checkParams({ embeddings: messages, cond: fullCond, options });
await this.checkParams({
embeddings: messages,
cond: fullCond,
options,
});
// make some time gap for history test case
await sleep(100);
return [Array.from(randomBytes(options.dimensions)).map(v => v % 128)];
return [
Array.from(randomBytes(options.dimensions ?? DEFAULT_DIMENSIONS)).map(
v => v % 128
),
];
}
override async *streamObject(
async *streamObject(
cond: ModelConditions,
messages: PromptMessage[],
options: CopilotChatOptions = {}
): AsyncIterable<StreamObject> {
const fullCond = { ...cond, outputType: ModelOutputType.Object };
await this.checkParams({ messages, cond: fullCond, options });
await this.checkParams({
messages,
cond: fullCond,
options,
});
// make some time gap for history test case
await sleep(100);
@@ -1,11 +1,12 @@
export { createFactory } from './factory';
export * from './prompt-service.mock';
export * from './team-workspace.mock';
export * from './user.mock';
export * from './workspace.mock';
export * from './workspace-user.mock';
import { MockAccessToken } from './access-token.mock';
import { MockCopilotProvider } from './copilot.mock';
import { installMockCopilotRuntime, MockCopilotProvider } from './copilot.mock';
import { MockDocMeta } from './doc-meta.mock';
import { MockDocSnapshot } from './doc-snapshot.mock';
import { MockDocUser } from './doc-user.mock';
@@ -30,4 +31,10 @@ export const Mockers = {
AccessToken: MockAccessToken,
};
export { MockCopilotProvider, MockEventBus, MockJobQueue, MockMailer };
export {
installMockCopilotRuntime,
MockCopilotProvider,
MockEventBus,
MockJobQueue,
MockMailer,
};
@@ -0,0 +1,110 @@
import { Injectable } from '@nestjs/common';
import { CopilotPromptInvalid } from '../../base';
import { llmGetBuiltInPromptSpec, llmRenderBuiltInPrompt } from '../../native';
import { PromptService } from '../../plugins/copilot/prompt';
import type { Prompt } from '../../plugins/copilot/prompt/spec';
import type {
PromptConfig,
PromptMessage,
} from '../../plugins/copilot/providers/types';
@Injectable()
export class TestingPromptService extends PromptService {
private readonly customPrompts = new Map<string, Prompt>();
private readonly builtInPromptOverrides = new Map<string, Prompt>();
reset() {
this.customPrompts.clear();
this.builtInPromptOverrides.clear();
}
async set(
name: string,
model: string,
messages: PromptMessage[],
config?: PromptConfig | null,
extraConfig?: { optionalModels: string[] }
) {
this.assertCustomPromptName(name);
const existing = this.customPrompts.get(name);
this.customPrompts.set(name, {
name,
model,
action: existing?.action,
optionalModels: existing?.optionalModels?.length
? [...existing.optionalModels, ...(extraConfig?.optionalModels ?? [])]
: extraConfig?.optionalModels,
config: config ? structuredClone(config) : undefined,
messages: this.cloneMessages(messages),
});
}
async overrideBuiltIn(
name: string,
data: {
messages?: PromptMessage[];
model?: string;
config?: PromptConfig | null;
}
) {
const current = this.loadBuiltInPrompt(name);
if (!current) {
throw new CopilotPromptInvalid(
`Built-in prompt ${name} not found in native catalog`
);
}
const { config, messages, model } = data;
const next = this.clonePrompt(current);
if (model !== undefined) {
next.model = model;
}
if (config === null) {
next.config = undefined;
} else if (config !== undefined) {
next.config = structuredClone(config);
}
if (messages) {
next.messages = this.cloneMessages(messages);
}
this.builtInPromptOverrides.set(name, next);
}
protected override lookupCompatPrompt(name: string) {
return (
this.builtInPromptOverrides.get(name) ??
this.customPrompts.get(name) ??
null
);
}
private assertCustomPromptName(name: string) {
if (this.loadBuiltInPrompt(name)) {
throw new CopilotPromptInvalid(
`Built-in prompt ${name} is owned by native catalog`
);
}
}
private loadBuiltInPrompt(name: string): Prompt | null {
const spec = llmGetBuiltInPromptSpec(name);
if (!spec) return null;
const prompt = llmRenderBuiltInPrompt({ name, renderParams: {} });
return {
name: spec.name,
action: spec.action,
model: spec.model,
optionalModels: spec.optionalModels,
config: spec.config,
messages: prompt.messages.map(message => ({
role: message.role,
content: message.content,
...(message.params ? { params: message.params } : {}),
})),
};
}
}