mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat(memory): add Spaces support, platform/group annotation, and bot name to Nowledge Mem
- Use Nowledge Mem Spaces for per-bot memory isolation (space name: memoh:{botID})
- Auto-ensure space on first use with sync.Map cache
- Add platform/conversation context header to stored text: (Telegram 群组「开发讨论」)
- Replace [我] with bot's actual display name [小助手]
- Thread ConversationType, ConversationName, Platform, BotName through AfterChatRequest
- Add resolveBotDisplayName to resolver for DB lookup
This commit is contained in:
+22
-5
@@ -60,19 +60,25 @@ Nowledge Mem 是单用户个人知识库,没有 `user_id` 或 `group_id` 概
|
|||||||
|
|
||||||
### 存储文本格式
|
### 存储文本格式
|
||||||
|
|
||||||
每条记忆对应一轮对话(用户消息 + bot 回复):
|
每条记忆对应一轮对话(用户消息 + bot 回复),头部标注来源上下文:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
(Telegram 群组「开发讨论」)
|
||||||
[张三] 我最近在用 Rust 重写后端
|
[张三] 我最近在用 Rust 重写后端
|
||||||
[我] 很好的选择,Rust 的性能和安全性都很出色
|
[小助手] 很好的选择,Rust 的性能和安全性都很出色
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **头部标注**:`({Platform} {会话类型}「{群组名}」)`
|
||||||
|
- Platform:从 `CurrentChannel` 取值(telegram / feishu / discord 等),首字母大写
|
||||||
|
- 会话类型映射:`group` → `群组`,`private` → `私聊`,`thread` → `话题`
|
||||||
|
- 群组名:有则加 `「...」`,无则省略(私聊一般没有群组名)
|
||||||
|
- 示例:`(Telegram 私聊)` 或 `(Feishu 群组「开发讨论」)`
|
||||||
- **用户消息**:`[{display-name}] {消息内容}`
|
- **用户消息**:`[{display-name}] {消息内容}`
|
||||||
- display-name 从 YAML front-matter header 中解析(Memoh 的 user_header.go 在每条用户消息中嵌入了 `display-name` 字段)
|
- display-name 从 YAML front-matter header 中解析(Memoh 的 user_header.go 在每条用户消息中嵌入了 `display-name` 字段)
|
||||||
- 回退链:YAML header → AfterChatRequest.DisplayName → `"用户"`
|
- 回退链:YAML header → AfterChatRequest.DisplayName → `"用户"`
|
||||||
- **Bot 消息**:`[我] {消息内容}`
|
- **Bot 消息**:`[{bot-display-name}] {消息内容}`
|
||||||
- LLM 读到 `[我]` 时自然理解为"这是我之前说过的话"
|
- 使用 bot 的 display name(从 bots 表的 `display_name` 字段取)
|
||||||
- `我` 不是命名实体,不会在知识图谱中产生误导节点
|
- Nowledge Mem 的实体抽取会将 bot 名识别为 Person 实体
|
||||||
|
|
||||||
### 为什么不用 `(@username)` 双标识
|
### 为什么不用 `(@username)` 双标识
|
||||||
|
|
||||||
@@ -87,6 +93,17 @@ AfterChatRequest 中只有 `DisplayName`(人类可读名)和内部 UUID(Us
|
|||||||
|
|
||||||
不需要在查询时拼接发言人信息。
|
不需要在查询时拼接发言人信息。
|
||||||
|
|
||||||
|
### Spaces 隔离
|
||||||
|
|
||||||
|
利用 Nowledge Mem 的 Spaces 功能实现 per-bot 记忆隔离:
|
||||||
|
|
||||||
|
- 每个 bot 自动映射到一个 Space,名称为 `memoh:{botID}`(botID 是稳定的 UUID)
|
||||||
|
- 首次使用时自动 ensure(`GET /spaces` 查找 → 未找到则 `POST /spaces` 创建)
|
||||||
|
- `sync.Map` 缓存 `botID → spaceID` 映射,避免重复 API 调用
|
||||||
|
- 所有 `POST /memories`、`POST /memories/search` 调用带 `space_id` 参数
|
||||||
|
- 不同 bot 的记忆完全隔离,搜索不互相干扰
|
||||||
|
- Entity graph 保持全局(Nowledge Mem 设计如此),跨 bot 的实体关联仍可用
|
||||||
|
|
||||||
### 上下文注入格式
|
### 上下文注入格式
|
||||||
|
|
||||||
与现有 provider 一致的 `<memory-context>` XML 格式:
|
与现有 provider 一致的 `<memory-context>` XML 格式:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/memohai/memoh/internal/conversation"
|
"github.com/memohai/memoh/internal/conversation"
|
||||||
|
"github.com/memohai/memoh/internal/db"
|
||||||
memprovider "github.com/memohai/memoh/internal/memory/adapters"
|
memprovider "github.com/memohai/memoh/internal/memory/adapters"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,11 +78,33 @@ func (r *Resolver) storeMemory(ctx context.Context, req conversation.ChatRequest
|
|||||||
ChannelIdentityID: strings.TrimSpace(req.SourceChannelIdentityID),
|
ChannelIdentityID: strings.TrimSpace(req.SourceChannelIdentityID),
|
||||||
DisplayName: r.resolveDisplayName(ctx, req),
|
DisplayName: r.resolveDisplayName(ctx, req),
|
||||||
TimezoneLocation: tzLoc,
|
TimezoneLocation: tzLoc,
|
||||||
|
ConversationType: strings.TrimSpace(req.ConversationType),
|
||||||
|
ConversationName: strings.TrimSpace(req.ConversationName),
|
||||||
|
Platform: strings.TrimSpace(req.CurrentChannel),
|
||||||
|
BotName: r.resolveBotDisplayName(ctx, botID),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
r.logger.Warn("memory provider OnAfterChat failed", slog.String("bot_id", botID), slog.Any("error", err))
|
r.logger.Warn("memory provider OnAfterChat failed", slog.String("bot_id", botID), slog.Any("error", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) resolveBotDisplayName(ctx context.Context, botID string) string {
|
||||||
|
if r.queries == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
botUUID, err := db.ParseUUID(botID)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
row, err := r.queries.GetBotByID(ctx, botUUID)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if row.DisplayName.Valid {
|
||||||
|
return strings.TrimSpace(row.DisplayName.String)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func toProviderMessages(messages []conversation.ModelMessage) []memprovider.Message {
|
func toProviderMessages(messages []conversation.ModelMessage) []memprovider.Message {
|
||||||
out := make([]memprovider.Message, 0, len(messages))
|
out := make([]memprovider.Message, 0, len(messages))
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ func newNmemClient(config map[string]any) (*nmemClient, error) {
|
|||||||
type nmemMemoryCreateRequest struct {
|
type nmemMemoryCreateRequest struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Source string `json:"source,omitempty"`
|
Source string `json:"source,omitempty"`
|
||||||
|
SpaceID string `json:"space_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type nmemMemoryUpdateRequest struct {
|
type nmemMemoryUpdateRequest struct {
|
||||||
@@ -52,6 +53,7 @@ type nmemSearchRequest struct {
|
|||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
Limit int `json:"limit,omitempty"`
|
Limit int `json:"limit,omitempty"`
|
||||||
IncludeEntities bool `json:"include_entities,omitempty"`
|
IncludeEntities bool `json:"include_entities,omitempty"`
|
||||||
|
SpaceID string `json:"space_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type nmemMemory struct {
|
type nmemMemory struct {
|
||||||
@@ -80,25 +82,43 @@ type nmemDeleteResponse struct {
|
|||||||
DeletedEntities int `json:"deleted_entities,omitempty"`
|
DeletedEntities int `json:"deleted_entities,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Space types ---
|
||||||
|
|
||||||
|
type nmemSpaceCreateRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type nmemSpace struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type nmemSpacesResponse struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Spaces []nmemSpace `json:"spaces"`
|
||||||
|
}
|
||||||
|
|
||||||
// --- Client methods ---
|
// --- Client methods ---
|
||||||
|
|
||||||
func (c *nmemClient) addMemory(ctx context.Context, content, source string) (*nmemMemory, error) {
|
func (c *nmemClient) addMemory(ctx context.Context, content, source, spaceID string) (*nmemMemory, error) {
|
||||||
var result nmemMemory
|
var result nmemMemory
|
||||||
if err := c.doJSON(ctx, http.MethodPost, "/memories", nmemMemoryCreateRequest{
|
if err := c.doJSON(ctx, http.MethodPost, "/memories", nmemMemoryCreateRequest{
|
||||||
Content: content,
|
Content: content,
|
||||||
Source: source,
|
Source: source,
|
||||||
|
SpaceID: spaceID,
|
||||||
}, &result); err != nil {
|
}, &result); err != nil {
|
||||||
return nil, fmt.Errorf("nowledgemem add: %w", err)
|
return nil, fmt.Errorf("nowledgemem add: %w", err)
|
||||||
}
|
}
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *nmemClient) searchMemories(ctx context.Context, query string, limit int) ([]nmemSearchResult, error) {
|
func (c *nmemClient) searchMemories(ctx context.Context, query string, limit int, spaceID string) ([]nmemSearchResult, error) {
|
||||||
var result nmemSearchResponse
|
var result nmemSearchResponse
|
||||||
if err := c.doJSON(ctx, http.MethodPost, "/memories/search", nmemSearchRequest{
|
if err := c.doJSON(ctx, http.MethodPost, "/memories/search", nmemSearchRequest{
|
||||||
Query: query,
|
Query: query,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
IncludeEntities: true,
|
IncludeEntities: true,
|
||||||
|
SpaceID: spaceID,
|
||||||
}, &result); err != nil {
|
}, &result); err != nil {
|
||||||
return nil, fmt.Errorf("nowledgemem search: %w", err)
|
return nil, fmt.Errorf("nowledgemem search: %w", err)
|
||||||
}
|
}
|
||||||
@@ -123,6 +143,51 @@ func (c *nmemClient) deleteMemory(ctx context.Context, id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *nmemClient) listSpaces(ctx context.Context) ([]nmemSpace, error) {
|
||||||
|
var result nmemSpacesResponse
|
||||||
|
if err := c.doJSON(ctx, http.MethodGet, "/spaces", nil, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("nowledgemem list spaces: %w", err)
|
||||||
|
}
|
||||||
|
return result.Spaces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nmemClient) createSpace(ctx context.Context, name string) (*nmemSpace, error) {
|
||||||
|
var result nmemSpacesResponse
|
||||||
|
if err := c.doJSON(ctx, http.MethodPost, "/spaces", nmemSpaceCreateRequest{
|
||||||
|
Name: name,
|
||||||
|
}, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("nowledgemem create space: %w", err)
|
||||||
|
}
|
||||||
|
for _, s := range result.Spaces {
|
||||||
|
if s.Name == name {
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result.Spaces) > 0 {
|
||||||
|
last := result.Spaces[len(result.Spaces)-1]
|
||||||
|
return &last, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("nowledgemem create space: space %q not found in response", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureSpace finds an existing space by name or creates it. Returns the space ID.
|
||||||
|
func (c *nmemClient) ensureSpace(ctx context.Context, name string) (string, error) {
|
||||||
|
spaces, err := c.listSpaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, s := range spaces {
|
||||||
|
if s.Name == name {
|
||||||
|
return s.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
created, err := c.createSpace(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return created.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// --- HTTP helper ---
|
// --- HTTP helper ---
|
||||||
|
|
||||||
func (c *nmemClient) doJSON(ctx context.Context, method, urlPath string, body any, result any) error {
|
func (c *nmemClient) doJSON(ctx context.Context, method, urlPath string, body any, result any) error {
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package nowledgemem
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/memohai/memoh/internal/mcp"
|
"github.com/memohai/memoh/internal/mcp"
|
||||||
adapters "github.com/memohai/memoh/internal/memory/adapters"
|
adapters "github.com/memohai/memoh/internal/memory/adapters"
|
||||||
@@ -25,6 +28,7 @@ const (
|
|||||||
type NowledgeMemProvider struct {
|
type NowledgeMemProvider struct {
|
||||||
client *nmemClient
|
client *nmemClient
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
spaceIDs sync.Map // botID → spaceID cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNowledgeMemProvider(log *slog.Logger, config map[string]any) (*NowledgeMemProvider, error) {
|
func NewNowledgeMemProvider(log *slog.Logger, config map[string]any) (*NowledgeMemProvider, error) {
|
||||||
@@ -50,7 +54,12 @@ func (p *NowledgeMemProvider) OnBeforeChat(ctx context.Context, req adapters.Bef
|
|||||||
if query == "" {
|
if query == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
results, err := p.client.searchMemories(ctx, query, nmemContextMaxItems)
|
spaceID, err := p.resolveSpaceID(ctx, req.BotID)
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Warn("nowledgemem resolve space failed", slog.Any("error", err))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
results, err := p.client.searchMemories(ctx, query, nmemContextMaxItems, spaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Warn("nowledgemem search for context failed", slog.Any("error", err))
|
p.logger.Warn("nowledgemem search for context failed", slog.Any("error", err))
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -91,11 +100,16 @@ func (p *NowledgeMemProvider) OnAfterChat(ctx context.Context, req adapters.Afte
|
|||||||
if len(req.Messages) == 0 {
|
if len(req.Messages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
content := formatConversation(req.Messages, req.DisplayName)
|
content := formatConversation(req.Messages, req.DisplayName, req.BotName, req.Platform, req.ConversationType, req.ConversationName)
|
||||||
if content == "" {
|
if content == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, err := p.client.addMemory(ctx, content, nmemSource); err != nil {
|
spaceID, err := p.resolveSpaceID(ctx, req.BotID)
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Warn("nowledgemem resolve space failed", slog.Any("error", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := p.client.addMemory(ctx, content, nmemSource, spaceID); err != nil {
|
||||||
p.logger.Warn("nowledgemem store memory failed", slog.Any("error", err))
|
p.logger.Warn("nowledgemem store memory failed", slog.Any("error", err))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -126,7 +140,7 @@ func (*NowledgeMemProvider) ListTools(_ context.Context, _ mcp.ToolSessionContex
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NowledgeMemProvider) CallTool(ctx context.Context, _ mcp.ToolSessionContext, toolName string, arguments map[string]any) (map[string]any, error) {
|
func (p *NowledgeMemProvider) CallTool(ctx context.Context, session mcp.ToolSessionContext, toolName string, arguments map[string]any) (map[string]any, error) {
|
||||||
if toolName != nmemToolSearchMemory {
|
if toolName != nmemToolSearchMemory {
|
||||||
return nil, mcp.ErrToolNotFound
|
return nil, mcp.ErrToolNotFound
|
||||||
}
|
}
|
||||||
@@ -147,7 +161,12 @@ func (p *NowledgeMemProvider) CallTool(ctx context.Context, _ mcp.ToolSessionCon
|
|||||||
limit = nmemMaxLimit
|
limit = nmemMaxLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := p.client.searchMemories(ctx, query, limit)
|
spaceID, err := p.resolveSpaceID(ctx, session.BotID)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.BuildToolErrorResult(fmt.Sprintf("resolve space failed: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := p.client.searchMemories(ctx, query, limit, spaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.BuildToolErrorResult("memory search failed"), nil
|
return mcp.BuildToolErrorResult("memory search failed"), nil
|
||||||
}
|
}
|
||||||
@@ -171,12 +190,16 @@ func (p *NowledgeMemProvider) CallTool(ctx context.Context, _ mcp.ToolSessionCon
|
|||||||
func (p *NowledgeMemProvider) Add(ctx context.Context, req adapters.AddRequest) (adapters.SearchResponse, error) {
|
func (p *NowledgeMemProvider) Add(ctx context.Context, req adapters.AddRequest) (adapters.SearchResponse, error) {
|
||||||
text := strings.TrimSpace(req.Message)
|
text := strings.TrimSpace(req.Message)
|
||||||
if text == "" && len(req.Messages) > 0 {
|
if text == "" && len(req.Messages) > 0 {
|
||||||
text = formatConversation(req.Messages, "")
|
text = formatConversation(req.Messages, "", "", "", "", "")
|
||||||
}
|
}
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return adapters.SearchResponse{}, errors.New("message is required")
|
return adapters.SearchResponse{}, errors.New("message is required")
|
||||||
}
|
}
|
||||||
mem, err := p.client.addMemory(ctx, text, nmemSource)
|
spaceID, err := p.resolveSpaceID(ctx, req.BotID)
|
||||||
|
if err != nil {
|
||||||
|
return adapters.SearchResponse{}, fmt.Errorf("resolve space: %w", err)
|
||||||
|
}
|
||||||
|
mem, err := p.client.addMemory(ctx, text, nmemSource, spaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return adapters.SearchResponse{}, err
|
return adapters.SearchResponse{}, err
|
||||||
}
|
}
|
||||||
@@ -190,7 +213,11 @@ func (p *NowledgeMemProvider) Search(ctx context.Context, req adapters.SearchReq
|
|||||||
} else if limit > nmemMaxLimit {
|
} else if limit > nmemMaxLimit {
|
||||||
limit = nmemMaxLimit
|
limit = nmemMaxLimit
|
||||||
}
|
}
|
||||||
results, err := p.client.searchMemories(ctx, req.Query, limit)
|
spaceID, err := p.resolveSpaceID(ctx, req.BotID)
|
||||||
|
if err != nil {
|
||||||
|
return adapters.SearchResponse{}, fmt.Errorf("resolve space: %w", err)
|
||||||
|
}
|
||||||
|
results, err := p.client.searchMemories(ctx, req.Query, limit, spaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return adapters.SearchResponse{}, err
|
return adapters.SearchResponse{}, err
|
||||||
}
|
}
|
||||||
@@ -251,11 +278,40 @@ func (*NowledgeMemProvider) Usage(_ context.Context, _ map[string]any) (adapters
|
|||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
|
const nmemSpacePrefix = "memoh:"
|
||||||
|
|
||||||
|
// resolveSpaceID returns the Nowledge Mem space ID for a given bot.
|
||||||
|
// It caches the mapping in a sync.Map to avoid repeated API calls.
|
||||||
|
func (p *NowledgeMemProvider) resolveSpaceID(ctx context.Context, botID string) (string, error) {
|
||||||
|
botID = strings.TrimSpace(botID)
|
||||||
|
if botID == "" {
|
||||||
|
return "", errors.New("botID is required for space resolution")
|
||||||
|
}
|
||||||
|
if cached, ok := p.spaceIDs.Load(botID); ok {
|
||||||
|
return cached.(string), nil
|
||||||
|
}
|
||||||
|
spaceName := nmemSpacePrefix + botID
|
||||||
|
spaceID, err := p.client.ensureSpace(ctx, spaceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ensure space %q: %w", spaceName, err)
|
||||||
|
}
|
||||||
|
p.spaceIDs.Store(botID, spaceID)
|
||||||
|
return spaceID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// formatConversation formats a round of messages into attributed text for storage.
|
// formatConversation formats a round of messages into attributed text for storage.
|
||||||
// User messages are tagged with the sender's display name parsed from the YAML
|
// User messages are tagged with the sender's display name parsed from the YAML
|
||||||
// front-matter header; bot messages are tagged with [我].
|
// front-matter header; bot messages are tagged with [botName].
|
||||||
func formatConversation(messages []adapters.Message, fallbackDisplayName string) string {
|
// A header line annotates platform and conversation context.
|
||||||
|
func formatConversation(messages []adapters.Message, fallbackDisplayName, botName, platform, convType, convName string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
|
// Header annotation: (Platform convType「convName」)
|
||||||
|
header := buildContextHeader(platform, convType, convName)
|
||||||
|
if header != "" {
|
||||||
|
sb.WriteString(header)
|
||||||
|
}
|
||||||
|
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
text := strings.TrimSpace(msg.Content)
|
text := strings.TrimSpace(msg.Content)
|
||||||
if text == "" {
|
if text == "" {
|
||||||
@@ -288,13 +344,61 @@ func formatConversation(messages []adapters.Message, fallbackDisplayName string)
|
|||||||
if sb.Len() > 0 {
|
if sb.Len() > 0 {
|
||||||
sb.WriteByte('\n')
|
sb.WriteByte('\n')
|
||||||
}
|
}
|
||||||
sb.WriteString("[我] ")
|
sb.WriteString("[")
|
||||||
|
sb.WriteString(botName)
|
||||||
|
sb.WriteString("] ")
|
||||||
sb.WriteString(text)
|
sb.WriteString(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildContextHeader produces the platform/conversation annotation line.
|
||||||
|
// Format: (Platform convType「convName」)
|
||||||
|
func buildContextHeader(platform, convType, convName string) string {
|
||||||
|
platform = strings.TrimSpace(platform)
|
||||||
|
convType = strings.TrimSpace(convType)
|
||||||
|
convName = strings.TrimSpace(convName)
|
||||||
|
|
||||||
|
platformDisplay := capitalizeFirst(platform)
|
||||||
|
convTypeDisplay := mapConversationType(convType)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("(")
|
||||||
|
sb.WriteString(platformDisplay)
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(convTypeDisplay)
|
||||||
|
if convName != "" {
|
||||||
|
sb.WriteString("「")
|
||||||
|
sb.WriteString(convName)
|
||||||
|
sb.WriteString("」")
|
||||||
|
}
|
||||||
|
sb.WriteString(")")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapConversationType(t string) string {
|
||||||
|
switch t {
|
||||||
|
case "group":
|
||||||
|
return "群组"
|
||||||
|
case "private":
|
||||||
|
return "私聊"
|
||||||
|
case "thread":
|
||||||
|
return "话题"
|
||||||
|
default:
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func capitalizeFirst(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
runes := []rune(s)
|
||||||
|
runes[0] = unicode.ToUpper(runes[0])
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
// parseDisplayNameFromYAML extracts the display-name field from a YAML
|
// parseDisplayNameFromYAML extracts the display-name field from a YAML
|
||||||
// front-matter header (---\n...\n---\n) and returns the remaining body.
|
// front-matter header (---\n...\n---\n) and returns the remaining body.
|
||||||
// If no valid header is found, displayName is empty and body is the original text.
|
// If no valid header is found, displayName is empty and body is the original text.
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ type AfterChatRequest struct {
|
|||||||
ChannelIdentityID string
|
ChannelIdentityID string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
TimezoneLocation *time.Location
|
TimezoneLocation *time.Location
|
||||||
|
ConversationType string // "private", "group", "thread"
|
||||||
|
ConversationName string // group name (set for group/thread conversations)
|
||||||
|
Platform string // "telegram", "feishu", "discord", etc.
|
||||||
|
BotName string // bot's display name
|
||||||
}
|
}
|
||||||
|
|
||||||
// LLM is the interface for LLM operations needed by memory service.
|
// LLM is the interface for LLM operations needed by memory service.
|
||||||
|
|||||||
Reference in New Issue
Block a user