更改路由包

main
落雨楓 7 months ago
parent 449cbae77d
commit ba06f46898

@ -12,7 +12,11 @@
"author": "Hyperzlib",
"license": "MIT",
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.2",
"@serafin/schema-builder": "^0.18.17",
"@tripleuniverse/koa-swagger-decorator": "^1.9.0",
"@waylaidwanderer/fetch-event-source": "^3.0.1",
"ajv": "^8.17.1",
"extensionless": "^1.9.9",
"got": "^14.4.7",
"hpagent": "^1.2.0",
@ -20,6 +24,7 @@
"koa-body": "^6.0.1",
"koa-logger": "^3.2.1",
"koa-router": "^13.0.1",
"koa-swagger-decorator": "^2.2.1",
"pg": "^8.16.0",
"reflect-metadata": "^0.2.2",
"typeorm": "^0.3.24",
@ -33,7 +38,8 @@
"@types/koa-router": "^7.4.8",
"@types/node": "^22.15.21",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"typescript-json-schema": "^0.65.1"
},
"imports": {
"#app/*": "./dist/*"

File diff suppressed because it is too large Load Diff

@ -4,7 +4,7 @@ import * as fs from 'node:fs';
import winston from 'winston';
import Yaml from 'yaml';
import { Logger } from '#app/utils/logger.js';
import Koa from 'koa';
import Koa, { Next } from 'koa';
import Router from 'koa-router';
import koaLogger from 'koa-logger';
import { koaBody } from 'koa-body';
@ -14,6 +14,8 @@ import { Entities } from './entity/index.js';
import { ServerConfig } from './types/ServerConfig.js';
import { LLMApiProxies } from './proxies/index.js';
import { MemoryController } from './controllers/MemoryController.js';
import { swaggerRouter } from './router.js';
import { SwaggerRouter, z } from 'koa-swagger-decorator';
export default class App {
public config: ServerConfig = {
@ -27,11 +29,11 @@ export default class App {
public baseLogger!: winston.Logger;
public logger!: Logger;
public webApp!: Koa;
public router!: Router;
public router!: SwaggerRouter;
public db?: DataSource;
public db: DataSource | null = null;
constructor(configPath: string) {
this.loadConfig(configPath);
@ -138,15 +140,20 @@ export default class App {
let subRouter = new Router({ prefix: `/${proxyId}` });
new LLMApiProxies[proxyConfig.type](this, subRouter, proxyId, proxyConfig);
this.router.use(subRouter.routes())
.use(subRouter.allowedMethods());
this.router.use(subRouter.routes() as any)
.use(subRouter.allowedMethods() as any);
this.logger.info(`Initialized proxy: ${proxyId} (${proxyConfig.type})`);
}
}
public initHttpServer(): void {
this.webApp = new Koa();
this.router = new Router();
this.webApp.context.getLogger = this.getLogger.bind(this);
this.webApp.context.application = this;
this.webApp.context.database = this.db;
this.router = swaggerRouter;
if (this.debug) {
this.webApp.use(koaLogger());
@ -155,10 +162,44 @@ export default class App {
this.webApp.use(koaBody());
new EchoBackController(this, this.router);
new MemoryController(this, this.router);
}
MemoryController.initRoutes(this, this.router);
}
public async httpErrorHandler(ctx: Koa.Context, next: Next): Promise<void> {
try {
await next();
} catch (error: any) {
if (error.name === 'ZodError') {
const zodError = error as z.ZodError;
ctx.status = 422; // Unprocessable Entity
ctx.body = {
success: false,
message: 'Validation error',
errors: zodError.errors,
};
return;
} else if (error.status) {
ctx.status = error.status;
ctx.body = {
success: false,
message: error.message || 'An error occurred',
};
} else {
this.logger.error('Internal server error: ' + error.message);
console.log(error);
ctx.status = 500; // Internal Server Error
ctx.body = {
success: false,
message: 'Internal server error',
};
}
}
}
private start(): void {
this.webApp.use(this.httpErrorHandler.bind(this));
this.webApp.use(this.router.routes())
.use(this.router.allowedMethods());

@ -1,14 +1,13 @@
import App from "#app/App.js";
import { Logger } from "#app/utils/logger.js";
import Koa from "koa";
import Router from "koa-router";
import { SwaggerRouter } from "koa-swagger-decorator";
export class EchoBackController {
private app: App;
private router: Router;
private router: SwaggerRouter;
private logger: Logger;
constructor(app: App, router: Router) {
constructor(app: App, router: SwaggerRouter) {
this.app = app;
this.router = router;
this.logger = app.getLogger('EchoBackController');

@ -5,32 +5,58 @@ import { ConversationPage } from "#app/entity/ConversationPage.js";
import { Logger } from "#app/utils/logger.js";
import Koa, { Next } from "koa";
import Router from "koa-router";
import { ActionRes, CreateNewPageReq, CreateNewPageReqType, GetConversationInfoRes, GetConversationInfoResType, GetDialogueInfoRes, GetDialogueInfoResType, GetMessageContentQuery, GetPageInfoRes, GetPageInfoResType, GetPageListRes, GetPageListResType, OverrideDialogueMessagesReq, OverrideDialogueMessagesReqType, UpdateDialogueMessagesReq, UpdateDialogueMessagesReqType, type ActionResType } from "./schemas/MemoryControllerSchemas.js";
import { body, responses, routeConfig, SwaggerRouter, z } from "koa-swagger-decorator";
type ResponsesConfig = Parameters<typeof routeConfig>[0]['responses'];
export const DefaultResponses: ResponsesConfig = {
404: {
description: 'Not found',
content: {
'application/json': {
schema: ActionRes,
},
},
},
422: {
description: 'Invalid parameters',
content: {
'application/json': {
schema: ActionRes,
},
},
},
500: {
description: 'Internal server error',
content: {
'application/json': {
schema: ActionRes,
},
},
},
}
export class MemoryController {
private app: App;
private router: Router;
private logger: Logger;
constructor(app: App, router: Router) {
this.app = app;
this.router = router;
this.logger = app.getLogger('MemoryController');
this.initRoutes();
public static initRoutes(app: App, router: SwaggerRouter): void {
router.applyRoute(MemoryController);
}
private initRoutes(): void {
var memoryRouter = new Router({
var memoryRouter = new SwaggerRouter({
prefix: '/memory',
});
memoryRouter.all('/v1/*path', this.checkDb);
memoryRouter.get('/v1/:conversationId', this.getConversationInfo);
memoryRouter.all('/v1/(.*)', this.checkDb);
memoryRouter.get('/v1/:conversationId/last_dialogue', this.getConversationLastDialogue);
memoryRouter.post('/v1/:conversationId/last_dialogue', this.updateConversationLastDialogue);
memoryRouter.get('/v1/:conversationId/last_dialogue/message/:role', async (ctx) => { });
memoryRouter.post('/v1/:conversationId/last_dialogue/message/:role', async (ctx) => { });
memoryRouter.post('/v1/:conversationId/append', this.appendMessagesToConversation);
memoryRouter.post('/v1/:conversationId/override', this.overrideMessagesToConversation);
memoryRouter.delete('/v1/:conversationId', this.deleteConversation);
memoryRouter.get('/v1/:conversationId/pages', this.listPages);
@ -49,30 +75,39 @@ export class MemoryController {
memoryRouter.delete('/v1/:conversationId/page/:pageId/metadata/:varKey', async (ctx) => { });
memoryRouter.delete('/v1/:conversationId/page/:pageId/metadata', async (ctx) => { });
memoryRouter.post('/v1/:conversationId/page/:pageId/:varKey/delete', async (ctx) => { });
this.router.use(memoryRouter.routes())
.use(memoryRouter.allowedMethods());
}
private checkDb = async (ctx: Koa.ParameterizedContext, next: Next): Promise<void> => {
if (!this.app.db) {
public checkDb(ctx: Koa.ParameterizedContext): boolean {
if (!ctx.database) {
ctx.status = 500;
ctx.body = { error: 'Database connection is not available' };
return;
return false;
}
await next();
return true;
}
private getConversationInfo = async (ctx: Koa.ParameterizedContext): Promise<Conversation | null> => {
const conversationId = ctx.params.conversationId;
if (!conversationId) {
ctx.status = 400;
ctx.body = { error: 'Conversation ID is required' };
@routeConfig({
method: 'get',
path: '/memory/v1/{conversationId}',
summary: 'Get conversation information',
operationId: 'getConversationInfo',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to retrieve information for.'),
}),
},
responses: DefaultResponses,
})
@responses(GetConversationInfoRes)
public async getConversationInfo(ctx: Koa.ParameterizedContext): Promise<Conversation | null> {
if (!this.checkDb(ctx)) {
return null;
}
const conversationId = ctx.params.conversationId as string;
let conversationData = await this.app.db!.getRepository(Conversation)
let conversationData = await ctx.database!.getRepository(Conversation)
.findOneBy({ conversationId: conversationId });
if (!conversationData) {
@ -82,18 +117,38 @@ export class MemoryController {
}
ctx.status = 200;
ctx.body = conversationData;
ctx.body = {
conversationId: conversationData.conversationId,
llmApi: conversationData.llmApi,
dialogueCount: conversationData.dialogueCount,
currentPage: conversationData.currentPage,
createdAt: conversationData.createdAt.toISOString(),
updatedAt: conversationData.updatedAt.toISOString(),
} as GetConversationInfoResType;
return conversationData;
}
private getConversationLastDialogue = async (ctx: Koa.ParameterizedContext): Promise<ConversationDialogue | null> => {
@routeConfig({
method: 'get',
path: '/memory/v1/{conversationId}/last_dialogue',
summary: 'Get the last dialogue of a conversation',
operationId: 'getConversationLastDialogue',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to retrieve the last dialogue for.'),
}),
},
responses: DefaultResponses,
})
@responses(GetDialogueInfoRes)
public async getConversationLastDialogue(ctx: Koa.ParameterizedContext): Promise<ConversationDialogue | null> {
const conversation = await this.getConversationInfo(ctx);
if (!conversation) {
return null;
}
const lastDialogue = await conversation.getLastDialogue(this.app);
const lastDialogue = await conversation.getLastDialogue(ctx.application);
if (!lastDialogue) {
ctx.status = 404;
ctx.body = { error: 'No dialogues found for this conversation' };
@ -101,17 +156,49 @@ export class MemoryController {
}
ctx.status = 200;
ctx.body = lastDialogue;
ctx.body = {
id: lastDialogue.id,
conversationId: lastDialogue.conversationId,
dialogueCount: lastDialogue.dialogueCount,
page: lastDialogue.page,
messages: lastDialogue.messages,
updatedAt: lastDialogue.updatedAt.toISOString(),
} as GetDialogueInfoResType;
return lastDialogue;
}
private updateConversationLastDialogue = async (ctx: Koa.ParameterizedContext): Promise<void> => {
if (!ctx.request.body || !ctx.request.body.messages || !Array.isArray(ctx.request.body.messages)) {
ctx.status = 400;
ctx.body = { error: 'Invalid request body, messages array is required' };
@routeConfig({
method: 'post',
path: '/memory/v1/{conversationId}/last_dialogue',
summary: 'Update the last dialogue of a conversation',
operationId: 'updateConversationLastDialogue',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to update the last dialogue for.'),
}),
},
responses: DefaultResponses,
})
@body(UpdateDialogueMessagesReq)
@responses(ActionRes)
public async updateConversationLastDialogue(ctx: Koa.ParameterizedContext): Promise<void> {
if (!this.checkDb(ctx)) {
return;
}
try {
UpdateDialogueMessagesReq.parse(ctx.request.body);
} catch (error: any) {
ctx.status = 422;
ctx.body = {
success: false,
message: 'Invalid request body',
error: error.message,
} as ActionResType;
return;
}
const body = ctx.request.body as UpdateDialogueMessagesReqType;
const lastDialogue = await this.getConversationLastDialogue(ctx);
@ -119,75 +206,343 @@ export class MemoryController {
return;
}
const messages = ctx.request.body.messages;
const messages = body.messages;
lastDialogue.messages = messages;
lastDialogue.updatedAt = new Date();
await this.app.db!.getRepository(ConversationDialogue)
await ctx.database!.getRepository(ConversationDialogue)
.save(lastDialogue);
ctx.status = 200;
ctx.body = { success: true };
ctx.body = {
success: true,
message: 'Last dialogue updated successfully',
} as ActionResType;
}
private appendMessagesToConversation = async (ctx: Koa.ParameterizedContext): Promise<void> => {
if (!ctx.request.body || !ctx.request.body.messages || !Array.isArray(ctx.request.body.messages)) {
ctx.status = 400;
ctx.body = { error: 'Invalid request body, messages array is required' };
@routeConfig({
method: 'get',
path: '/memory/v1/{conversationId}/last_dialogue/message/{role}',
summary: 'Get the last dialogue of a conversation',
operationId: 'getConversationLastDialogueMessage',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to retrieve the last dialogue for.'),
role: z.string().describe('The role of the message to retrieve, e.g., "user" or "assistant".'),
}),
query: GetMessageContentQuery,
},
responses: {
200: {
description: 'The content of the last message for the specified role.',
content: {
'text/plain': {
schema: z.string().describe('The content of the last message for the specified role.'),
},
},
},
...DefaultResponses,
}
})
public async getConversationLastDialogueMessage(ctx: Koa.ParameterizedContext): Promise<void> {
const lastDialogue = await this.getConversationLastDialogue(ctx);
if (!lastDialogue) {
return;
}
if (!ctx.params.role) {
ctx.status = 422;
ctx.body = { error: 'Role parameter is required' };
return;
}
const role = ctx.params.role as string;
let messageContent: string | null = null;
if (typeof ctx.query.offset === 'string') {
const offset = parseInt(ctx.query.offset, 10);
if (isNaN(offset) || offset < 0) {
ctx.status = 422;
ctx.body = { error: 'Invalid offset value' };
return;
}
let foundCount = 0;
for (let i = 0; i < lastDialogue.messages.length; i++) {
const msg = lastDialogue.messages[i];
if (msg.role === role) {
if (foundCount === offset) {
messageContent = msg.content;
break;
}
foundCount++;
}
}
} else if (typeof ctx.query.offsetEnd === 'string') {
const offsetEnd = parseInt(ctx.query.offsetEnd, 10);
if (isNaN(offsetEnd) || offsetEnd < 0) {
ctx.status = 422;
ctx.body = { error: 'Invalid offsetEnd value' };
return;
}
let foundCount = 0;
for (let i = lastDialogue.messages.length - 1; i >= 0; i--) {
const msg = lastDialogue.messages[i];
if (msg.role === role) {
if (foundCount === offsetEnd) {
messageContent = msg.content;
break;
}
foundCount++;
}
}
} else {
messageContent = lastDialogue.messages.find(msg => msg.role === role)?.content || null;
}
if (messageContent === null) {
ctx.status = 404;
ctx.body = { error: 'Message not found for the specified role and offset' };
return;
}
ctx.status = 200;
ctx.type = 'text/plain';
ctx.body = messageContent;
}
@routeConfig({
method: 'post',
path: '/memory/v1/{conversationId}/append',
summary: 'Append messages to a conversation',
operationId: 'appendMessagesToConversation',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to append messages to.'),
}),
},
responses: DefaultResponses,
})
@body(UpdateDialogueMessagesReq)
@responses(ActionRes)
public async appendMessagesToConversation(ctx: Koa.ParameterizedContext): Promise<void> {
try {
UpdateDialogueMessagesReq.parse(ctx.request.body);
} catch (error: any) {
ctx.status = 422;
ctx.body = {
success: false,
message: 'Invalid request body',
error: error.message,
} as ActionResType;
return;
}
const body = ctx.request.body as UpdateDialogueMessagesReqType;
const conversation = await this.getConversationInfo(ctx);
if (!conversation) {
return;
}
const messages = ctx.request.body.messages;
const dialogueCount = ctx.request.body.dialogueCount ?? undefined;
conversation.appendMessages(this.app, ctx.request.body.messages, {
dialogueCount,
conversation.appendMessages(ctx.application, body.messages, {
dialogueCount: body.dialogueCount ?? undefined,
});
ctx.status = 200;
ctx.body = { success: true };
ctx.body = {
success: true,
message: 'Messages appended successfully',
} as ActionResType;
}
private deleteConversation = async (ctx: Koa.ParameterizedContext): Promise<void> => {
@routeConfig({
method: 'post',
path: '/memory/v1/{conversationId}/override',
summary: 'Override messages in a conversation',
operationId: 'overrideMessagesToConversation',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to override messages in.'),
}),
},
responses: DefaultResponses,
})
@body(OverrideDialogueMessagesReq)
@responses(ActionRes)
public async overrideMessagesToConversation(ctx: Koa.ParameterizedContext): Promise<void> {
try {
OverrideDialogueMessagesReq.parse(ctx.request.body);
} catch (error: any) {
ctx.status = 422;
ctx.body = {
success: false,
message: 'Invalid request body',
error: error.message,
} as ActionResType;
return;
}
const body = ctx.request.body as OverrideDialogueMessagesReqType;
const conversation = await this.getConversationInfo(ctx);
if (!conversation) {
return;
}
await this.app.db!.getRepository(Conversation)
conversation.replaceHistoryMessages(ctx.application, body.messages, {
dialogueCount: body.dialogueCount,
});
ctx.status = 200;
ctx.body = {
success: true,
message: 'Messages overrided successfully',
} as ActionResType;
}
@routeConfig({
method: 'delete',
path: '/memory/v1/{conversationId}',
summary: 'Delete a conversation',
operationId: 'deleteConversation',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to delete.'),
}),
},
responses: DefaultResponses,
})
@responses(ActionRes)
public async deleteConversation(ctx: Koa.ParameterizedContext): Promise<void> {
const conversation = await this.getConversationInfo(ctx);
if (!conversation) {
return;
}
await ctx.database!.getRepository(Conversation)
.remove(conversation);
ctx.status = 200;
ctx.body = { success: true };
ctx.body = {
success: true,
message: 'Conversation deleted successfully',
} as ActionResType;
}
private listPages = async (ctx: Koa.ParameterizedContext): Promise<void> => {
@routeConfig({
method: 'get',
path: '/memory/v1/{conversationId}/pages',
summary: 'List all pages of a conversation',
operationId: 'listPages',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to list pages for.'),
}),
},
responses: DefaultResponses,
})
@responses(GetPageListRes as any)
public async listPages(ctx: Koa.ParameterizedContext): Promise<void> {
const conversation = await this.getConversationInfo(ctx);
if (!conversation) {
return;
}
const pages = await this.app.db!.getRepository(ConversationPage)
const pages = await ctx.database!.getRepository(ConversationPage)
.find({ where: { conversationId: conversation.conversationId } });
ctx.status = 200;
ctx.body = pages;
ctx.body = pages.map(page => ({
id: page.id,
conversationId: page.conversationId,
pageId: page.pageId,
updatedAt: page.updatedAt.toISOString(),
})) as GetPageListResType;
};
private createNewPage = async (ctx: Koa.ParameterizedContext): Promise<void> => {
@routeConfig({
method: 'post',
path: '/memory/v1/{conversationId}/new_page',
summary: 'Create a new page for a conversation',
operationId: 'createNewPage',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to create a new page for.'),
}),
},
responses: DefaultResponses,
})
@body(CreateNewPageReq)
@responses(ActionRes)
public async createNewPage(ctx: Koa.ParameterizedContext): Promise<void> {
try {
CreateNewPageReq.parse(ctx.request.body);
} catch (error: any) {
ctx.status = 422;
ctx.body = {
success: false,
message: 'Invalid request body',
error: error.message,
} as ActionResType;
}
const body = ctx.request.body as CreateNewPageReqType;
const conversation = await this.getConversationInfo(ctx);
if (!conversation) {
return;
}
const initialMetadata = ctx.request.body.metadata || {};
await conversation.createNewPage(this.app, initialMetadata);
await conversation.createNewPage(ctx.application, body.metadata ?? undefined);
ctx.status = 200;
ctx.body = { success: true };
ctx.body = {
success: true,
message: 'New page created successfully',
} as ActionResType;
};
@routeConfig({
method: 'get',
path: '/memory/v1/{conversationId}/page/{pageId}',
summary: 'Get information about a specific page in a conversation',
operationId: 'getPageInfo',
request: {
params: z.object({
conversationId: z.string().describe('The conversation ID to retrieve the page for.'),
pageId: z.number().describe('The page ID to retrieve information for.'),
}),
},
responses: DefaultResponses,
})
@responses(GetPageInfoRes)
public async getPageInfo(ctx: Koa.ParameterizedContext): Promise<ConversationPage | null> {
const conversation = await this.getConversationInfo(ctx);
if (!conversation) {
return null;
}
const pageId = parseInt(ctx.params.pageId, 10);
if (isNaN(pageId) || pageId < 0) {
ctx.status = 422;
ctx.body = { error: 'Invalid page ID' };
return null;
}
const pageData = await ctx.database!.getRepository(ConversationPage)
.findOneBy({ conversationId: conversation.conversationId, pageId: pageId });
if (!pageData) {
ctx.status = 404;
ctx.body = { error: 'Page not found' };
return null;
}
ctx.status = 200;
ctx.body = {
id: pageData.id,
conversationId: pageData.conversationId,
pageId: pageData.pageId,
metadata: pageData.metadata,
updatedAt: pageData.updatedAt.toISOString(),
} as GetPageInfoResType;
return pageData;
}
}

@ -0,0 +1,122 @@
import { SB } from '@serafin/schema-builder';
import { z } from 'koa-swagger-decorator';
export const ConversationDataSchema = z.object({
conversationId: z.string().describe('The unique identifier for the conversation.'),
llmApi: z.string().describe('The API id of the LLM used in the conversation.'),
dialogueCount: z.number().int().min(0).describe('Current dialogue count (from Dify) in the conversation.'),
currentPage: z.number().int().min(0).describe('The current page ID in the conversation.'),
createdAt: z.string().describe('The timestamp when the conversation was created.').datetime(),
updatedAt: z.string().describe('The timestamp when the conversation was last updated.').datetime(),
});
export const CommonApiMessageSchema = z.object({
role: z.string().describe('The role of the message sender, e.g., "user" or "assistant".'),
content: z.string().describe('The content of the message.'),
name: z.string().describe('The name of the message sender, if applicable.').nullable().optional(),
}).passthrough();
export const ConversationDialogueSchema = z.object({
id: z.number().int().min(0).describe('The unique identifier for the dialogue.'),
conversationId: z.string().describe('The unique identifier for the conversation.'),
dialogueCount: z.number().int().min(0).describe('The count of dialogues of this dialogue.'),
page: z.number().int().min(0).describe('The page number of the dialogue.'),
messages: z.array(CommonApiMessageSchema).describe('Messages'),
updatedAt: z.string().describe('The time when the dialogue was last updated.').datetime(),
});
export const ConversationPageSchema = z.object({
id: z.number().int().min(0).describe('The unique identifier for the page record.'),
conversationId: z.string().describe('The unique identifier for the conversation.'),
pageId: z.number().int().min(0).describe('The page number of the conversation.'),
metadata: z.record(z.any()).describe('Metadata'),
updatedAt: z.string().describe('The timestamp when the page was last updated.').datetime(),
});
export const ActionRes = z.object({
success: z.boolean().describe('Indicates whether the action was successful.'),
message: z.string().describe('A message providing additional information about the action result.'),
error: z.string().describe('An error message if the action failed.').nullable().optional(),
});
export type ActionResType = z.infer<typeof ActionRes>;
/**
*
*/
export const GetConversationInfoRes = ConversationDataSchema;
export type GetConversationInfoResType = z.infer<typeof GetConversationInfoRes>;
/**
*
*/
export const GetDialogueInfoRes = ConversationDialogueSchema;
export type GetDialogueInfoResType = z.infer<typeof GetDialogueInfoRes>;
/**
*
*/
export const UpdateDialogueMessagesReq = z.object({
messages: z.array(
z.object({
role: z.string().describe('The role of the message sender, e.g., "user" or "assistant".'),
content: z.string().describe('The content of the message.'),
name: z.string().describe('The name of the message sender, if applicable.').nullable().optional(),
}).passthrough()
).describe('An array of messages to update the dialogue with.'),
dialogueCount: z.number().int().min(0).describe('The count of dialogues in the conversation.').nullable().optional(),
});
export type UpdateDialogueMessagesReqType = z.infer<typeof UpdateDialogueMessagesReq>;
export const OverrideDialogueMessagesReq = z.object({
messages: z.array(CommonApiMessageSchema).describe('An array of messages to update the dialogue with.'),
dialogueCount: z.number().int().min(0).describe('The count of dialogues in the conversation.'),
});
export type OverrideDialogueMessagesReqType = z.infer<typeof OverrideDialogueMessagesReq>;
export const GetMessageContentQuery = z.object({
offset: z.string().describe('The number of dialogues to skip. for example, 1 means update the 2nd message of this role.').nullable().optional(),
offsetEnd: z.string().describe('The number of dialogues to skip from the end. for example, 1 means update the 2nd last message of this role.').nullable().optional(),
}).describe('GetMessageContentQuery');
export type GetMessageContentQueryType = z.infer<typeof GetMessageContentQuery>;
export const GetPageListRes = z.array(
ConversationPageSchema.omit({ metadata: true })
).describe('GetPageListRes');
export type GetPageListResType = z.infer<typeof GetPageListRes>;
export const GetPageInfoRes = ConversationPageSchema;
export type GetPageInfoResType = z.infer<typeof GetPageInfoRes>;
export const GetPageMetadataRes = z.object({
metadata: z.record(z.any()).describe('The metadata of the page.'),
}).describe('GetPageMetadataRes');
export type GetPageMetadataResType = z.infer<typeof GetPageMetadataRes>;
export const UpdatePageMetadataReq = z.object({
metadata: z.record(z.any()).describe('The metadata to update for the page.'),
}).describe('UpdatePageMetadataReq');
export type UpdatePageMetadataReqType = z.infer<typeof UpdatePageMetadataReq>;
export const GetPageMetadataItemRes = z.object({
key: z.string().describe('The key of the metadata item.'),
value: z.any().describe('The value of the metadata item.'),
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'null']).describe('The type of the metadata item value.'),
exists: z.boolean().describe('Indicates whether the metadata item exists.'),
}).describe('GetPageMetadataItemRes');
export type GetPageMetadataItemResType = z.infer<typeof GetPageMetadataItemRes>;
export const UpdatePageMetadataItemReq = z.object({
value: z.any().describe('The new value for the metadata item.'),
valueIsJson: z.boolean().describe('Indicates whether the value is a JSON string.').default(false),
}).describe('UpdatePageMetadataItemReq');
export type UpdatePageMetadataItemReqType = z.infer<typeof UpdatePageMetadataItemReq>;
export const CreateNewPageReq = z.object({
metadata: z.record(z.any()).describe('The metadata for the new page.').nullable().optional(),
}).describe('NewPageReq');
export type CreateNewPageReqType = z.infer<typeof CreateNewPageReq>;
export const CreateNewPageRes = z.object({
pageId: z.number().int().min(0).describe('The ID of the newly created page.'),
}).describe('NewPageRes');
export type CreateNewPageResType = z.infer<typeof CreateNewPageRes>;

