Files
wehub-resource-sync f99010fae1
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
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

415 lines
9.8 KiB
Go

package parser
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
)
var _ Provider = (*openHandsProvider)(nil)
type openHandsProviderFactory struct {
def AgentDef
}
func newOpenHandsProviderFactory(def AgentDef) ProviderFactory {
return openHandsProviderFactory{def: cloneAgentDef(def)}
}
func (f openHandsProviderFactory) Definition() AgentDef {
return cloneAgentDef(f.def)
}
func (f openHandsProviderFactory) Capabilities() Capabilities {
return openHandsProviderCapabilities()
}
func (f openHandsProviderFactory) NewProvider(cfg ProviderConfig) Provider {
cfg = cfg.Clone()
return &openHandsProvider{
ProviderBase: ProviderBase{
Def: cloneAgentDef(f.def),
Caps: openHandsProviderCapabilities(),
Config: cfg,
},
sources: newOpenHandsSourceSet(cfg.Roots),
}
}
type openHandsProvider struct {
ProviderBase
sources openHandsSourceSet
}
func (p *openHandsProvider) Discover(ctx context.Context) ([]SourceRef, error) {
return p.sources.Discover(ctx)
}
func (p *openHandsProvider) WatchPlan(ctx context.Context) (WatchPlan, error) {
return p.sources.WatchPlan(ctx)
}
func (p *openHandsProvider) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
return p.sources.SourcesForChangedPath(ctx, req)
}
func (p *openHandsProvider) FindSource(
ctx context.Context,
req FindSourceRequest,
) (SourceRef, bool, error) {
req = ProviderFindRequestWithRawSessionID(p.Def, req)
return p.sources.FindSource(ctx, req)
}
func (p *openHandsProvider) Fingerprint(
ctx context.Context,
source SourceRef,
) (SourceFingerprint, error) {
return p.sources.Fingerprint(ctx, source)
}
func (p *openHandsProvider) 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("openhands source path unavailable")
}
machine := firstNonEmptyJSONLString(req.Machine, p.Config.Machine)
sess, msgs, err := p.parseSession(path, machine)
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
}
type openHandsSource struct {
Root string
Path string
}
type openHandsSourceSet struct {
roots []string
}
func newOpenHandsSourceSet(roots []string) openHandsSourceSet {
return openHandsSourceSet{roots: cleanJSONLRoots(roots)}
}
func (s openHandsSourceSet) 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
}
entries, err := os.ReadDir(root)
if err != nil {
continue
}
for _, entry := range entries {
if !entry.IsDir() || !IsValidSessionID(entry.Name()) {
continue
}
sessionDir := filepath.Join(root, entry.Name())
source, ok := s.sourceRef(root, sessionDir)
if !ok {
continue
}
addJSONLSource(source, &sources, seen)
}
}
sortJSONLSources(sources)
return sources, nil
}
func (s openHandsSourceSet) WatchPlan(context.Context) (WatchPlan, error) {
roots := make([]WatchRoot, 0, len(s.roots))
for _, root := range s.roots {
roots = append(roots, WatchRoot{
Path: root,
Recursive: false,
DebounceKey: string(AgentOpenHands) + ":dir:" + root,
})
}
return WatchPlan{Roots: roots}, nil
}
func (s openHandsSourceSet) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if req.WatchRoot != "" {
root := filepath.Clean(req.WatchRoot)
if !s.hasRoot(root) {
return nil, nil
}
source, ok := s.sourceForPathInRoot(root, req.Path)
if !ok {
return nil, nil
}
return []SourceRef{source}, nil
}
for _, root := range s.roots {
source, ok := s.sourceForPathInRoot(root, req.Path)
if ok {
return []SourceRef{source}, nil
}
}
return nil, nil
}
func (s openHandsSourceSet) 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.sourceForPath(path); ok {
return source, true, nil
}
}
if req.RawSessionID == "" {
return SourceRef{}, false, nil
}
for _, root := range s.roots {
path := s.sessionDirForID(root, req.RawSessionID)
if path == "" {
continue
}
if source, ok := s.sourceRef(root, path); ok {
return source, true, nil
}
}
return SourceRef{}, false, nil
}
// sessionDirForID locates an OpenHands conversation directory under
// root by its raw session ID. It first tries the raw ID and its
// dash-stripped form as literal directory names, then falls back to
// matching any session directory whose normalized ID equals the
// normalized raw ID.
func (s openHandsSourceSet) sessionDirForID(root, rawID string) string {
if root == "" || !IsValidSessionID(rawID) {
return ""
}
candidates := []string{rawID}
stripped := strings.ReplaceAll(rawID, "-", "")
if stripped != rawID {
candidates = append(candidates, stripped)
}
for _, cand := range candidates {
sessionDir := filepath.Join(root, cand)
if isOpenHandsSessionDir(sessionDir) {
return sessionDir
}
}
entries, err := os.ReadDir(root)
if err != nil {
return ""
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
sessionDir := filepath.Join(root, entry.Name())
if !isOpenHandsSessionDir(sessionDir) {
continue
}
if normalizeOpenHandsSessionID(entry.Name()) ==
normalizeOpenHandsSessionID(rawID) {
return sessionDir
}
}
return ""
}
func (s openHandsSourceSet) 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("openhands source path unavailable")
}
snapshot, err := OpenHandsSnapshot(path)
if err != nil {
return SourceFingerprint{}, err
}
return SourceFingerprint{
Key: firstNonEmptyJSONLString(source.FingerprintKey, source.Key, path),
Size: snapshot.Size,
MTimeNS: snapshot.Mtime,
Hash: snapshot.Hash,
}, nil
}
func (s openHandsSourceSet) pathFromSource(source SourceRef) (string, bool) {
switch src := source.Opaque.(type) {
case openHandsSource:
return src.Path, src.Path != ""
case *openHandsSource:
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.(openHandsSource)
return src.Path, true
}
}
return "", false
}
func (s openHandsSourceSet) 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 openHandsSourceSet) sourceForPathInRoot(
root string,
path string,
) (SourceRef, bool) {
sessionDir, ok := openHandsSessionDirForPath(root, path)
if !ok {
return SourceRef{}, false
}
return s.sourceRef(root, sessionDir)
}
func (s openHandsSourceSet) sourceRef(root, path string) (SourceRef, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
if !isOpenHandsSessionDir(path) {
return SourceRef{}, false
}
rel, err := filepath.Rel(root, path)
if err != nil || rel == "." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) ||
strings.Contains(rel, string(filepath.Separator)) {
return SourceRef{}, false
}
if !IsValidSessionID(rel) {
return SourceRef{}, false
}
return SourceRef{
Provider: AgentOpenHands,
Key: path,
DisplayPath: path,
FingerprintKey: path,
Opaque: openHandsSource{
Root: root,
Path: path,
},
}, true
}
func (s openHandsSourceSet) hasRoot(root string) bool {
for _, configured := range s.roots {
if samePath(root, configured) {
return true
}
}
return false
}
func openHandsSessionDirForPath(root, path string) (string, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
rel, err := filepath.Rel(root, path)
if err != nil || rel == "." || rel == ".." ||
strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", false
}
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) == 0 || !IsValidSessionID(parts[0]) {
return "", false
}
switch len(parts) {
case 1:
case 2:
if parts[1] != "base_state.json" && parts[1] != "TASKS.json" {
return "", false
}
case 3:
if parts[1] != "events" || filepath.Ext(parts[2]) != ".json" {
return "", false
}
default:
return "", false
}
return filepath.Join(root, parts[0]), true
}
func openHandsProviderCapabilities() Capabilities {
return Capabilities{
Source: SourceCapabilities{
DiscoverSources: CapabilitySupported,
WatchSources: CapabilitySupported,
ClassifyChangedPath: CapabilitySupported,
FindSource: CapabilitySupported,
CompositeFingerprint: CapabilitySupported,
MultiSessionSource: CapabilityNotApplicable,
PerSessionErrors: CapabilityNotApplicable,
ExcludedSessions: CapabilityNotApplicable,
ForceReplaceOnParse: CapabilityNotApplicable,
},
Content: ContentCapabilities{
FirstMessage: CapabilitySupported,
Cwd: CapabilitySupported,
Thinking: CapabilitySupported,
ToolCalls: CapabilitySupported,
ToolResults: CapabilitySupported,
Model: CapabilitySupported,
},
}
}