mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 11:59:51 +08:00
feat(core): improve byok editing (#15427)
fix #14287 fix #15359 fix #15424 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Redesigned workspace AI provider settings with connection testing, storage options, model selection, capability management, ordering, and custom endpoints. * AI chat model choices now adapt to the selected workspace and conversation route. * Added support for image-based AI requests. * **Bug Fixes** * Improved handling of unavailable or outdated model selections. * App configuration updates now reject overlapping paths and load deterministically. * **Tests** * Expanded coverage for provider models, AI chat scoping, image requests, and configuration validation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -144,6 +144,8 @@ test('image request builder receives only serializable request options', async t
|
||||
runtime.streamImageArtifacts({}, [{ role: 'user', content: 'draw' }], {
|
||||
quality: 'high',
|
||||
seed: 42,
|
||||
modelName: 'stabilityai/stable-diffusion-xl-base-1.0',
|
||||
loras: [{ path: 'https://example.com/sketch.safetensors', scale: 1 }],
|
||||
signal: controller.signal,
|
||||
user: 'user-1',
|
||||
})
|
||||
@@ -158,6 +160,13 @@ test('image request builder receives only serializable request options', async t
|
||||
outputFormat: 'webp',
|
||||
seed: 42,
|
||||
},
|
||||
providerOptions: {
|
||||
provider: 'fal',
|
||||
options: {
|
||||
model_name: 'stabilityai/stable-diffusion-xl-base-1.0',
|
||||
loras: [{ path: 'https://example.com/sketch.safetensors', scale: 1 }],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
@@ -14,6 +15,7 @@ const module = await createModule({
|
||||
const service = module.get(ServerService);
|
||||
const user = await module.create(Mockers.User);
|
||||
const models = module.get(Models);
|
||||
const db = module.get(PrismaClient);
|
||||
|
||||
test.afterEach(async () => {
|
||||
Sinon.reset();
|
||||
@@ -111,6 +113,40 @@ test('should revalidate config', async t => {
|
||||
t.is(service.getConfig().server.externalUrl, newValue);
|
||||
});
|
||||
|
||||
test('should reject overlapping app config paths in one update', async t => {
|
||||
await t.throwsAsync(
|
||||
models.appConfig.save(user.id, [
|
||||
{ key: 'testOverlapRoot.branch', value: { enabled: true } },
|
||||
{ key: 'testOverlapRoot.branch.enabled', value: false },
|
||||
]),
|
||||
{ message: /must not overlap/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('should serialize concurrent overlapping app config updates', async t => {
|
||||
const root = `testConcurrentOverlap.${faker.string.uuid()}`;
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
models.appConfig.save(user.id, [{ key: root, value: { enabled: true } }]),
|
||||
models.appConfig.save(user.id, [
|
||||
{ key: `${root}.enabled`, value: false },
|
||||
]),
|
||||
]);
|
||||
|
||||
t.is(results.filter(result => result.status === 'fulfilled').length, 1);
|
||||
t.is(results.filter(result => result.status === 'rejected').length, 1);
|
||||
t.regex(
|
||||
String(results.find(result => result.status === 'rejected')?.reason),
|
||||
/must not overlap/
|
||||
);
|
||||
} finally {
|
||||
await db.appConfig.deleteMany({
|
||||
where: { id: { startsWith: root } },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should emit config changed event', async t => {
|
||||
const newUrl = faker.internet.url();
|
||||
|
||||
|
||||
@@ -9,11 +9,34 @@ export class AppConfigModel extends BaseModel {
|
||||
async load(excludedKeys: string[] = []) {
|
||||
return this.db.appConfig.findMany({
|
||||
where: excludedKeys.length ? { id: { notIn: excludedKeys } } : undefined,
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async save(user: string, updates: Array<{ key: string; value: any }>) {
|
||||
await this.db
|
||||
.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${'app-config-paths'}, 0))`;
|
||||
const existing = await this.db.appConfig.findMany({
|
||||
select: { id: true },
|
||||
});
|
||||
const updateKeys = updates.map(update => update.key);
|
||||
for (const [index, key] of updateKeys.entries()) {
|
||||
const overlappingKey = [
|
||||
...existing.map(config => config.id),
|
||||
...updateKeys.slice(0, index),
|
||||
].find(
|
||||
candidate =>
|
||||
candidate !== key &&
|
||||
(candidate.startsWith(`${key}.`) || key.startsWith(`${candidate}.`))
|
||||
);
|
||||
if (overlappingKey) {
|
||||
throw new Error(
|
||||
`App config paths must not overlap: ${overlappingKey} and ${key}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.allSettled(
|
||||
updates.map(async update => {
|
||||
return this.db.appConfig.upsert({
|
||||
|
||||
@@ -398,13 +398,13 @@ export class CapabilityRuntime {
|
||||
_filter?: ProviderFilter,
|
||||
slot = 'image.generate'
|
||||
): AsyncIterableIterator<NativeImageArtifact> {
|
||||
const { quality, seed } = options;
|
||||
const { quality, seed, modelName, loras } = options;
|
||||
const result = (await this.execute(
|
||||
slot,
|
||||
buildLlmImageRequestFromMessages({
|
||||
model: 'route-selected',
|
||||
messages: preparePromptMessagesForNativeRequest(messages, true),
|
||||
options: { quality, seed },
|
||||
options: { quality, seed, modelName, loras },
|
||||
}),
|
||||
cond,
|
||||
options
|
||||
|
||||
Reference in New Issue
Block a user