@ -6,22 +6,40 @@ import { CommonApiMessage } from "#app/types/Messages.js"
@Entity()
export class Conversation {
/**
* The conversation id from Dify.
*/
@PrimaryColumn("uuid", { name: "conversation_id", comment: "Conversation ID from Dify" })
conversationId!: string
/**
* The LLM API id used for this conversation.
*/
@Column("varchar", { name: "llm_api", length: 255 })
@Index()
llmApi!: string
/**
* Current dialogue count (from Dify).
*/
@Column("integer", { name: "dialogue_count", default: 0 })
dialogueCount!: number
/**
* Current page number.
*/
@Column("integer", { name: "current_page", default: 0 })
currentPage!: number
/**
* Created time
*/
@Column("timestamp", { name: "created_at" })
createdAt!: Date
/**
* Updated time
*/
@Column("timestamp", { name: "updated_at" })
updatedAt!: Date

11
src/index.d.ts vendored

@ -0,0 +1,11 @@
import { Logger } from './utils/logger';
import { DataSource } from 'typeorm';
import App from './App';
declare module 'koa' {
interface ExtendableContext {
getLogger: (tag: string) => Logger;
application: App;
database: DataSource | null;
}
}

@ -135,7 +135,7 @@ export class OpenAIApiProxy extends BaseLLMProxy {
};
}
let metaConfigMatch = systemMessageContent.match(/<metaconfig>(.*?)<\/metaconfig>/);
let metaConfigMatch = systemMessageContent.match(/<metaconfig>([\s\S]*?)<\/metaconfig>/);
if (!metaConfigMatch || metaConfigMatch.length < 2) {
this.logger.warn('No valid metaconfig found in system prompt, skipping memory processing.');
// 如果没有找到有效的元数据,则返回原始消息。
@ -150,7 +150,7 @@ export class OpenAIApiProxy extends BaseLLMProxy {
let metaConfigContent = metaConfigMatch[1].trim();
// 将 metadata 从 system prompt 中去除
systemMessage.content = systemMessageContent.replace(/<metaconfig>.*?<\/metaconfig>/, '').trim();
systemMessage.content = systemMessageContent.replace(/<metaconfig>[\s\S]*?<\/metaconfig>/, '').trim();
let metaConfig: ApiProxyMetaConfig = {};
try {
@ -228,7 +228,7 @@ export class OpenAIApiProxy extends BaseLLMProxy {
private async processMessageBeforeSave(message: CommonApiMessage): Promise<CommonApiMessage> {
message = { ...message };
if (typeof message?.content === 'string') {
message.content = message.content!.replace(/<excludeinmemory>.*?<\/excludeinmemory>/g, '');
message.content = message.content!.replace(/<excludeinmemory>[\s\S]*?<\/excludeinmemory>/g, '');
}
return message;
@ -440,9 +440,11 @@ export class OpenAIApiProxy extends BaseLLMProxy {
try {
let reqBody: any = ctx.request.body;
console.log('Request Body:', reqBody);
console.log('Request Headers:', ctx.request.headers);
let messagesInfo = this.extractMessagesInfo(reqBody?.messages || []);
console.log('Extracted Messages Info:', messagesInfo);
let { metaConfig, messages, userMessage, systemMessage } = messagesInfo;
let savedMessages = await this.loadMemory(messagesInfo);

@ -0,0 +1,27 @@
import Router from 'koa-router';
import { SwaggerRouter } from 'koa-swagger-decorator';
export const swaggerRouter = new SwaggerRouter({
spec: {
info: {
title: 'Memory Proxy for Dify API',
description: 'This is a memory proxy for Dify API, which is used to store and retrieve data in memory.',
version: '1.0.0',
},
servers: [
{
url: 'http://localhost:8616',
description: 'Local server',
}
],
security: [
{
BearerAuth: []
}
],
},
swaggerHtmlEndpoint: '/docs',
swaggerJsonEndpoint: '/openapi.json',
});
swaggerRouter.swagger();

@ -24,7 +24,7 @@
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "Node18", /* Specify what module code is generated. */
"module": "NodeNext", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */

Loading…
Cancel
Save