Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

691 lines
16 KiB
Go

package parser
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
var (
_ Provider = (*openClawProvider)(nil)
_ Provider = (*qClawProvider)(nil)
)
type clawProviderSpec struct {
agent AgentType
sessionFile func(string) bool
sessionID func(string) string
}
type openClawProviderFactory struct {
def AgentDef
}
func newOpenClawProviderFactory(def AgentDef) ProviderFactory {
return openClawProviderFactory{def: cloneAgentDef(def)}
}
func (f openClawProviderFactory) Definition() AgentDef {
return cloneAgentDef(f.def)
}
func (f openClawProviderFactory) Capabilities() Capabilities {
return openClawProviderCapabilities()
}
func (f openClawProviderFactory) NewProvider(cfg ProviderConfig) Provider {
cfg = cfg.Clone()
return &openClawProvider{
ProviderBase: ProviderBase{
Def: cloneAgentDef(f.def),
Caps: openClawProviderCapabilities(),
Config: cfg,
},
sources: newClawSourceSet(cfg.Roots, openClawProviderSpec()),
}
}
type openClawProvider struct {
ProviderBase
sources clawSourceSet
}
func (p *openClawProvider) Discover(ctx context.Context) ([]SourceRef, error) {
return p.sources.Discover(ctx)
}
func (p *openClawProvider) WatchPlan(ctx context.Context) (WatchPlan, error) {
return p.sources.WatchPlan(ctx)
}
func (p *openClawProvider) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
return p.sources.SourcesForChangedPath(ctx, req)
}
func (p *openClawProvider) FindSource(
ctx context.Context,
req FindSourceRequest,
) (SourceRef, bool, error) {
req = ProviderFindRequestWithRawSessionID(p.Def, req)
return p.sources.FindSource(ctx, req)
}
func (p *openClawProvider) Fingerprint(
ctx context.Context,
source SourceRef,
) (SourceFingerprint, error) {
return p.sources.Fingerprint(ctx, source)
}
func (p *openClawProvider) Parse(
ctx context.Context,
req ParseRequest,
) (ParseOutcome, error) {
if err := ctx.Err(); err != nil {
return ParseOutcome{}, err
}
path, ok := p.sources.pathFromSource(req.Source)
if !ok {
return ParseOutcome{}, fmt.Errorf("%s source path unavailable", p.Def.Type)
}
machine := firstNonEmptyJSONLString(req.Machine, p.Config.Machine)
sess, msgs, err := p.parseSession(path, "", machine)
return clawParseOutcome(req, sess, msgs, err)
}
type qClawProviderFactory struct {
def AgentDef
}
func newQClawProviderFactory(def AgentDef) ProviderFactory {
return qClawProviderFactory{def: cloneAgentDef(def)}
}
func (f qClawProviderFactory) Definition() AgentDef {
return cloneAgentDef(f.def)
}
func (f qClawProviderFactory) Capabilities() Capabilities {
return qClawProviderCapabilities()
}
func (f qClawProviderFactory) NewProvider(cfg ProviderConfig) Provider {
cfg = cfg.Clone()
return &qClawProvider{
ProviderBase: ProviderBase{
Def: cloneAgentDef(f.def),
Caps: qClawProviderCapabilities(),
Config: cfg,
},
sources: newClawSourceSet(cfg.Roots, qClawProviderSpec()),
}
}
type qClawProvider struct {
ProviderBase
sources clawSourceSet
}
func (p *qClawProvider) Discover(ctx context.Context) ([]SourceRef, error) {
return p.sources.Discover(ctx)
}
func (p *qClawProvider) WatchPlan(ctx context.Context) (WatchPlan, error) {
return p.sources.WatchPlan(ctx)
}
func (p *qClawProvider) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
return p.sources.SourcesForChangedPath(ctx, req)
}
func (p *qClawProvider) FindSource(
ctx context.Context,
req FindSourceRequest,
) (SourceRef, bool, error) {
req = ProviderFindRequestWithRawSessionID(p.Def, req)
return p.sources.FindSource(ctx, req)
}
func (p *qClawProvider) Fingerprint(
ctx context.Context,
source SourceRef,
) (SourceFingerprint, error) {
return p.sources.Fingerprint(ctx, source)
}
func (p *qClawProvider) Parse(
ctx context.Context,
req ParseRequest,
) (ParseOutcome, error) {
if err := ctx.Err(); err != nil {
return ParseOutcome{}, err
}
path, ok := p.sources.pathFromSource(req.Source)
if !ok {
return ParseOutcome{}, fmt.Errorf("%s source path unavailable", p.Def.Type)
}
machine := firstNonEmptyJSONLString(req.Machine, p.Config.Machine)
sess, msgs, err := p.parseSession(path, "", machine)
return clawParseOutcome(req, sess, msgs, err)
}
type clawSource struct {
Root string
Path string
}
type clawSourceSet struct {
roots []string
spec clawProviderSpec
}
func newClawSourceSet(roots []string, spec clawProviderSpec) clawSourceSet {
return clawSourceSet{
roots: cleanJSONLRoots(roots),
spec: spec,
}
}
func (s clawSourceSet) Discover(ctx context.Context) ([]SourceRef, error) {
var sources []SourceRef
seen := make(map[string]struct{})
for _, root := range s.roots {
if err := ctx.Err(); err != nil {
return nil, err
}
for _, source := range s.discoverRoot(root) {
key := string(source.Provider) + "\x00" + source.Key
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
sources = append(sources, source)
}
}
sort.Slice(sources, func(i, j int) bool {
if sources[i].DisplayPath != sources[j].DisplayPath {
return sources[i].DisplayPath < sources[j].DisplayPath
}
return sources[i].Key < sources[j].Key
})
return sources, nil
}
func (s clawSourceSet) WatchPlan(context.Context) (WatchPlan, error) {
roots := make([]WatchRoot, 0, len(s.roots))
for _, root := range s.roots {
roots = append(roots, WatchRoot{
Path: root,
Recursive: true,
IncludeGlobs: []string{"*.jsonl", "*.jsonl.*"},
DebounceKey: string(s.spec.agent) + ":claw:" + root,
})
}
return WatchPlan{Roots: roots}, nil
}
func (s clawSourceSet) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if req.WatchRoot != "" {
for _, root := range s.roots {
if !samePath(root, req.WatchRoot) {
continue
}
source, ok := s.sourceForChangedPathInRoot(root, req)
if !ok {
return nil, nil
}
return []SourceRef{source}, nil
}
return nil, nil
}
source, ok := s.sourceForChangedPath(req)
if !ok {
return nil, nil
}
return []SourceRef{source}, nil
}
func (s clawSourceSet) FindSource(
ctx context.Context,
req FindSourceRequest,
) (SourceRef, bool, error) {
if err := ctx.Err(); err != nil {
return SourceRef{}, false, err
}
for _, path := range []string{req.StoredFilePath, req.FingerprintKey} {
if path == "" {
continue
}
if source, ok := s.sourceForStoredPath(path); ok {
return source, true, nil
}
}
if req.RawSessionID == "" {
return SourceRef{}, false, nil
}
for _, root := range s.roots {
path := s.sourcePathForRawID(root, req.RawSessionID)
if path == "" {
continue
}
if source, ok := s.sourceRef(root, path); ok {
return source, true, nil
}
}
return SourceRef{}, false, nil
}
func (s clawSourceSet) Fingerprint(
ctx context.Context,
source SourceRef,
) (SourceFingerprint, error) {
if err := ctx.Err(); err != nil {
return SourceFingerprint{}, err
}
path, ok := s.pathFromSource(source)
if !ok {
return SourceFingerprint{}, fmt.Errorf("%s source path unavailable", s.spec.agent)
}
info, err := os.Stat(path)
if err != nil {
return SourceFingerprint{}, fmt.Errorf("stat %s: %w", path, err)
}
if info.IsDir() {
return SourceFingerprint{}, fmt.Errorf("stat %s: source is a directory", path)
}
// Legacy processOpenClaw/processQClaw persisted a full-file content hash
// (file_hash). Without it here the parse outcome leaves Session.File.Hash
// empty and a resync clears the stored hash to NULL.
hash, err := hashJSONLSourceFile(path)
if err != nil {
return SourceFingerprint{}, err
}
return SourceFingerprint{
Key: firstNonEmptyJSONLString(source.FingerprintKey, source.Key, path),
Size: info.Size(),
MTimeNS: info.ModTime().UnixNano(),
Hash: hash,
}, nil
}
func (s clawSourceSet) pathFromSource(source SourceRef) (string, bool) {
switch src := source.Opaque.(type) {
case clawSource:
return src.Path, src.Path != ""
case *clawSource:
if src != nil && src.Path != "" {
return src.Path, true
}
}
for _, candidate := range []string{
source.DisplayPath,
source.FingerprintKey,
source.Key,
} {
if ref, ok := s.sourceForPath(candidate); ok {
src := ref.Opaque.(clawSource)
return src.Path, true
}
}
return "", false
}
func (s clawSourceSet) sourceForPath(path string) (SourceRef, bool) {
for _, root := range s.roots {
if source, ok := s.sourceForPathInRoot(root, path); ok {
return source, true
}
}
return SourceRef{}, false
}
func (s clawSourceSet) sourceForChangedPath(req ChangedPathRequest) (SourceRef, bool) {
for _, root := range s.roots {
if source, ok := s.sourceForChangedPathInRoot(root, req); ok {
return source, true
}
}
return SourceRef{}, false
}
func (s clawSourceSet) sourceForChangedPathInRoot(
root string,
req ChangedPathRequest,
) (SourceRef, bool) {
if source, ok := s.sourceForPathInRoot(root, req.Path); ok {
return source, true
}
if !jsonlMissingPathFallbackAllowed(req) {
return SourceRef{}, false
}
return s.sourceForStoredPathInRoot(root, req.Path)
}
func (s clawSourceSet) sourceForStoredPath(path string) (SourceRef, bool) {
for _, root := range s.roots {
if source, ok := s.sourceForStoredPathInRoot(root, path); ok {
return source, true
}
}
return SourceRef{}, false
}
func (s clawSourceSet) sourceForStoredPathInRoot(
root string,
path string,
) (SourceRef, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
rawID, ok := s.rawIDFromPath(root, path)
if !ok {
return SourceRef{}, false
}
best := s.sourcePathForRawID(root, rawID)
if best == "" {
return SourceRef{}, false
}
return s.sourceRef(root, best)
}
func (s clawSourceSet) sourceForPathInRoot(root string, path string) (SourceRef, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
rawID, ok := s.rawIDFromPath(root, path)
if !ok {
return SourceRef{}, false
}
best := s.sourcePathForRawID(root, rawID)
if best == "" || !samePath(best, path) {
return SourceRef{}, false
}
return s.sourceRef(root, best)
}
func (s clawSourceSet) sourceRef(root string, path string) (SourceRef, bool) {
rawID, ok := s.rawIDFromPath(root, path)
if !ok {
return SourceRef{}, false
}
return SourceRef{
Provider: s.spec.agent,
Key: path,
DisplayPath: path,
FingerprintKey: path,
ProjectHint: clawAgentIDFromRawID(rawID),
Opaque: clawSource{
Root: filepath.Clean(root),
Path: filepath.Clean(path),
},
}, true
}
func (s clawSourceSet) rawIDFromPath(root string, path string) (string, bool) {
rel, err := filepath.Rel(filepath.Clean(root), filepath.Clean(path))
if err != nil {
return "", false
}
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) != 3 || parts[1] != "sessions" {
return "", false
}
if !IsValidSessionID(parts[0]) || !s.spec.sessionFile(parts[2]) {
return "", false
}
sessionID := s.spec.sessionID(parts[2])
if !IsValidSessionID(sessionID) {
return "", false
}
return parts[0] + ":" + sessionID, true
}
func (s clawSourceSet) discoverRoot(root string) []SourceRef {
if root == "" {
return nil
}
agentEntries, err := os.ReadDir(root)
if err != nil {
return nil
}
var sources []SourceRef
for _, agentEntry := range agentEntries {
if !isDirOrSymlink(agentEntry, root) {
continue
}
if !IsValidSessionID(agentEntry.Name()) {
continue
}
sessionsDir := filepath.Join(root, agentEntry.Name(), "sessions")
entries, err := os.ReadDir(sessionsDir)
if err != nil {
continue
}
best := make(map[string]os.DirEntry)
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !s.spec.sessionFile(name) {
continue
}
sessionID := s.spec.sessionID(name)
if !IsValidSessionID(sessionID) {
continue
}
prev, exists := best[sessionID]
if !exists {
best[sessionID] = entry
continue
}
best[sessionID] = s.bestEntry(prev, entry)
}
for _, entry := range best {
source, ok := s.sourceRef(root, filepath.Join(sessionsDir, entry.Name()))
if ok {
sources = append(sources, source)
}
}
}
return sources
}
func (s clawSourceSet) sourcePathForRawID(root, rawID string) string {
if root == "" {
return ""
}
agentID, sessionID, ok := strings.Cut(rawID, ":")
if !ok || !IsValidSessionID(agentID) || !IsValidSessionID(sessionID) {
return ""
}
sessionsDir := filepath.Join(root, agentID, "sessions")
active := filepath.Join(sessionsDir, sessionID+".jsonl")
if _, err := os.Stat(active); err == nil {
return active
}
entries, err := os.ReadDir(sessionsDir)
if err != nil {
return ""
}
var best os.DirEntry
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !s.spec.sessionFile(name) {
continue
}
if s.spec.sessionID(name) != sessionID {
continue
}
if best == nil {
best = entry
continue
}
best = s.bestEntry(best, entry)
}
if best == nil {
return ""
}
return filepath.Join(sessionsDir, best.Name())
}
func (s clawSourceSet) bestEntry(a, b os.DirEntry) os.DirEntry {
aActive := strings.HasSuffix(a.Name(), ".jsonl")
bActive := strings.HasSuffix(b.Name(), ".jsonl")
if aActive && !bActive {
return a
}
if bActive && !aActive {
return b
}
aTime := clawArchiveTime(a)
bTime := clawArchiveTime(b)
if !aTime.IsZero() && !bTime.IsZero() {
if bTime.After(aTime) {
return b
}
return a
}
if !aTime.IsZero() {
return a
}
if !bTime.IsZero() {
return b
}
ai, errA := a.Info()
bi, errB := b.Info()
if errA == nil && errB == nil && bi.ModTime().After(ai.ModTime()) {
return b
}
return a
}
func clawArchiveTime(e os.DirEntry) time.Time {
name := e.Name()
idx := strings.Index(name, ".jsonl.")
if idx <= 0 {
return time.Time{}
}
suffix := name[idx+len(".jsonl."):]
_, tsStr, ok := strings.Cut(suffix, ".")
if !ok {
return time.Time{}
}
if tIdx := strings.IndexByte(tsStr, 'T'); tIdx >= 0 {
datePart := tsStr[:tIdx+1]
timePart := tsStr[tIdx+1:]
timePart = strings.Replace(timePart, "-", ":", 1)
timePart = strings.Replace(timePart, "-", ":", 1)
tsStr = datePart + timePart
}
t, err := time.Parse("2006-01-02T15:04:05.000Z", tsStr)
if err != nil {
t, err = time.Parse("2006-01-02T15:04:05Z", tsStr)
}
if err != nil {
return time.Time{}
}
return t
}
func clawParseOutcome(
req ParseRequest,
sess *ParsedSession,
msgs []ParsedMessage,
err error,
) (ParseOutcome, error) {
if err != nil {
return ParseOutcome{}, err
}
if sess == nil {
return ParseOutcome{
ResultSetComplete: true,
SkipReason: SkipNoSession,
}, nil
}
if req.Fingerprint.Hash != "" {
sess.File.Hash = req.Fingerprint.Hash
}
return ParseOutcome{
Results: []ParseResultOutcome{{
Result: ParseResult{
Session: *sess,
Messages: msgs,
},
DataVersion: DataVersionCurrent,
}},
ResultSetComplete: true,
}, nil
}
func openClawProviderSpec() clawProviderSpec {
return clawProviderSpec{
agent: AgentOpenClaw,
sessionFile: IsOpenClawSessionFile,
sessionID: OpenClawSessionID,
}
}
func qClawProviderSpec() clawProviderSpec {
return clawProviderSpec{
agent: AgentQClaw,
sessionFile: IsQClawSessionFile,
sessionID: QClawSessionID,
}
}
func clawAgentIDFromRawID(rawID string) string {
agentID, _, ok := strings.Cut(rawID, ":")
if !ok {
return ""
}
return agentID
}
func openClawProviderCapabilities() Capabilities {
return clawProviderCapabilities()
}
func qClawProviderCapabilities() Capabilities {
return clawProviderCapabilities()
}
func clawProviderCapabilities() Capabilities {
return Capabilities{
Source: jsonlFileProviderSourceCapabilities(),
Content: ContentCapabilities{
FirstMessage: CapabilitySupported,
Thinking: CapabilitySupported,
ToolCalls: CapabilitySupported,
ToolResults: CapabilitySupported,
PerMessageTokenUsage: CapabilitySupported,
Model: CapabilitySupported,
},
}
}