Files
casdoor--casdoor/object/openclaw_transcript_storage.go
2026-07-13 13:36:11 +08:00

440 lines
13 KiB
Go

// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
)
const (
openClawTranscriptResourceTag = "openclawTranscript"
openClawSessionTranscriptPreviewLimit = 2 * 1024 * 1024
openClawTranscriptDefaultStorageProviderName = "openclaw-transcript-storage"
)
type OpenClawRawTranscriptRef struct {
FileName string `json:"fileName,omitempty"`
FileSize int `json:"fileSize,omitempty"`
}
type OpenClawSessionTranscriptPreview struct {
FileName string `json:"fileName"`
FileSize int `json:"fileSize"`
LoadedSize int `json:"loadedSize"`
Truncated bool `json:"truncated"`
Content string `json:"content"`
}
func uploadOpenClawRawTranscript(logProvider *Provider, sessionID string, transcriptPath string, fileSize int64) error {
storageProvider, err := getOpenClawTranscriptStorageProvider(logProvider)
if err != nil {
return err
}
if storageProvider == nil {
return fmt.Errorf("no enabled Storage provider found")
}
resource, err := getOpenClawRawTranscriptResource(logProvider.Owner, logProvider.Name, sessionID)
if err != nil {
return err
}
objectKey := ""
if resource != nil {
objectKey = strings.TrimSpace(resource.Name)
}
if objectKey == "" {
fullFilePath := getOpenClawTranscriptStoragePath(logProvider.Owner, logProvider.Name, sessionID, util.GenerateUUID())
objectKey = getOpenClawTranscriptObjectKey(storageProvider, fullFilePath)
}
if strings.Contains(objectKey, "..") {
return fmt.Errorf("the objectKey: %s is not allowed", objectKey)
}
if err := putOpenClawRawTranscriptObject(storageProvider, objectKey, transcriptPath); err != nil {
return err
}
resource = buildOpenClawRawTranscriptResource(logProvider, storageProvider, sessionID, transcriptPath, fileSize, objectKey, resource)
_, err = AddOrUpdateResource(resource)
return err
}
func getOpenClawTranscriptStorageProvider(logProvider *Provider) (*Provider, error) {
if logProvider == nil {
return nil, fmt.Errorf("provider is nil")
}
providers, err := GetProviders(logProvider.Owner)
if err != nil {
return nil, err
}
storageProvider, err := selectOpenClawTranscriptStorageProvider(logProvider, providers)
if err != nil {
return nil, err
}
if storageProvider != nil {
return storageProvider, nil
}
return createOpenClawTranscriptDefaultStorageProvider(logProvider)
}
func selectOpenClawTranscriptStorageProvider(logProvider *Provider, providers []*Provider) (*Provider, error) {
selectedName := strings.TrimSpace(logProvider.ProviderUrl)
if selectedName != "" {
for _, provider := range providers {
if provider == nil || provider.Name != selectedName {
continue
}
if !isUsableOpenClawTranscriptStorageProviderForOwner(logProvider.Owner, provider) {
return nil, fmt.Errorf("selected provider %s is not an enabled Storage provider", selectedName)
}
return provider, nil
}
return nil, fmt.Errorf("selected Storage provider %s is not found", selectedName)
}
for _, provider := range providers {
if !isUsableOpenClawTranscriptStorageProviderForOwner(logProvider.Owner, provider) {
continue
}
return provider, nil
}
return nil, nil
}
func isUsableOpenClawTranscriptStorageProvider(provider *Provider) bool {
return provider != nil && provider.Category == "Storage" && !strings.EqualFold(strings.TrimSpace(provider.State), "Disabled")
}
func isUsableOpenClawTranscriptStorageProviderForOwner(owner string, provider *Provider) bool {
return isUsableOpenClawTranscriptStorageProvider(provider) && strings.TrimSpace(provider.Owner) == strings.TrimSpace(owner)
}
func createOpenClawTranscriptDefaultStorageProvider(logProvider *Provider) (*Provider, error) {
if logProvider == nil {
return nil, fmt.Errorf("provider is nil")
}
provider := &Provider{
Owner: logProvider.Owner,
Name: openClawTranscriptDefaultStorageProviderName,
CreatedTime: util.GetCurrentTime(),
DisplayName: "OpenClaw Transcript Storage",
Category: "Storage",
Type: ProviderTypeLocalFileSystem,
State: "Enabled",
PathPrefix: "openclaw-transcripts",
Domain: getOpenClawDefaultStorageDomain(),
}
affected, err := AddProvider(provider)
if err != nil {
providers, getErr := GetProviders(logProvider.Owner)
if getErr != nil {
return nil, err
}
for _, existing := range providers {
if existing != nil && existing.Name == openClawTranscriptDefaultStorageProviderName && isUsableOpenClawTranscriptStorageProviderForOwner(logProvider.Owner, existing) {
return existing, nil
}
}
return nil, err
}
if !affected {
return nil, fmt.Errorf("failed to create default OpenClaw transcript Storage provider")
}
return provider, nil
}
func getOpenClawDefaultStorageDomain() string {
if origin := strings.TrimSpace(conf.GetConfigString("origin")); origin != "" {
return origin
}
return strings.TrimSpace(conf.GetConfigString("originFrontend"))
}
func getOpenClawTranscriptStoragePath(owner string, providerName string, sessionID string, nonce string) string {
return path.Join(
"openclaw",
sanitizeOpenClawStoragePathSegment(owner),
sanitizeOpenClawStoragePathSegment(providerName),
"sessions",
sanitizeOpenClawStoragePathSegment(nonce),
fmt.Sprintf("%s.jsonl", sanitizeOpenClawStoragePathSegment(sessionID)),
)
}
func getOpenClawTranscriptObjectKey(provider *Provider, fullFilePath string) string {
escapedPath := util.UrlJoin(provider.PathPrefix, fullFilePath)
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
if provider.Type == ProviderTypeLocalFileSystem {
objectKey = strings.TrimLeft(objectKey, "/")
}
if provider.Type == ProviderTypeTencentCloudCOS {
objectKey = escapePath(objectKey)
}
return objectKey
}
func getOpenClawTranscriptResourceParent(owner string, providerName string, sessionID string) string {
owner = strings.TrimSpace(owner)
providerName = strings.TrimSpace(providerName)
sessionID = strings.TrimSpace(sessionID)
if owner == "" || providerName == "" || sessionID == "" {
return ""
}
return path.Join("openclaw", util.GetMd5Hash(fmt.Sprintf("%s|%s|%s", owner, providerName, sessionID)))
}
func getOpenClawRawTranscriptResource(owner string, providerName string, sessionID string) (*Resource, error) {
owner = strings.TrimSpace(owner)
parent := getOpenClawTranscriptResourceParent(owner, providerName, sessionID)
if owner == "" || parent == "" {
return nil, nil
}
resource := Resource{}
existed, err := ormer.Engine.
Where("owner = ? and tag = ? and parent = ?", owner, openClawTranscriptResourceTag, parent).
Desc("created_time").
Get(&resource)
if err != nil {
return nil, err
}
if !existed {
return nil, nil
}
return &resource, nil
}
func getOpenClawRawTranscriptRef(owner string, providerName string, sessionID string) (*OpenClawRawTranscriptRef, error) {
resource, err := getOpenClawRawTranscriptResource(owner, providerName, sessionID)
if err != nil {
return nil, err
}
return buildOpenClawRawTranscriptRef(resource), nil
}
func buildOpenClawRawTranscriptRef(resource *Resource) *OpenClawRawTranscriptRef {
if resource == nil {
return nil
}
return &OpenClawRawTranscriptRef{
FileName: resource.FileName,
FileSize: resource.FileSize,
}
}
func GetOpenClawSessionTranscript(id string, lang string) (*OpenClawSessionTranscriptPreview, error) {
entry, _, payload, err := loadOpenClawSessionEntry(id)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
resource, err := getOpenClawRawTranscriptResource(entry.Owner, entry.Provider, payload.SessionID)
if err != nil {
return nil, err
}
if resource == nil {
return nil, nil
}
provider, err := getOpenClawTranscriptStorageProviderByName(resource.Owner, resource.Provider)
if err != nil {
return nil, err
}
if provider == nil || provider.Category != "Storage" {
return nil, fmt.Errorf("storage provider %s is not found", resource.Provider)
}
storageProvider, err := getStorageProvider(provider, lang)
if err != nil {
return nil, err
}
reader, err := storageProvider.GetStream(refineObjectKey(provider, resource.Name))
if err != nil {
return nil, err
}
defer reader.Close()
fileName := strings.TrimSpace(resource.FileName)
if fileName == "" {
fileName = fmt.Sprintf("%s.jsonl", payload.SessionID)
}
content, truncated, err := readOpenClawSessionTranscriptPreview(reader, openClawSessionTranscriptPreviewLimit)
if err != nil {
return nil, err
}
return &OpenClawSessionTranscriptPreview{
FileName: filepath.Base(fileName),
FileSize: resource.FileSize,
LoadedSize: len(content),
Truncated: truncated,
Content: string(content),
}, nil
}
func putOpenClawRawTranscriptObject(provider *Provider, objectKey string, transcriptPath string) error {
storageProvider, err := getStorageProvider(provider, "en")
if err != nil {
return err
}
file, err := os.Open(transcriptPath)
if err != nil {
return err
}
defer file.Close()
_, err = storageProvider.Put(refineObjectKey(provider, objectKey), file)
return err
}
func buildOpenClawRawTranscriptResource(logProvider *Provider, storageProvider *Provider, sessionID string, transcriptPath string, fileSize int64, objectKey string, current *Resource) *Resource {
now := util.GetCurrentTime()
createdTime := now
if current != nil && strings.TrimSpace(current.CreatedTime) != "" {
createdTime = current.CreatedTime
}
return &Resource{
Owner: logProvider.Owner,
Name: objectKey,
CreatedTime: createdTime,
User: "",
Provider: storageProvider.Name,
Application: CasdoorApplication,
Tag: openClawTranscriptResourceTag,
Parent: getOpenClawTranscriptResourceParent(logProvider.Owner, logProvider.Name, sessionID),
FileName: filepath.Base(transcriptPath),
FileType: "text",
FileFormat: filepath.Ext(transcriptPath),
FileSize: int(fileSize),
Url: "",
Description: "OpenClaw raw session transcript",
}
}
func getOpenClawTranscriptStorageProviderByName(owner string, name string) (*Provider, error) {
if strings.TrimSpace(owner) == "" || strings.TrimSpace(name) == "" {
return nil, nil
}
provider := Provider{Owner: owner, Name: name}
existed, err := ormer.Engine.Get(&provider)
if err != nil {
return nil, err
}
if !existed {
return nil, nil
}
return &provider, nil
}
func readOpenClawSessionTranscriptPreview(reader io.Reader, limit int) ([]byte, bool, error) {
if limit < 0 {
limit = 0
}
content, err := io.ReadAll(io.LimitReader(reader, int64(limit)+1))
if err != nil {
return nil, false, err
}
if len(content) > limit {
return content[:limit], true, nil
}
return content, false, nil
}
func loadOpenClawSessionEntry(id string) (*Entry, *Provider, openClawBehaviorPayload, error) {
entry, err := GetEntry(id)
if err != nil {
return nil, nil, openClawBehaviorPayload{}, err
}
if entry == nil {
return nil, nil, openClawBehaviorPayload{}, nil
}
if strings.TrimSpace(entry.Type) != "session" {
return nil, nil, openClawBehaviorPayload{}, fmt.Errorf("entry %s is not an OpenClaw session entry", id)
}
provider, err := GetProvider(util.GetId(entry.Owner, entry.Provider))
if err != nil {
return nil, nil, openClawBehaviorPayload{}, err
}
if provider != nil && !isOpenClawLogProvider(provider) {
return nil, nil, openClawBehaviorPayload{}, fmt.Errorf("entry %s is not an OpenClaw session entry", id)
}
payload, err := parseOpenClawSessionGraphPayload(entry)
if err != nil {
return nil, nil, openClawBehaviorPayload{}, fmt.Errorf("failed to parse anchor entry %s: %w", entry.Name, err)
}
return entry, provider, payload, nil
}
func sanitizeOpenClawStoragePathSegment(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "unknown"
}
var builder strings.Builder
for _, ch := range value {
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == '.' {
builder.WriteRune(ch)
} else {
builder.WriteByte('_')
}
}
sanitized := strings.Trim(builder.String(), ". ")
for strings.Contains(sanitized, "..") {
sanitized = strings.ReplaceAll(sanitized, "..", "_")
}
if sanitized == "" {
return "unknown"
}
return sanitized
}