Files
AFFiNE-Mirror/packages/backend/server/src/plugins/copilot/prompt/service.ts
DarkSky 728e02cab7 feat: bump eslint & oxlint (#14452)
#### PR Dependency Tree


* **PR #14452** 👈

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 null-safety, dependency tracking, upload validation, and
error logging for more reliable uploads, clipboard, calendar linking,
telemetry, PDF/theme printing, and preview/zoom behavior.
* Tightened handling of all-day calendar events (missing date now
reported).

* **Deprecations**
  * Removed deprecated RadioButton and RadioButtonGroup; use RadioGroup.

* **Chores**
* Unified and upgraded linting/config, reorganized imports, and
standardized binary handling for more consistent builds and tooling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-16 13:52:08 +08:00

221 lines
5.6 KiB
TypeScript

import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import { Prisma, PrismaClient } from '@prisma/client';
import { Config, OnEvent } from '../../../base';
import {
PromptConfig,
PromptConfigSchema,
PromptMessage,
PromptMessageSchema,
} from '../providers/types';
import { ChatPrompt } from './chat-prompt';
import {
CopilotPromptScenario,
prompts,
refreshPrompts,
Scenario,
} from './prompts';
@Injectable()
export class PromptService implements OnApplicationBootstrap {
private readonly logger = new Logger(PromptService.name);
private readonly cache = new Map<string, ChatPrompt>();
constructor(
private readonly config: Config,
private readonly db: PrismaClient
) {}
async onApplicationBootstrap() {
this.cache.clear();
await refreshPrompts(this.db);
}
@OnEvent('config.init')
async onConfigInit() {
await this.setup(this.config.copilot?.scenarios);
}
@OnEvent('config.changed')
async onConfigChanged(event: Events['config.changed']) {
if ('copilot' in event.updates) {
await this.setup(event.updates.copilot?.scenarios);
}
}
protected async setup(scenarios?: CopilotPromptScenario) {
if (!!scenarios && scenarios.override_enabled && scenarios.scenarios) {
this.logger.log('Updating prompts based on scenarios...');
for (const [scenario, model] of Object.entries(scenarios.scenarios)) {
const promptNames = Scenario[scenario as keyof typeof Scenario] || [];
if (!promptNames.length) continue;
for (const name of promptNames) {
const prompt = prompts.find(p => p.name === name);
if (prompt && model) {
await this.update(
prompt.name,
{ model, modified: true },
{ model: { not: model } }
);
}
}
}
} else {
this.logger.log('No scenarios enabled, using default prompts.');
const prompts = Object.values(Scenario).flat();
for (const prompt of prompts) {
await this.update(prompt, { modified: false });
}
}
}
/**
* list prompt names
* @returns prompt names
*/
async listNames() {
return this.db.aiPrompt
.findMany({ select: { name: true } })
.then(prompts => Array.from(new Set(prompts.map(p => p.name))));
}
async list() {
return this.db.aiPrompt.findMany({
select: {
name: true,
action: true,
model: true,
config: true,
messages: {
select: { role: true, content: true, params: true },
orderBy: { idx: 'asc' },
},
},
orderBy: { action: { sort: 'asc', nulls: 'first' } },
});
}
/**
* get prompt messages by prompt name
* @param name prompt name
* @returns prompt messages
*/
async get(name: string): Promise<ChatPrompt | null> {
// skip cache in dev mode to ensure the latest prompt is always fetched
if (!env.dev) {
const cached = this.cache.get(name);
if (cached) return cached;
}
const prompt = await this.db.aiPrompt.findUnique({
where: {
name,
},
select: {
name: true,
action: true,
model: true,
optionalModels: true,
config: true,
messages: {
select: {
role: true,
content: true,
params: true,
},
orderBy: {
idx: 'asc',
},
},
},
});
const messages = PromptMessageSchema.array().safeParse(prompt?.messages);
const config = PromptConfigSchema.safeParse(prompt?.config);
if (prompt && messages.success && config.success) {
const chatPrompt = ChatPrompt.createFromPrompt({
...prompt,
config: config.data,
messages: messages.data,
});
this.cache.set(name, chatPrompt);
return chatPrompt;
}
return null;
}
async set(
name: string,
model: string,
messages: PromptMessage[],
config?: PromptConfig | null
) {
return await this.db.aiPrompt
.create({
data: {
name,
model,
config: config || undefined,
messages: {
create: messages.map((m, idx) => ({
idx,
...m,
attachments: m.attachments || undefined,
params: m.params || undefined,
})),
},
},
})
.then(ret => ret.id);
}
@Transactional()
async update(
name: string,
data: {
messages?: PromptMessage[];
model?: string;
modified?: boolean;
config?: PromptConfig;
},
where?: Prisma.AiPromptWhereInput
) {
const { config, messages, model, modified } = data;
const existing = await this.db.aiPrompt
.count({ where: { ...where, name } })
.then(count => count > 0);
if (existing) {
await this.db.aiPrompt.update({
where: { name },
data: {
config: config || undefined,
updatedAt: new Date(),
modified,
model,
messages: messages
? {
// cleanup old messages
deleteMany: {},
create: messages.map((m, idx) => ({
idx,
...m,
attachments: m.attachments || undefined,
params: m.params || undefined,
})),
}
: undefined,
},
});
this.cache.delete(name);
}
}
async delete(name: string) {
const { id } = await this.db.aiPrompt.delete({ where: { name } });
this.cache.delete(name);
return id;
}
}