feat(server): improve context management (#15448)

#### PR Dependency Tree


* **PR #15448** 👈

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**
* Added workspace artifact upload, browsing, removal, deduplication, and
library ownership support.
* Copilot now supports scoped document and artifact search, canvas
reading, live editor context, and frontend tools.
* Added scope and focus selectors with source-resolution receipts in
chat.
* Added embedding health, progress, synchronization, and retrieval
capabilities.
* Added BYOK policy visibility, provider restrictions, endpoint dialect
selection, and validation.
* Added delegated editor interactions and userdata document
authorization.

* **Bug Fixes**
* Improved attachment handling, cancellation, access control, retrieval
fallbacks, workspace synchronization, and configuration validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-08-10 09:27:58 +08:00
committed by GitHub
parent 42322d13fe
commit ee899a267b
311 changed files with 20468 additions and 14806 deletions
@@ -16,6 +16,12 @@ test('should create config', t => {
t.is(typeof config.auth.passwordRequirements.max, 'number');
t.is(typeof config.job.queue, 'object');
t.deepEqual(config.copilot.byok.allowedProviders, [
'openai',
'anthropic',
'gemini',
'fal',
]);
});
test('should override config', async t => {
@@ -89,6 +95,16 @@ test('should validate config', t => {
error.message,
'Invalid app config for module `auth` with key `passwordRequirements`. Minimum length of password must be less than maximum length.'
);
const [nativeError] = config.validate([
{
module: 'copilot',
key: 'byok.allowedProviders',
value: ['openai', 'openai'],
},
])!;
t.true(nativeError instanceof InvalidAppConfig);
t.regex(nativeError.message, /supported and unique/);
});
test('should override correctly', t => {
@@ -26,4 +26,9 @@ export class ConfigModule {
}
export { Config, ConfigFactory };
export { defineModuleConfig, type JSONSchema } from './register';
export {
defineModuleConfig,
defineNativeModuleConfig,
type JSONSchema,
type NativeAppConfigDescriptor,
} from './register';
@@ -8,22 +8,35 @@ import { z } from 'zod';
import { type EnvConfigType, parseEnvValue } from './env';
import { AppConfigByPath } from './types';
export type JSONSchema = { description?: string } & (
| { type?: undefined; oneOf?: JSONSchema[] }
| {
type: 'string' | 'number' | 'boolean';
enum?: string[];
}
| {
type: 'array';
items?: JSONSchema;
}
| {
type: 'object';
properties?: Record<string, JSONSchema>;
required?: string[];
}
);
export type JSONSchema = {
$id?: string;
$ref?: string;
$schema?: string;
additionalProperties?: boolean | JSONSchema;
allOf?: JSONSchema[];
anyOf?: JSONSchema[];
definitions?: Record<string, JSONSchema>;
description?: string;
default?: unknown;
enum?: unknown[];
format?: string;
items?: JSONSchema;
minItems?: number;
minLength?: number;
oneOf?: JSONSchema[];
pattern?: string;
properties?: Record<string, JSONSchema>;
required?: string[];
title?: string;
type?:
| 'string'
| 'number'
| 'boolean'
| 'array'
| 'object'
| 'null'
| Array<'string' | 'number' | 'boolean' | 'array' | 'object' | 'null'>;
};
type ConfigType = EnvConfigType | 'array' | 'object' | 'any';
export type ConfigDescriptor<T> = {
@@ -34,6 +47,7 @@ export type ConfigDescriptor<T> = {
default: T;
env?: [string, EnvConfigType];
link?: string;
internal?: boolean;
};
type ConfigDefineDescriptor<T> = {
@@ -44,6 +58,7 @@ type ConfigDefineDescriptor<T> = {
env?: string | [string, EnvConfigType];
link?: string;
schema?: JSONSchema;
internal?: boolean;
};
function typeFromShape(shape: z.ZodType<any>): ConfigType {
@@ -87,19 +102,17 @@ function shapeFromType(type: ConfigType): z.ZodType<any> {
}
function typeFromSchema(schema: JSONSchema): ConfigType {
if ('type' in schema) {
switch (schema.type) {
case 'string':
return 'string';
case 'number':
return 'float';
case 'boolean':
return 'boolean';
case 'array':
return 'array';
case 'object':
return 'object';
}
switch (schema.type) {
case 'string':
return 'string';
case 'number':
return 'float';
case 'boolean':
return 'boolean';
case 'array':
return 'array';
case 'object':
return 'object';
}
return 'any';
@@ -168,6 +181,7 @@ function standardizeDescriptor<T>(
},
env,
link: desc.link,
internal: desc.internal,
schema: {
type: schemaFromType(type),
description: desc.desc,
@@ -200,12 +214,20 @@ export const getDescriptors = once(() => {
export function defineModuleConfig<T extends keyof AppConfigSchema>(
module: T,
defs: ModuleConfigDescriptors<AppConfigByPath<T>>
) {
registerModuleConfig(
module,
defs as Record<string, ConfigDefineDescriptor<unknown>>
);
}
function registerModuleConfig(
module: string,
defs: Record<string, ConfigDefineDescriptor<unknown>>
) {
const descriptors: Record<string, ConfigDescriptor<any>> = {};
Object.entries(defs).forEach(([key, desc]) => {
descriptors[key] = standardizeDescriptor(
desc as ConfigDefineDescriptor<any>
);
descriptors[key] = standardizeDescriptor(desc);
});
APP_CONFIG_DESCRIPTORS[module] = {
@@ -214,7 +236,52 @@ export function defineModuleConfig<T extends keyof AppConfigSchema>(
};
}
const CONFIG_JSON_PATHS = [
export type NativeAppConfigDescriptor = {
key: string;
description: string;
defaultValue: unknown;
schema: JSONSchema;
internal: boolean;
};
export function defineNativeModuleConfig<T extends keyof AppConfigSchema>(
module: T,
descriptors: NativeAppConfigDescriptor[],
validate: (module: string, key: string, value: unknown) => string[],
nodeDefinitions: Partial<ModuleConfigDescriptors<AppConfigByPath<T>>> = {}
) {
registerModuleConfig(module, {
...nodeDefinitions,
...Object.fromEntries(
descriptors.map(descriptor => [
descriptor.key,
{
desc: descriptor.description,
default: descriptor.defaultValue,
schema: descriptor.schema,
internal: descriptor.internal,
validate: (value: unknown) => {
const errors = validate(module, descriptor.key, value);
return errors.length
? {
success: false as const,
error: new z.ZodError(
errors.map(message => ({
code: z.ZodIssueCode.custom,
message,
path: [],
}))
),
}
: { success: true as const, data: value };
},
},
])
),
} as Record<string, ConfigDefineDescriptor<unknown>>);
}
export const CONFIG_JSON_PATHS = [
join(env.projectRoot, 'config.json'),
`${homedir()}/.affine/config/config.json`,
];
+21 -37
View File
@@ -1,5 +1,4 @@
import { STATUS_CODES } from 'node:http';
import { escape } from 'node:querystring';
import { HttpStatus, Logger } from '@nestjs/common';
import { ClsServiceManager } from 'nestjs-cls';
@@ -791,35 +790,6 @@ export const USER_FRIENDLY_ERRORS = {
message: ({ provider, kind, message }) =>
`Provider ${provider} failed with ${kind} error: ${message || 'unknown'}`,
},
copilot_invalid_context: {
type: 'invalid_input',
args: { contextId: 'string' },
message: ({ contextId }) => `Invalid copilot context ${contextId}.`,
},
copilot_context_file_not_supported: {
type: 'bad_request',
args: { fileName: 'string', message: 'string' },
message: ({ fileName, message }) =>
`File ${fileName} is not supported to use as context: ${message}`,
},
copilot_failed_to_modify_context: {
type: 'internal_server_error',
args: { contextId: 'string', message: 'string' },
message: ({ contextId, message }) =>
`Failed to modify context ${contextId}: ${message}`,
},
copilot_failed_to_match_context: {
type: 'internal_server_error',
args: { contextId: 'string', content: 'string', message: 'string' },
message: ({ contextId, content, message }) =>
`Failed to match context ${contextId} with "${escape(content)}": ${message}`,
},
copilot_failed_to_match_global_context: {
type: 'internal_server_error',
args: { workspaceId: 'string', content: 'string', message: 'string' },
message: ({ workspaceId, content, message }) =>
`Failed to match context in workspace ${workspaceId} with "${escape(content)}": ${message}`,
},
copilot_embedding_disabled: {
type: 'action_forbidden',
message: `Embedding feature is disabled, please contact the administrator to enable it in the workspace settings.`,
@@ -828,6 +798,27 @@ export const USER_FRIENDLY_ERRORS = {
type: 'action_forbidden',
message: `Embedding feature not available, you may need to install pgvector extension to your database`,
},
copilot_selected_sources_processing: {
type: 'bad_request',
message: `Selected sources are still processing. Try again shortly.`,
},
copilot_selected_sources_failed: {
type: 'bad_request',
message: `Selected sources could not be processed. Remove the failed source or try again.`,
},
copilot_selected_sources_unavailable: {
type: 'action_forbidden',
message: `Selected sources are not available for AI retrieval.`,
},
copilot_selected_sources_limit_exceeded: {
type: 'invalid_input',
message: `Too many or too much content was selected. Select fewer sources and try again.`,
},
copilot_failed_to_add_workspace_artifact: {
type: 'internal_server_error',
args: { message: 'string' },
message: ({ message }) => `Failed to add workspace artifact: ${message}`,
},
copilot_transcription_job_exists: {
type: 'bad_request',
message: 'Transcription job already exists',
@@ -840,13 +831,6 @@ export const USER_FRIENDLY_ERRORS = {
type: 'bad_request',
message: `Audio not provided.`,
},
copilot_failed_to_add_workspace_file_embedding: {
type: 'internal_server_error',
args: { message: 'string' },
message: ({ message }) =>
`Failed to add workspace file embedding: ${message}`,
},
// Quota & Limit errors
blob_quota_exceeded: {
type: 'quota_exceeded',
@@ -868,62 +868,6 @@ export class CopilotProviderSideError extends UserFriendlyError {
super('internal_server_error', 'copilot_provider_side_error', message, args);
}
}
@ObjectType()
class CopilotInvalidContextDataType {
@Field() contextId!: string
}
export class CopilotInvalidContext extends UserFriendlyError {
constructor(args: CopilotInvalidContextDataType, message?: string | ((args: CopilotInvalidContextDataType) => string)) {
super('invalid_input', 'copilot_invalid_context', message, args);
}
}
@ObjectType()
class CopilotContextFileNotSupportedDataType {
@Field() fileName!: string
@Field() message!: string
}
export class CopilotContextFileNotSupported extends UserFriendlyError {
constructor(args: CopilotContextFileNotSupportedDataType, message?: string | ((args: CopilotContextFileNotSupportedDataType) => string)) {
super('bad_request', 'copilot_context_file_not_supported', message, args);
}
}
@ObjectType()
class CopilotFailedToModifyContextDataType {
@Field() contextId!: string
@Field() message!: string
}
export class CopilotFailedToModifyContext extends UserFriendlyError {
constructor(args: CopilotFailedToModifyContextDataType, message?: string | ((args: CopilotFailedToModifyContextDataType) => string)) {
super('internal_server_error', 'copilot_failed_to_modify_context', message, args);
}
}
@ObjectType()
class CopilotFailedToMatchContextDataType {
@Field() contextId!: string
@Field() content!: string
@Field() message!: string
}
export class CopilotFailedToMatchContext extends UserFriendlyError {
constructor(args: CopilotFailedToMatchContextDataType, message?: string | ((args: CopilotFailedToMatchContextDataType) => string)) {
super('internal_server_error', 'copilot_failed_to_match_context', message, args);
}
}
@ObjectType()
class CopilotFailedToMatchGlobalContextDataType {
@Field() workspaceId!: string
@Field() content!: string
@Field() message!: string
}
export class CopilotFailedToMatchGlobalContext extends UserFriendlyError {
constructor(args: CopilotFailedToMatchGlobalContextDataType, message?: string | ((args: CopilotFailedToMatchGlobalContextDataType) => string)) {
super('internal_server_error', 'copilot_failed_to_match_global_context', message, args);
}
}
export class CopilotEmbeddingDisabled extends UserFriendlyError {
constructor(message?: string) {
@@ -937,6 +881,40 @@ export class CopilotEmbeddingUnavailable extends UserFriendlyError {
}
}
export class CopilotSelectedSourcesProcessing extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'copilot_selected_sources_processing', message);
}
}
export class CopilotSelectedSourcesFailed extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'copilot_selected_sources_failed', message);
}
}
export class CopilotSelectedSourcesUnavailable extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'copilot_selected_sources_unavailable', message);
}
}
export class CopilotSelectedSourcesLimitExceeded extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'copilot_selected_sources_limit_exceeded', message);
}
}
@ObjectType()
class CopilotFailedToAddWorkspaceArtifactDataType {
@Field() message!: string
}
export class CopilotFailedToAddWorkspaceArtifact extends UserFriendlyError {
constructor(args: CopilotFailedToAddWorkspaceArtifactDataType, message?: string | ((args: CopilotFailedToAddWorkspaceArtifactDataType) => string)) {
super('internal_server_error', 'copilot_failed_to_add_workspace_artifact', message, args);
}
}
export class CopilotTranscriptionJobExists extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'copilot_transcription_job_exists', message);
@@ -954,16 +932,6 @@ export class CopilotTranscriptionAudioNotProvided extends UserFriendlyError {
super('bad_request', 'copilot_transcription_audio_not_provided', message);
}
}
@ObjectType()
class CopilotFailedToAddWorkspaceFileEmbeddingDataType {
@Field() message!: string
}
export class CopilotFailedToAddWorkspaceFileEmbedding extends UserFriendlyError {
constructor(args: CopilotFailedToAddWorkspaceFileEmbeddingDataType, message?: string | ((args: CopilotFailedToAddWorkspaceFileEmbeddingDataType) => string)) {
super('internal_server_error', 'copilot_failed_to_add_workspace_file_embedding', message, args);
}
}
export class BlobQuotaExceeded extends UserFriendlyError {
constructor(message?: string) {
@@ -1317,17 +1285,16 @@ export enum ErrorNames {
COPILOT_PROMPT_INVALID,
COPILOT_PROVIDER_NOT_SUPPORTED,
COPILOT_PROVIDER_SIDE_ERROR,
COPILOT_INVALID_CONTEXT,
COPILOT_CONTEXT_FILE_NOT_SUPPORTED,
COPILOT_FAILED_TO_MODIFY_CONTEXT,
COPILOT_FAILED_TO_MATCH_CONTEXT,
COPILOT_FAILED_TO_MATCH_GLOBAL_CONTEXT,
COPILOT_EMBEDDING_DISABLED,
COPILOT_EMBEDDING_UNAVAILABLE,
COPILOT_SELECTED_SOURCES_PROCESSING,
COPILOT_SELECTED_SOURCES_FAILED,
COPILOT_SELECTED_SOURCES_UNAVAILABLE,
COPILOT_SELECTED_SOURCES_LIMIT_EXCEEDED,
COPILOT_FAILED_TO_ADD_WORKSPACE_ARTIFACT,
COPILOT_TRANSCRIPTION_JOB_EXISTS,
COPILOT_TRANSCRIPTION_JOB_NOT_FOUND,
COPILOT_TRANSCRIPTION_AUDIO_NOT_PROVIDED,
COPILOT_FAILED_TO_ADD_WORKSPACE_FILE_EMBEDDING,
BLOB_QUOTA_EXCEEDED,
STORAGE_QUOTA_EXCEEDED,
MEMBER_QUOTA_EXCEEDED,
@@ -1368,5 +1335,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion',
types: () =>
[GraphqlBadRequestDataType, HttpRequestErrorDataType, SsrfBlockedErrorDataType, ResponseTooLargeErrorDataType, ImageFormatNotSupportedDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CalendarProviderRequestErrorDataType, NoCopilotProviderAvailableDataType, CopilotFailedToGenerateEmbeddingDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, UnsupportedServerVersionDataType, MentionUserDocAccessDeniedDataType, InvalidAppConfigDataType, InvalidAppConfigInputDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
[GraphqlBadRequestDataType, HttpRequestErrorDataType, SsrfBlockedErrorDataType, ResponseTooLargeErrorDataType, ImageFormatNotSupportedDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CalendarProviderRequestErrorDataType, NoCopilotProviderAvailableDataType, CopilotFailedToGenerateEmbeddingDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotFailedToAddWorkspaceArtifactDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, UnsupportedServerVersionDataType, MentionUserDocAccessDeniedDataType, InvalidAppConfigDataType, InvalidAppConfigInputDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
});
@@ -10,7 +10,9 @@ export {
Config,
ConfigFactory,
defineModuleConfig,
defineNativeModuleConfig,
type JSONSchema,
type NativeAppConfigDescriptor,
} from './config';
export * from './cors';
export * from './error';