mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-01 06:10:16 +08:00
fix(server): index & gc queue & llm compatibility (#15528)
fix #15523 fix #15526 #### PR Dependency Tree * **PR #15528** 👈 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 - **Bug Fixes** - Improved search generation cleanup, provider error reporting, and reconciliation reliability. - Retired search resources are cleaned up safely, including after credential changes. - Prompt size checks now ignore tool parameters and provide clearer errors. - Reserved documents are protected from accidental cleanup, and malformed identifiers are rejected. - **Configuration** - Managed Copilot profiles require explicit, non-duplicated model assignments. - Improved managed provider profile migration. - **Performance & Reliability** - Reduced unnecessary search-history cleanup and adjusted consistency-check intervals. - Failed reconciliation jobs stop after one attempt and are removed automatically. - **Data Updates** - Updated legacy AI session prompt names to current labels. - Improved cloud load-balancer health-check configuration. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
UPDATE ai_sessions_metadata
|
||||
SET prompt_name = CASE prompt_name
|
||||
WHEN 'Search With AFFiNE AI' THEN 'Chat With AFFiNE AI'
|
||||
WHEN 'debug:action:dalle3' THEN 'Generate image'
|
||||
WHEN 'debug:action:fal-sd15' THEN 'Generate image'
|
||||
WHEN 'debug:action:gpt-image-1' THEN 'Generate image'
|
||||
WHEN 'debug:action:fal-remove-bg' THEN 'Remove background'
|
||||
WHEN 'debug:action:fal-upscaler' THEN 'Upscale image'
|
||||
WHEN 'debug:action:fal-face-to-sticker' THEN 'Convert to sticker'
|
||||
ELSE prompt_name
|
||||
END
|
||||
WHERE prompt_name IN (
|
||||
'Search With AFFiNE AI',
|
||||
'debug:action:dalle3',
|
||||
'debug:action:fal-sd15',
|
||||
'debug:action:gpt-image-1',
|
||||
'debug:action:fal-remove-bg',
|
||||
'debug:action:fal-upscaler',
|
||||
'debug:action:fal-face-to-sticker'
|
||||
);
|
||||
@@ -1444,6 +1444,17 @@ test('workspace sync push-doc-update should enforce doc update permissions', asy
|
||||
);
|
||||
t.is(userdataError.name, 'SPACE_ACCESS_DENIED');
|
||||
|
||||
const malformedDatabaseError = getErrorResponse(
|
||||
t,
|
||||
await emitWithAck(socket, 'space:push-doc-update', {
|
||||
spaceType: 'workspace',
|
||||
spaceId: workspace.id,
|
||||
docId: 'db$docProperties',
|
||||
update: createYjsUpdateBase64(),
|
||||
})
|
||||
);
|
||||
t.is(malformedDatabaseError.name, 'SPACE_ACCESS_DENIED');
|
||||
|
||||
const updates = await db.update.count({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -277,7 +277,11 @@ export class BackendRuntimeSearchJob {
|
||||
await this.queue.add(
|
||||
'backendRuntime.reconcileSearchProjection',
|
||||
{ limit: 100 },
|
||||
{ jobId: 'backend-runtime-search-reconciliation', removeOnFail: true }
|
||||
{
|
||||
jobId: 'backend-runtime-search-reconciliation',
|
||||
attempts: 1,
|
||||
removeOnFail: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
SpaceAccessDenied,
|
||||
} from '../../base';
|
||||
import { Models } from '../../models';
|
||||
import { authorizeUserdataDocSubject } from '../../native';
|
||||
import { authorizeReservedDocSubject } from '../../native';
|
||||
import { CurrentUser } from '../auth';
|
||||
import {
|
||||
DocReader,
|
||||
@@ -469,7 +469,7 @@ export class SpaceSyncGateway
|
||||
await this.ac.user(userId).doc(spaceId, docId).assert(action);
|
||||
}
|
||||
|
||||
private assertUserdataSubject(
|
||||
private assertReservedDocSubject(
|
||||
spaceType: SpaceType,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
@@ -477,7 +477,7 @@ export class SpaceSyncGateway
|
||||
) {
|
||||
if (
|
||||
spaceType === SpaceType.Workspace &&
|
||||
!authorizeUserdataDocSubject(userId, workspaceId, docId)
|
||||
!authorizeReservedDocSubject(userId, workspaceId, docId)
|
||||
) {
|
||||
throw new SpaceAccessDenied({ spaceId: workspaceId });
|
||||
}
|
||||
@@ -928,7 +928,12 @@ export class SpaceSyncGateway
|
||||
}
|
||||
|
||||
try {
|
||||
this.assertUserdataSubject(event.spaceType, userId, spaceId, docId);
|
||||
this.assertReservedDocSubject(
|
||||
event.spaceType,
|
||||
userId,
|
||||
spaceId,
|
||||
docId
|
||||
);
|
||||
await this.assertDocActionAllowed(
|
||||
event.spaceType,
|
||||
userId,
|
||||
@@ -1095,7 +1100,7 @@ export class SpaceSyncGateway
|
||||
if (space.docId === undefined) {
|
||||
continue;
|
||||
}
|
||||
this.assertUserdataSubject(
|
||||
this.assertReservedDocSubject(
|
||||
space.spaceType,
|
||||
user.id,
|
||||
space.spaceId,
|
||||
@@ -1212,7 +1217,7 @@ export class SpaceSyncGateway
|
||||
const id = new DocID(docId, spaceId);
|
||||
const adapter = this.selectAdapter(client, spaceType);
|
||||
adapter.assertIn(spaceId);
|
||||
this.assertUserdataSubject(spaceType, user.id, spaceId, id.guid);
|
||||
this.assertReservedDocSubject(spaceType, user.id, spaceId, id.guid);
|
||||
await this.assertDocActionAllowed(
|
||||
spaceType,
|
||||
user.id,
|
||||
@@ -1247,7 +1252,7 @@ export class SpaceSyncGateway
|
||||
@MessageBody() { spaceType, spaceId, docId }: DeleteDocMessage
|
||||
): Promise<EventResponse<{ success: true }>> {
|
||||
const adapter = this.selectAdapter(client, spaceType);
|
||||
this.assertUserdataSubject(spaceType, user.id, spaceId, docId);
|
||||
this.assertReservedDocSubject(spaceType, user.id, spaceId, docId);
|
||||
await this.assertDocActionAllowed(
|
||||
spaceType,
|
||||
user.id,
|
||||
@@ -1273,7 +1278,7 @@ export class SpaceSyncGateway
|
||||
const adapter = this.selectAdapter(client, spaceType);
|
||||
|
||||
// Quota recovery mode is intentionally not applied to sync.
|
||||
this.assertUserdataSubject(spaceType, user.id, spaceId, docId);
|
||||
this.assertReservedDocSubject(spaceType, user.id, spaceId, docId);
|
||||
await this.assertDocActionAllowed(
|
||||
spaceType,
|
||||
user.id,
|
||||
|
||||
@@ -137,9 +137,10 @@ test('managed provider migration preserves explicit profiles and converts legacy
|
||||
});
|
||||
const profiles = [
|
||||
{
|
||||
id: 'openai-default',
|
||||
type: 'openai',
|
||||
id: 'cloudflare-existing',
|
||||
type: 'cloudflareWorkersAi',
|
||||
priority: 7,
|
||||
models: ['@cf/baai/bge-reranker-base'],
|
||||
config: { apiKey: 'profile-key' },
|
||||
},
|
||||
];
|
||||
@@ -148,12 +149,16 @@ test('managed provider migration preserves explicit profiles and converts legacy
|
||||
{ id: 'copilot.providers.profiles', value: profiles },
|
||||
{
|
||||
id: 'copilot.providers.openai',
|
||||
value: { apiKey: 'shadowed-legacy-key' },
|
||||
value: { apiKey: 'openai-key' },
|
||||
},
|
||||
{
|
||||
id: 'copilot.providers.gemini',
|
||||
value: { apiKey: 'gemini-key' },
|
||||
},
|
||||
{
|
||||
id: 'copilot.providers.geminiVertex',
|
||||
value: { projectId: 'gemini-vertex-project' },
|
||||
},
|
||||
{
|
||||
id: 'copilot.providers.defaults',
|
||||
value: { fallback: 'openai-default' },
|
||||
@@ -169,18 +174,38 @@ test('managed provider migration preserves explicit profiles and converts legacy
|
||||
});
|
||||
t.deepEqual(migrated.value, [
|
||||
...profiles,
|
||||
{
|
||||
id: 'openai-default',
|
||||
type: 'openai',
|
||||
priority: 7,
|
||||
models: ['gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-image-1', 'gpt-4o-mini'],
|
||||
config: { apiKey: 'openai-key' },
|
||||
},
|
||||
{
|
||||
id: 'gemini-default',
|
||||
type: 'gemini',
|
||||
priority: 4,
|
||||
models: ['gemini-3.7-flash', 'gemini-embedding-001'],
|
||||
config: { apiKey: 'gemini-key' },
|
||||
},
|
||||
{
|
||||
id: 'geminiVertex-default',
|
||||
type: 'geminiVertex',
|
||||
priority: 3,
|
||||
models: ['gemini-3.7-flash'],
|
||||
config: { projectId: 'gemini-vertex-project' },
|
||||
enabled: false,
|
||||
},
|
||||
]);
|
||||
t.is(
|
||||
await t.context.db.appConfig.count({
|
||||
where: {
|
||||
id: {
|
||||
in: ['copilot.providers.openai', 'copilot.providers.gemini'],
|
||||
in: [
|
||||
'copilot.providers.openai',
|
||||
'copilot.providers.gemini',
|
||||
'copilot.providers.geminiVertex',
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
+28
-3
@@ -13,6 +13,15 @@ const PROVIDERS = [
|
||||
] as const;
|
||||
|
||||
const PROVIDER_IDS = PROVIDERS.map(provider => `copilot.providers.${provider}`);
|
||||
const PROVIDER_MODELS: Record<(typeof PROVIDERS)[number], string[]> = {
|
||||
openai: ['gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-image-1', 'gpt-4o-mini'],
|
||||
cloudflareWorkersAi: ['@cf/baai/bge-reranker-base'],
|
||||
fal: ['lora/image-to-image', 'workflowutils/teed'],
|
||||
gemini: ['gemini-3.7-flash', 'gemini-embedding-001'],
|
||||
geminiVertex: ['gemini-3.7-flash'],
|
||||
anthropic: ['claude-sonnet-4-6'],
|
||||
anthropicVertex: ['claude-sonnet-4-6'],
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
@@ -58,23 +67,39 @@ export class ConvergeManagedProviderProfiles1786810000000 {
|
||||
: []
|
||||
)
|
||||
);
|
||||
const assignedModels = new Set(
|
||||
profiles.flatMap(profile =>
|
||||
isRecord(profile) &&
|
||||
profile.enabled !== false &&
|
||||
Array.isArray(profile.models)
|
||||
? profile.models.filter(
|
||||
(model): model is string => typeof model === 'string'
|
||||
)
|
||||
: []
|
||||
)
|
||||
);
|
||||
|
||||
for (const [index, provider] of PROVIDERS.entries()) {
|
||||
const legacy = byId.get(`copilot.providers.${provider}`);
|
||||
if (!legacy) {
|
||||
continue;
|
||||
}
|
||||
if (!legacy) continue;
|
||||
if (!isRecord(legacy.value)) {
|
||||
throw new Error(`copilot.providers.${provider} must be an object`);
|
||||
}
|
||||
const id = `${provider}-default`;
|
||||
if (!profileIds.has(id)) {
|
||||
const models = PROVIDER_MODELS[provider].filter(
|
||||
model => !assignedModels.has(model)
|
||||
);
|
||||
const enabled = models.length > 0;
|
||||
profiles.push({
|
||||
id,
|
||||
type: provider,
|
||||
priority: PROVIDERS.length - index,
|
||||
models: enabled ? models : PROVIDER_MODELS[provider],
|
||||
config: legacy.value,
|
||||
...(enabled ? {} : { enabled: false }),
|
||||
});
|
||||
models.forEach(model => assignedModels.add(model));
|
||||
profileIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,8 +231,8 @@ import type {
|
||||
} from './plugins/copilot/runtime/contracts/tool-contract';
|
||||
|
||||
export const mergeUpdatesInApplyWay = serverNativeModule.mergeUpdatesInApplyWay;
|
||||
export const authorizeUserdataDocSubject =
|
||||
serverNativeModule.authorizeUserdataDocSubject;
|
||||
export const authorizeReservedDocSubject =
|
||||
serverNativeModule.authorizeReservedDocSubject;
|
||||
export const authSessionAccessTokenKeyId =
|
||||
serverNativeModule.authSessionAccessTokenKeyId;
|
||||
export const createAuthSessionRefreshToken =
|
||||
|
||||
@@ -104,6 +104,7 @@ test('does not schedule or run native search reconciliation when disabled', asyn
|
||||
await job.scheduleReconciliation();
|
||||
t.deepEqual(queue.add.firstCall.args[2], {
|
||||
jobId: 'backend-runtime-search-reconciliation',
|
||||
attempts: 1,
|
||||
removeOnFail: true,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user