mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ai): expose global memory management api
This commit is contained in:
1 parent
ed8ab84b4b
commit
2938c42f9f
10 files changed
+375
-2
No files matched your search
@@ -221,6 +221,7 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
|
|||||||
aiContext: {
|
aiContext: {
|
||||||
aiDataDir,
|
aiDataDir,
|
||||||
aiChatManager,
|
aiChatManager,
|
||||||
|
aiMemoryService,
|
||||||
assistantManager,
|
assistantManager,
|
||||||
skillManagerCore,
|
skillManagerCore,
|
||||||
llmConfigStore,
|
llmConfigStore,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type { PathProvider } from '@openchatlab/core'
|
|||||||
import type {
|
import type {
|
||||||
DatabaseManager,
|
DatabaseManager,
|
||||||
AIChatManager,
|
AIChatManager,
|
||||||
|
AIMemoryService,
|
||||||
AssistantManager,
|
AssistantManager,
|
||||||
SkillManagerCore,
|
SkillManagerCore,
|
||||||
LLMConfigStore,
|
LLMConfigStore,
|
||||||
@@ -59,6 +60,7 @@ import { getServerAiLogger } from '../../../ai/logger'
|
|||||||
export interface AiContextOptions {
|
export interface AiContextOptions {
|
||||||
aiDataDir: string
|
aiDataDir: string
|
||||||
aiChatManager: AIChatManager
|
aiChatManager: AIChatManager
|
||||||
|
aiMemoryService: AIMemoryService
|
||||||
assistantManager: AssistantManager
|
assistantManager: AssistantManager
|
||||||
skillManagerCore: SkillManagerCore
|
skillManagerCore: SkillManagerCore
|
||||||
llmConfigStore: LLMConfigStore
|
llmConfigStore: LLMConfigStore
|
||||||
@@ -177,6 +179,7 @@ export function registerWebRoutes(
|
|||||||
...(ai && {
|
...(ai && {
|
||||||
aiDataDir: ai.aiDataDir,
|
aiDataDir: ai.aiDataDir,
|
||||||
aiChatManager: ai.aiChatManager,
|
aiChatManager: ai.aiChatManager,
|
||||||
|
aiMemoryService: ai.aiMemoryService,
|
||||||
assistantManager: ai.assistantManager,
|
assistantManager: ai.assistantManager,
|
||||||
skillManagerCore: ai.skillManagerCore,
|
skillManagerCore: ai.skillManagerCore,
|
||||||
llmConfigStore: ai.llmConfigStore,
|
llmConfigStore: ai.llmConfigStore,
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export async function startInternalServer(
|
|||||||
streamImport: electronStreamImport,
|
streamImport: electronStreamImport,
|
||||||
aiDataDir,
|
aiDataDir,
|
||||||
aiChatManager: getAIChatManager(),
|
aiChatManager: getAIChatManager(),
|
||||||
|
aiMemoryService: newAIMemoryService,
|
||||||
assistantManager: getAssistantManager(),
|
assistantManager: getAssistantManager(),
|
||||||
skillManagerCore: getSkillManager(),
|
skillManagerCore: getSkillManager(),
|
||||||
llmConfigStore,
|
llmConfigStore,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { AuthProfile } from '@openchatlab/config'
|
|||||||
import type {
|
import type {
|
||||||
AgentStreamChunk,
|
AgentStreamChunk,
|
||||||
AIChatManager,
|
AIChatManager,
|
||||||
|
AIMemoryService,
|
||||||
AssistantManager,
|
AssistantManager,
|
||||||
CustomModelStore,
|
CustomModelStore,
|
||||||
CustomProviderStore,
|
CustomProviderStore,
|
||||||
@@ -60,6 +61,7 @@ export interface AgentStreamRequest {
|
|||||||
export interface AiRouteContext {
|
export interface AiRouteContext {
|
||||||
aiDataDir?: string
|
aiDataDir?: string
|
||||||
aiChatManager?: AIChatManager
|
aiChatManager?: AIChatManager
|
||||||
|
aiMemoryService?: AIMemoryService
|
||||||
assistantManager?: AssistantManager
|
assistantManager?: AssistantManager
|
||||||
skillManagerCore?: SkillManagerCore
|
skillManagerCore?: SkillManagerCore
|
||||||
llmConfigStore?: LLMConfigStore
|
llmConfigStore?: LLMConfigStore
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { describe, it } from 'node:test'
|
||||||
|
import Fastify from 'fastify'
|
||||||
|
import { AIMemoryService } from '@openchatlab/node-runtime'
|
||||||
|
import { registerAiMemoryRoutes } from './ai-memories'
|
||||||
|
|
||||||
|
const sqliteNativeBinding = process.env.CHATLAB_TEST_SQLITE_NATIVE_BINDING
|
||||||
|
|
||||||
|
function createFixture() {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'chatlab-ai-memory-routes-'))
|
||||||
|
const service = new AIMemoryService(dir, { nativeBinding: sqliteNativeBinding })
|
||||||
|
const app = Fastify()
|
||||||
|
registerAiMemoryRoutes(app, { aiMemoryService: service })
|
||||||
|
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
service,
|
||||||
|
async close() {
|
||||||
|
await app.close()
|
||||||
|
service.close()
|
||||||
|
try {
|
||||||
|
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 })
|
||||||
|
} catch {
|
||||||
|
// Windows can hold SQLite WAL handles briefly after close; temp cleanup is best-effort.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AI memory routes', () => {
|
||||||
|
it('creates user memories and lists exact scopes', async () => {
|
||||||
|
const fixture = createFixture()
|
||||||
|
try {
|
||||||
|
const globalResponse = await fixture.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/_web/ai/memories',
|
||||||
|
payload: {
|
||||||
|
scopeType: 'global',
|
||||||
|
scopeId: null,
|
||||||
|
content: ' 最近默认按 90 天 ',
|
||||||
|
sourceType: 'ai',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.equal(globalResponse.statusCode, 200)
|
||||||
|
assert.equal(globalResponse.json().content, '最近默认按 90 天')
|
||||||
|
assert.equal(globalResponse.json().sourceType, 'user')
|
||||||
|
|
||||||
|
await fixture.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/_web/ai/memories',
|
||||||
|
payload: { scopeType: 'contact', scopeId: 'contact-a', content: '大学同学' },
|
||||||
|
})
|
||||||
|
await fixture.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/_web/ai/memories',
|
||||||
|
payload: { scopeType: 'contact', scopeId: 'contact-b', content: '前同事' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const all = await fixture.app.inject({ method: 'GET', url: '/_web/ai/memories' })
|
||||||
|
assert.equal(all.statusCode, 200)
|
||||||
|
assert.equal(all.json().length, 3)
|
||||||
|
|
||||||
|
const filtered = await fixture.app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/_web/ai/memories?scopeType=contact&scopeId=contact-a',
|
||||||
|
})
|
||||||
|
assert.equal(filtered.statusCode, 200)
|
||||||
|
assert.deepEqual(
|
||||||
|
filtered.json().map((entry: { content: string }) => entry.content),
|
||||||
|
['大学同学']
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
await fixture.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates, deletes, and clears memories without letting clients spoof the source', async () => {
|
||||||
|
const fixture = createFixture()
|
||||||
|
try {
|
||||||
|
const created = fixture.service.create({
|
||||||
|
scopeType: 'group',
|
||||||
|
scopeId: 'group-a',
|
||||||
|
content: '旧结论',
|
||||||
|
sourceType: 'ai',
|
||||||
|
sourceAIChatId: 'chat-a',
|
||||||
|
})
|
||||||
|
fixture.service.create({
|
||||||
|
scopeType: 'group',
|
||||||
|
scopeId: 'group-b',
|
||||||
|
content: '另一个群',
|
||||||
|
sourceType: 'user',
|
||||||
|
})
|
||||||
|
|
||||||
|
const updated = await fixture.app.inject({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/_web/ai/memories/${created.id}`,
|
||||||
|
payload: { content: '用户确认后的结论', sourceType: 'ai' },
|
||||||
|
})
|
||||||
|
assert.equal(updated.statusCode, 200)
|
||||||
|
assert.deepEqual(
|
||||||
|
{
|
||||||
|
scopeType: updated.json().scopeType,
|
||||||
|
scopeId: updated.json().scopeId,
|
||||||
|
content: updated.json().content,
|
||||||
|
sourceType: updated.json().sourceType,
|
||||||
|
sourceAIChatId: updated.json().sourceAIChatId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scopeType: 'group',
|
||||||
|
scopeId: 'group-a',
|
||||||
|
content: '用户确认后的结论',
|
||||||
|
sourceType: 'user',
|
||||||
|
sourceAIChatId: null,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const cleared = await fixture.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/_web/ai/memories/clear',
|
||||||
|
payload: { scopeType: 'group', scopeId: 'group-a' },
|
||||||
|
})
|
||||||
|
assert.equal(cleared.statusCode, 200)
|
||||||
|
assert.deepEqual(cleared.json(), { success: true, cleared: 1 })
|
||||||
|
assert.deepEqual(
|
||||||
|
fixture.service.list().map((entry) => entry.scopeId),
|
||||||
|
['group-b']
|
||||||
|
)
|
||||||
|
|
||||||
|
const remainingId = fixture.service.list()[0]?.id
|
||||||
|
assert.ok(remainingId)
|
||||||
|
const deleted = await fixture.app.inject({ method: 'DELETE', url: `/_web/ai/memories/${remainingId}` })
|
||||||
|
assert.equal(deleted.statusCode, 200)
|
||||||
|
assert.deepEqual(deleted.json(), { success: true })
|
||||||
|
|
||||||
|
const missing = await fixture.app.inject({ method: 'DELETE', url: '/_web/ai/memories/missing' })
|
||||||
|
assert.equal(missing.statusCode, 404)
|
||||||
|
} finally {
|
||||||
|
await fixture.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requires explicit scope or all confirmation before clearing', async () => {
|
||||||
|
const fixture = createFixture()
|
||||||
|
try {
|
||||||
|
fixture.service.create({ scopeType: 'global', scopeId: null, content: '先给结论', sourceType: 'user' })
|
||||||
|
fixture.service.create({
|
||||||
|
scopeType: 'contact',
|
||||||
|
scopeId: 'contact-a',
|
||||||
|
content: '大学同学',
|
||||||
|
sourceType: 'user',
|
||||||
|
})
|
||||||
|
|
||||||
|
const implicit = await fixture.app.inject({ method: 'POST', url: '/_web/ai/memories/clear', payload: {} })
|
||||||
|
assert.equal(implicit.statusCode, 400)
|
||||||
|
assert.equal(fixture.service.list().length, 2)
|
||||||
|
|
||||||
|
const ambiguous = await fixture.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/_web/ai/memories/clear',
|
||||||
|
payload: { all: true, scopeType: 'global' },
|
||||||
|
})
|
||||||
|
assert.equal(ambiguous.statusCode, 400)
|
||||||
|
assert.equal(fixture.service.list().length, 2)
|
||||||
|
|
||||||
|
const explicit = await fixture.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/_web/ai/memories/clear',
|
||||||
|
payload: { all: true },
|
||||||
|
})
|
||||||
|
assert.equal(explicit.statusCode, 200)
|
||||||
|
assert.deepEqual(explicit.json(), { success: true, cleared: 2 })
|
||||||
|
assert.deepEqual(fixture.service.list(), [])
|
||||||
|
} finally {
|
||||||
|
await fixture.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects invalid scope and content instead of changing stored memories', async () => {
|
||||||
|
const fixture = createFixture()
|
||||||
|
try {
|
||||||
|
const invalidScope = await fixture.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/_web/ai/memories',
|
||||||
|
payload: { scopeType: 'contact', content: 'missing id' },
|
||||||
|
})
|
||||||
|
assert.equal(invalidScope.statusCode, 400)
|
||||||
|
|
||||||
|
const invalidQuery = await fixture.app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/_web/ai/memories?scopeId=contact-a',
|
||||||
|
})
|
||||||
|
assert.equal(invalidQuery.statusCode, 400)
|
||||||
|
|
||||||
|
const invalidContent = await fixture.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/_web/ai/memories',
|
||||||
|
payload: { scopeType: 'global', content: ' ' },
|
||||||
|
})
|
||||||
|
assert.equal(invalidContent.statusCode, 400)
|
||||||
|
assert.deepEqual(fixture.service.list(), [])
|
||||||
|
} finally {
|
||||||
|
await fixture.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type { FastifyInstance, FastifyReply } from 'fastify'
|
||||||
|
import type { AIMemoryScope, AIMemoryScopeType } from '@openchatlab/shared-types'
|
||||||
|
import type { AiRouteContext } from '../../context/ai'
|
||||||
|
|
||||||
|
type AiMemoryRouteContext = Pick<AiRouteContext, 'aiMemoryService'>
|
||||||
|
|
||||||
|
interface MemoryScopeInput {
|
||||||
|
scopeType?: string
|
||||||
|
scopeId?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScope(input: MemoryScopeInput): AIMemoryScope {
|
||||||
|
const scopeType = input.scopeType
|
||||||
|
if (scopeType !== 'global' && scopeType !== 'contact' && scopeType !== 'group') {
|
||||||
|
throw new Error('scopeType must be global, contact, or group')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scopeType: scopeType as AIMemoryScopeType,
|
||||||
|
scopeId: typeof input.scopeId === 'string' ? input.scopeId : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendBadRequest(reply: FastifyReply, error: unknown) {
|
||||||
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerAiMemoryRoutes(server: FastifyInstance, ctx: AiMemoryRouteContext): void {
|
||||||
|
const memoryService = ctx.aiMemoryService
|
||||||
|
if (!memoryService) return
|
||||||
|
|
||||||
|
server.get<{
|
||||||
|
Querystring: MemoryScopeInput
|
||||||
|
}>('/_web/ai/memories', async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const { scopeType, scopeId } = request.query
|
||||||
|
if (!scopeType) {
|
||||||
|
if (scopeId != null) throw new Error('scopeType is required when scopeId is provided')
|
||||||
|
return memoryService.list()
|
||||||
|
}
|
||||||
|
return memoryService.list(parseScope({ scopeType, scopeId }))
|
||||||
|
} catch (error) {
|
||||||
|
return sendBadRequest(reply, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.post<{
|
||||||
|
Body: MemoryScopeInput & { content?: string; sourceType?: string }
|
||||||
|
}>('/_web/ai/memories', async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const scope = parseScope(request.body ?? {})
|
||||||
|
return memoryService.create({
|
||||||
|
...scope,
|
||||||
|
content: request.body?.content ?? '',
|
||||||
|
sourceType: 'user',
|
||||||
|
sourceAIChatId: null,
|
||||||
|
sourceMessageId: null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return sendBadRequest(reply, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.put<{
|
||||||
|
Params: { id: string }
|
||||||
|
Body: { content?: string; sourceType?: string }
|
||||||
|
}>('/_web/ai/memories/:id', async (request, reply) => {
|
||||||
|
if (!memoryService.get(request.params.id)) {
|
||||||
|
return reply.code(404).send({ error: 'AI memory not found' })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return memoryService.update(request.params.id, {
|
||||||
|
content: request.body?.content ?? '',
|
||||||
|
sourceType: 'user',
|
||||||
|
sourceAIChatId: null,
|
||||||
|
sourceMessageId: null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return sendBadRequest(reply, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.delete<{
|
||||||
|
Params: { id: string }
|
||||||
|
}>('/_web/ai/memories/:id', async (request, reply) => {
|
||||||
|
if (!memoryService.forget(request.params.id)) {
|
||||||
|
return reply.code(404).send({ error: 'AI memory not found' })
|
||||||
|
}
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
server.post<{
|
||||||
|
Body: MemoryScopeInput & { all?: boolean }
|
||||||
|
}>('/_web/ai/memories/clear', async (request, reply) => {
|
||||||
|
const { all, scopeType, scopeId } = request.body ?? {}
|
||||||
|
if (all === true) {
|
||||||
|
if (scopeType || scopeId != null) {
|
||||||
|
return reply.code(400).send({ error: 'all cannot be combined with a scope' })
|
||||||
|
}
|
||||||
|
return { success: true, cleared: memoryService.clear() }
|
||||||
|
}
|
||||||
|
if (!scopeType) {
|
||||||
|
return reply.code(400).send({ error: 'A scope or all: true is required' })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return { success: true, cleared: memoryService.clear(parseScope({ scopeType, scopeId })) }
|
||||||
|
} catch (error) {
|
||||||
|
return sendBadRequest(reply, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import Fastify from 'fastify'
|
|||||||
import type { PathProvider } from '@openchatlab/core'
|
import type { PathProvider } from '@openchatlab/core'
|
||||||
import type {
|
import type {
|
||||||
AIChatManager,
|
AIChatManager,
|
||||||
|
AIMemoryService,
|
||||||
AssistantManager,
|
AssistantManager,
|
||||||
CustomModelStore,
|
CustomModelStore,
|
||||||
CustomProviderStore,
|
CustomProviderStore,
|
||||||
@@ -28,6 +29,7 @@ function completeContext(): AiRoutesContext {
|
|||||||
...baseContext(),
|
...baseContext(),
|
||||||
aiDataDir: '/tmp/chatlab-ai-route-test',
|
aiDataDir: '/tmp/chatlab-ai-route-test',
|
||||||
aiChatManager: {} as AIChatManager,
|
aiChatManager: {} as AIChatManager,
|
||||||
|
aiMemoryService: {} as AIMemoryService,
|
||||||
assistantManager: {} as AssistantManager,
|
assistantManager: {} as AssistantManager,
|
||||||
skillManagerCore: {} as SkillManagerCore,
|
skillManagerCore: {} as SkillManagerCore,
|
||||||
llmConfigStore: {} as LLMConfigStore,
|
llmConfigStore: {} as LLMConfigStore,
|
||||||
@@ -42,6 +44,7 @@ test('registerAiRoutes identifies each missing required dependency', () => {
|
|||||||
const requiredDependencies = [
|
const requiredDependencies = [
|
||||||
'aiDataDir',
|
'aiDataDir',
|
||||||
'aiChatManager',
|
'aiChatManager',
|
||||||
|
'aiMemoryService',
|
||||||
'assistantManager',
|
'assistantManager',
|
||||||
'skillManagerCore',
|
'skillManagerCore',
|
||||||
'llmConfigStore',
|
'llmConfigStore',
|
||||||
@@ -72,6 +75,9 @@ test('registerAiRoutes keeps static and graceful fallback routes without AI mana
|
|||||||
const assistants = await app.inject({ method: 'GET', url: '/_web/ai/assistants' })
|
const assistants = await app.inject({ method: 'GET', url: '/_web/ai/assistants' })
|
||||||
assert.equal(assistants.statusCode, 404)
|
assert.equal(assistants.statusCode, 404)
|
||||||
|
|
||||||
|
const memories = await app.inject({ method: 'GET', url: '/_web/ai/memories' })
|
||||||
|
assert.equal(memories.statusCode, 404)
|
||||||
|
|
||||||
const semanticStatus = await app.inject({ method: 'GET', url: '/_web/ai/semantic-index/status' })
|
const semanticStatus = await app.inject({ method: 'GET', url: '/_web/ai/semantic-index/status' })
|
||||||
assert.equal(semanticStatus.statusCode, 200)
|
assert.equal(semanticStatus.statusCode, 200)
|
||||||
assert.deepEqual(semanticStatus.json(), { status: null })
|
assert.deepEqual(semanticStatus.json(), { status: null })
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { registerAiChatTopicRoutes } from './ai-chat-topics'
|
|||||||
import { registerAiLlmStreamRoutes } from './ai-llm-stream'
|
import { registerAiLlmStreamRoutes } from './ai-llm-stream'
|
||||||
import { registerAiLlmRoutes } from './ai-llm'
|
import { registerAiLlmRoutes } from './ai-llm'
|
||||||
import { registerAiLogRoutes } from './ai-logs'
|
import { registerAiLogRoutes } from './ai-logs'
|
||||||
|
import { registerAiMemoryRoutes } from './ai-memories'
|
||||||
import { registerSemanticIndexRoutes } from './ai-semantic-index'
|
import { registerSemanticIndexRoutes } from './ai-semantic-index'
|
||||||
import { registerAiSkillRoutes } from './ai-skills'
|
import { registerAiSkillRoutes } from './ai-skills'
|
||||||
import { registerAiSummaryRoutes } from './ai-summaries'
|
import { registerAiSummaryRoutes } from './ai-summaries'
|
||||||
@@ -26,6 +27,7 @@ export interface AiRouteOptions {
|
|||||||
const REQUIRED_AI_DEPENDENCIES = [
|
const REQUIRED_AI_DEPENDENCIES = [
|
||||||
'aiDataDir',
|
'aiDataDir',
|
||||||
'aiChatManager',
|
'aiChatManager',
|
||||||
|
'aiMemoryService',
|
||||||
'assistantManager',
|
'assistantManager',
|
||||||
'skillManagerCore',
|
'skillManagerCore',
|
||||||
'llmConfigStore',
|
'llmConfigStore',
|
||||||
@@ -52,6 +54,7 @@ export function registerAiRoutes(server: FastifyInstance, ctx: AiRoutesContext,
|
|||||||
registerAiAgentStreamRoutes(server, ctx)
|
registerAiAgentStreamRoutes(server, ctx)
|
||||||
registerAiToolRoutes(server, ctx)
|
registerAiToolRoutes(server, ctx)
|
||||||
registerAiChatRoutes(server, ctx)
|
registerAiChatRoutes(server, ctx)
|
||||||
|
registerAiMemoryRoutes(server, ctx)
|
||||||
registerAiChatTopicRoutes(server, ctx)
|
registerAiChatTopicRoutes(server, ctx)
|
||||||
registerAiSummaryRoutes(server, ctx)
|
registerAiSummaryRoutes(server, ctx)
|
||||||
registerAiLogRoutes(server, ctx)
|
registerAiLogRoutes(server, ctx)
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import type {
|
|||||||
ToolCatalogEntry,
|
ToolCatalogEntry,
|
||||||
ToolExecuteResult,
|
ToolExecuteResult,
|
||||||
DesensitizeRule,
|
DesensitizeRule,
|
||||||
|
AIMemoryEntry,
|
||||||
|
AIMemoryScope,
|
||||||
|
CreateUserAIMemoryInput,
|
||||||
|
ClearAIMemoriesInput,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { get, post, put, del, fetchWithAuth } from '../utils/http'
|
import { get, post, put, del, fetchWithAuth } from '../utils/http'
|
||||||
|
|
||||||
@@ -59,6 +63,30 @@ export class FetchAIAdapter implements AIAdapter {
|
|||||||
return del<boolean>(`/ai/chats/${aiChatId}`)
|
return del<boolean>(`/ai/chats/${aiChatId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 长期记忆 =====
|
||||||
|
async getAIMemories(scope?: AIMemoryScope): Promise<AIMemoryEntry[]> {
|
||||||
|
if (!scope) return get<AIMemoryEntry[]>('/ai/memories')
|
||||||
|
const params = new URLSearchParams({ scopeType: scope.scopeType })
|
||||||
|
if (scope.scopeId) params.set('scopeId', scope.scopeId)
|
||||||
|
return get<AIMemoryEntry[]>(`/ai/memories?${params.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAIMemory(input: CreateUserAIMemoryInput): Promise<AIMemoryEntry> {
|
||||||
|
return post<AIMemoryEntry>('/ai/memories', input)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAIMemory(id: string, content: string): Promise<AIMemoryEntry> {
|
||||||
|
return put<AIMemoryEntry>(`/ai/memories/${id}`, { content })
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAIMemory(id: string): Promise<{ success: boolean }> {
|
||||||
|
return del<{ success: boolean }>(`/ai/memories/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAIMemories(input: ClearAIMemoriesInput): Promise<{ success: boolean; cleared: number }> {
|
||||||
|
return post<{ success: boolean; cleared: number }>('/ai/memories/clear', input)
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 消息 =====
|
// ===== 消息 =====
|
||||||
async getMessages(aiChatId: string): Promise<AIMessage[]> {
|
async getMessages(aiChatId: string): Promise<AIMessage[]> {
|
||||||
return get<AIMessage[]>(`/ai/chats/${aiChatId}/messages`)
|
return get<AIMessage[]>(`/ai/chats/${aiChatId}/messages`)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { TimeFilter } from '@/types/base'
|
import type { TimeFilter } from '@/types/base'
|
||||||
import type { ChartPayload, ChatEvidencePayload } from '@openchatlab/core'
|
import type { ChartPayload, ChatEvidencePayload } from '@openchatlab/core'
|
||||||
import type { AIEntityRef, CrossChatEvidencePayload } from '@openchatlab/shared-types'
|
import type { AIEntityRef, AIMemoryEntry, AIMemoryScope, CrossChatEvidencePayload } from '@openchatlab/shared-types'
|
||||||
import type { PlanContentBlock } from './planBlocks'
|
import type { PlanContentBlock } from './planBlocks'
|
||||||
|
|
||||||
export interface AIChat {
|
export interface AIChat {
|
||||||
@@ -14,7 +14,10 @@ export interface AIChat {
|
|||||||
updatedAt: number
|
updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { AIEntityRef } from '@openchatlab/shared-types'
|
export type { AIEntityRef, AIMemoryEntry, AIMemoryScope } from '@openchatlab/shared-types'
|
||||||
|
|
||||||
|
export type CreateUserAIMemoryInput = AIMemoryScope & { content: string }
|
||||||
|
export type ClearAIMemoriesInput = AIMemoryScope | { all: true }
|
||||||
|
|
||||||
export type ContentBlock =
|
export type ContentBlock =
|
||||||
| { type: 'text'; text: string; processDurationMs?: number }
|
| { type: 'text'; text: string; processDurationMs?: number }
|
||||||
@@ -129,6 +132,13 @@ export interface AIAdapter {
|
|||||||
updateAIChatTitle(aiChatId: string, title: string): Promise<boolean>
|
updateAIChatTitle(aiChatId: string, title: string): Promise<boolean>
|
||||||
deleteAIChat(aiChatId: string): Promise<boolean>
|
deleteAIChat(aiChatId: string): Promise<boolean>
|
||||||
|
|
||||||
|
// ===== 长期记忆 =====
|
||||||
|
getAIMemories(scope?: AIMemoryScope): Promise<AIMemoryEntry[]>
|
||||||
|
createAIMemory(input: CreateUserAIMemoryInput): Promise<AIMemoryEntry>
|
||||||
|
updateAIMemory(id: string, content: string): Promise<AIMemoryEntry>
|
||||||
|
deleteAIMemory(id: string): Promise<{ success: boolean }>
|
||||||
|
clearAIMemories(input: ClearAIMemoriesInput): Promise<{ success: boolean; cleared: number }>
|
||||||
|
|
||||||
// ===== 消息 =====
|
// ===== 消息 =====
|
||||||
getMessages(aiChatId: string): Promise<AIMessage[]>
|
getMessages(aiChatId: string): Promise<AIMessage[]>
|
||||||
addMessage(
|
addMessage(
|
||||||
|
|||||||
Reference in new issue
Block a user