mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-07-03 14:09:06 +02:00
17ffd7bf7c
Signed-off-by: Daniel <845765@qq.com>
357 lines
8.2 KiB
Go
357 lines
8.2 KiB
Go
// SiYuan - Refactor your thinking
|
|
// Copyright (c) 2020-present, b3log.org
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package agent
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/88250/gulu"
|
|
"github.com/88250/lute/ast"
|
|
"github.com/siyuan-note/filelock"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
func isValidSessionID(id string) bool {
|
|
return ast.IsNodeIDPattern(id)
|
|
}
|
|
|
|
var indexMu sync.Mutex
|
|
|
|
func sessionsIndexPath() string {
|
|
return filepath.Join(util.DataDir, "storage", "ai", "agent", "sessions", "index.json")
|
|
}
|
|
|
|
func sessionsDir() string {
|
|
return filepath.Join(util.DataDir, "storage", "ai", "agent", "sessions")
|
|
}
|
|
|
|
type SessionIndexItem struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
UpdatedAt int64 `json:"updatedAt"`
|
|
}
|
|
|
|
type SessionListResult struct {
|
|
Sessions []*SessionIndexItem `json:"sessions"`
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pageSize"`
|
|
}
|
|
|
|
type sessionMeta struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
UpdatedAt int64 `json:"updatedAt"`
|
|
}
|
|
|
|
func loadSessionIndex() map[string]*SessionIndexItem {
|
|
data, err := os.ReadFile(sessionsIndexPath())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var index map[string]*SessionIndexItem
|
|
if gulu.JSON.UnmarshalJSON(data, &index) != nil {
|
|
return nil
|
|
}
|
|
return index
|
|
}
|
|
|
|
func saveSessionIndex(index map[string]*SessionIndexItem) {
|
|
data, err := gulu.JSON.MarshalIndentJSON(index, "", "\t")
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = os.MkdirAll(filepath.Dir(sessionsIndexPath()), 0755)
|
|
if err := filelock.WriteFile(sessionsIndexPath(), data); err != nil {
|
|
logging.LogErrorf("save session index failed: %s", err)
|
|
}
|
|
}
|
|
|
|
func rebuildSessionIndex() map[string]*SessionIndexItem {
|
|
entries, err := os.ReadDir(sessionsDir())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
index := map[string]*SessionIndexItem{}
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
id := entry.Name()
|
|
sessionPath := filepath.Join(sessionsDir(), id, "session.json")
|
|
sessionData, err := os.ReadFile(sessionPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var meta sessionMeta
|
|
if gulu.JSON.UnmarshalJSON(sessionData, &meta) != nil || meta.ID == "" {
|
|
continue
|
|
}
|
|
title := meta.Title
|
|
if title == "" {
|
|
title = "AI Agent"
|
|
}
|
|
index[id] = &SessionIndexItem{
|
|
ID: meta.ID,
|
|
Title: title,
|
|
CreatedAt: meta.CreatedAt,
|
|
UpdatedAt: meta.UpdatedAt,
|
|
}
|
|
}
|
|
saveSessionIndex(index)
|
|
return index
|
|
}
|
|
|
|
func UpdateSessionIndex(id, title string, createdAt, updatedAt int64) {
|
|
if id == "" {
|
|
return
|
|
}
|
|
indexMu.Lock()
|
|
defer indexMu.Unlock()
|
|
|
|
index := loadSessionIndex()
|
|
if index == nil {
|
|
index = map[string]*SessionIndexItem{}
|
|
}
|
|
if title == "" {
|
|
title = "AI Agent"
|
|
}
|
|
index[id] = &SessionIndexItem{
|
|
ID: id,
|
|
Title: title,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
saveSessionIndex(index)
|
|
}
|
|
|
|
func ListSessions(page, pageSize int, keyword string) *SessionListResult {
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = 30
|
|
}
|
|
|
|
indexMu.Lock()
|
|
index := loadSessionIndex()
|
|
if index == nil || len(index) == 0 {
|
|
index = rebuildSessionIndex()
|
|
}
|
|
if index == nil {
|
|
indexMu.Unlock()
|
|
return &SessionListResult{
|
|
Sessions: []*SessionIndexItem{},
|
|
Total: 0,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}
|
|
}
|
|
snapshot := make(map[string]*SessionIndexItem, len(index))
|
|
for k, v := range index {
|
|
snapshot[k] = v
|
|
}
|
|
indexMu.Unlock()
|
|
|
|
entries, err := os.ReadDir(sessionsDir())
|
|
if err == nil {
|
|
dirMap := map[string]bool{}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
dirMap[entry.Name()] = true
|
|
}
|
|
}
|
|
|
|
needsSave := false
|
|
|
|
for id := range snapshot {
|
|
if !dirMap[id] {
|
|
delete(snapshot, id)
|
|
needsSave = true
|
|
}
|
|
}
|
|
|
|
for id := range dirMap {
|
|
if _, ok := snapshot[id]; !ok {
|
|
sessionPath := filepath.Join(sessionsDir(), id, "session.json")
|
|
sessionData, err := os.ReadFile(sessionPath)
|
|
if err == nil {
|
|
var meta sessionMeta
|
|
if gulu.JSON.UnmarshalJSON(sessionData, &meta) == nil && meta.ID != "" {
|
|
title := meta.Title
|
|
if title == "" {
|
|
title = "AI Agent"
|
|
}
|
|
snapshot[id] = &SessionIndexItem{
|
|
ID: meta.ID,
|
|
Title: title,
|
|
CreatedAt: meta.CreatedAt,
|
|
UpdatedAt: meta.UpdatedAt,
|
|
}
|
|
needsSave = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if needsSave {
|
|
indexMu.Lock()
|
|
saveSessionIndex(snapshot)
|
|
indexMu.Unlock()
|
|
}
|
|
}
|
|
|
|
index = snapshot
|
|
|
|
items := make([]*SessionIndexItem, 0, len(index))
|
|
for _, item := range index {
|
|
if keyword != "" {
|
|
kw := strings.ToLower(keyword)
|
|
title := strings.ToLower(item.Title)
|
|
if !strings.Contains(title, kw) {
|
|
continue
|
|
}
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
sort.Slice(items, func(i, j int) bool {
|
|
return items[i].CreatedAt > items[j].CreatedAt
|
|
})
|
|
|
|
total := len(items)
|
|
|
|
start := (page - 1) * pageSize
|
|
if start >= total {
|
|
return &SessionListResult{
|
|
Sessions: []*SessionIndexItem{},
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}
|
|
}
|
|
end := start + pageSize
|
|
if end > total {
|
|
end = total
|
|
}
|
|
|
|
return &SessionListResult{
|
|
Sessions: items[start:end],
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}
|
|
}
|
|
|
|
func GetSession(id string) (map[string]interface{}, error) {
|
|
if id == "" || !isValidSessionID(id) {
|
|
return nil, nil
|
|
}
|
|
sessionPath := filepath.Join(sessionsDir(), id, "session.json")
|
|
data, err := os.ReadFile(sessionPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var session map[string]interface{}
|
|
if err := gulu.JSON.UnmarshalJSON(data, &session); err != nil {
|
|
return nil, err
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
func SaveSession(data []byte) error {
|
|
var meta sessionMeta
|
|
if err := gulu.JSON.UnmarshalJSON(data, &meta); err != nil || meta.ID == "" || !isValidSessionID(meta.ID) {
|
|
return nil
|
|
}
|
|
|
|
dir := filepath.Join(sessionsDir(), meta.ID)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
logging.LogErrorf("create session dir failed: %s", err)
|
|
}
|
|
path := filepath.Join(dir, "session.json")
|
|
|
|
existing, err := os.ReadFile(path)
|
|
if err == nil && len(existing) > 0 {
|
|
var existingData map[string]interface{}
|
|
var newData map[string]interface{}
|
|
if gulu.JSON.UnmarshalJSON(existing, &existingData) == nil &&
|
|
gulu.JSON.UnmarshalJSON(data, &newData) == nil {
|
|
for k, v := range existingData {
|
|
if _, ok := newData[k]; !ok {
|
|
switch k {
|
|
// messages 不再保留:以 entries 为唯一持久化数据源,
|
|
// 前端下次保存时会用 entries 覆盖,老 messages 字段自然清除。
|
|
case "createdAt", "titled", "messageHistory", "entries", "snapshots", "id", "alwaysAllow":
|
|
newData[k] = v
|
|
}
|
|
}
|
|
}
|
|
merged, err := gulu.JSON.MarshalIndentJSON(newData, "", "\t")
|
|
if err == nil {
|
|
data = merged
|
|
}
|
|
}
|
|
} else {
|
|
var newData map[string]interface{}
|
|
if gulu.JSON.UnmarshalJSON(data, &newData) == nil {
|
|
indented, err := gulu.JSON.MarshalIndentJSON(newData, "", "\t")
|
|
if err == nil {
|
|
data = indented
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := filelock.WriteFile(path, data); err != nil {
|
|
logging.LogErrorf("save session file failed: %s", err)
|
|
}
|
|
|
|
title := meta.Title
|
|
if title == "" {
|
|
title = "AI Agent"
|
|
}
|
|
UpdateSessionIndex(meta.ID, title, meta.CreatedAt, meta.UpdatedAt)
|
|
|
|
return nil
|
|
}
|
|
|
|
func DeleteSession(id string) error {
|
|
if id == "" || !isValidSessionID(id) {
|
|
return nil
|
|
}
|
|
|
|
dir := filepath.Join(sessionsDir(), id)
|
|
_ = os.RemoveAll(dir)
|
|
|
|
indexMu.Lock()
|
|
index := loadSessionIndex()
|
|
if index != nil {
|
|
delete(index, id)
|
|
saveSessionIndex(index)
|
|
}
|
|
indexMu.Unlock()
|
|
|
|
return nil
|
|
}
|