Files
chenhg5-cc-connect/core/reference_render.go
Jerry bfb6d69780 Feature: normalize local references for IM rendering and add /show for direct file inspection (#495)
* feat(references): normalize and render local agent references

* feat(show): add reference-aware file and directory viewing

* docs: document references config and show usage

* chore: remove local working docs from PR branch

* fix: address lint issues in reference pipeline

* fix: drop stale quiet leftovers after main rebase

* fix: clean remaining rebase markers in show tests
2026-04-09 16:50:25 +08:00

544 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"fmt"
"path/filepath"
"regexp"
"sort"
"strings"
"unicode/utf8"
)
type ReferenceRenderCfg struct {
NormalizeAgents []string
RenderPlatforms []string
DisplayPath string
MarkerStyle string
EnclosureStyle string
}
type placeholderReplacement struct {
placeholder string
ref *localReference
keepText string
}
var (
reFenceBlock = regexp.MustCompile("(?s)```.*?```")
reInlineCodeSpan = regexp.MustCompile("`([^`\n]+)`")
reBareURL = regexp.MustCompile(`https?://[^\s<>()]+`)
reAbsOrFileRef = regexp.MustCompile(`file:///[^\s` + "`" + `<>\[\](),,、;;。!?!?]+|/[^\s` + "`" + `<>\[\](),,、;;。!?!?]+`)
reRelativeRef = regexp.MustCompile(`(?:\.\.?/|[A-Za-z0-9_.-]+/)[^\s` + "`" + `<>\[\](),,、;;。!?!?]+`)
reBasenameFileRef = regexp.MustCompile(`\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+(?:#L\d+(?:C\d+)?|:\d+(?::\d+)?|:\d+-\d+)?\b`)
)
func DefaultReferenceRenderCfg() ReferenceRenderCfg {
return ReferenceRenderCfg{
DisplayPath: "dirname_basename",
MarkerStyle: "emoji",
EnclosureStyle: "code",
}
}
func normalizeReferenceRenderCfg(cfg ReferenceRenderCfg) ReferenceRenderCfg {
n := DefaultReferenceRenderCfg()
if strings.TrimSpace(cfg.DisplayPath) != "" {
n.DisplayPath = strings.ToLower(strings.TrimSpace(cfg.DisplayPath))
}
if strings.TrimSpace(cfg.MarkerStyle) != "" {
n.MarkerStyle = strings.ToLower(strings.TrimSpace(cfg.MarkerStyle))
}
if strings.TrimSpace(cfg.EnclosureStyle) != "" {
n.EnclosureStyle = strings.ToLower(strings.TrimSpace(cfg.EnclosureStyle))
}
n.NormalizeAgents = normalizeReferenceScope(cfg.NormalizeAgents, supportedReferenceNormalizeAgents)
n.RenderPlatforms = normalizeReferenceScope(cfg.RenderPlatforms, supportedReferenceRenderPlatforms)
return n
}
var supportedReferenceNormalizeAgents = []string{"codex", "claudecode"}
var supportedReferenceRenderPlatforms = []string{"feishu", "weixin"}
func normalizeReferenceScope(values []string, supported []string) []string {
if len(values) == 0 {
return nil
}
supportedSet := make(map[string]struct{}, len(supported))
for _, v := range supported {
supportedSet[v] = struct{}{}
}
hasAll := false
seen := map[string]struct{}{}
out := make([]string, 0, len(values))
for _, v := range values {
key := strings.ToLower(strings.TrimSpace(v))
if key == "" {
continue
}
if key == "all" {
hasAll = true
continue
}
if _, ok := supportedSet[key]; !ok {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, key)
}
if hasAll {
return append([]string(nil), supported...)
}
return out
}
func (cfg ReferenceRenderCfg) renderEnabled(agentName, platformName string) bool {
if len(cfg.NormalizeAgents) == 0 || len(cfg.RenderPlatforms) == 0 {
return false
}
agentName = strings.ToLower(strings.TrimSpace(agentName))
platformName = strings.ToLower(strings.TrimSpace(platformName))
if !containsFolded(cfg.NormalizeAgents, agentName) {
return false
}
return containsFolded(cfg.RenderPlatforms, platformName)
}
func containsFolded(values []string, want string) bool {
for _, v := range values {
if strings.EqualFold(strings.TrimSpace(v), want) {
return true
}
}
return false
}
func TransformLocalReferences(text string, cfg ReferenceRenderCfg, agentName, platformName, workspaceDir string) string {
cfg = normalizeReferenceRenderCfg(cfg)
if !cfg.renderEnabled(agentName, platformName) || strings.TrimSpace(text) == "" {
return text
}
parts := splitWithMatches(text, reFenceBlock)
var out strings.Builder
for _, part := range parts {
if part.matched {
out.WriteString(part.text)
continue
}
out.WriteString(transformTextOutsideFence(part.text, cfg, workspaceDir))
}
return out.String()
}
func transformTextOutsideFence(text string, cfg ReferenceRenderCfg, workspaceDir string) string {
parts := splitWithMatches(text, reInlineCodeSpan)
replacements := make([]placeholderReplacement, 0)
var out strings.Builder
for _, part := range parts {
if !part.matched {
transformed, reps := transformNonCodeText(part.text, cfg, workspaceDir)
if len(replacements) > 0 && len(reps) > 0 {
offset := len(replacements)
for i := range reps {
oldPlaceholder := reps[i].placeholder
newPlaceholder := makeReferencePlaceholder(offset + i)
transformed = strings.ReplaceAll(transformed, oldPlaceholder, newPlaceholder)
reps[i].placeholder = newPlaceholder
}
}
out.WriteString(transformed)
replacements = append(replacements, reps...)
continue
}
match := reInlineCodeSpan.FindStringSubmatch(part.text)
if len(match) < 2 {
out.WriteString(part.text)
continue
}
ref, ok := parseLocalReference(match[1], workspaceDir)
if !ok {
out.WriteString(part.text)
continue
}
placeholder := makeReferencePlaceholder(len(replacements))
replacements = append(replacements, placeholderReplacement{placeholder: placeholder, ref: ref})
out.WriteString(placeholder)
}
return replaceReferencePlaceholders(out.String(), replacements, cfg)
}
func transformNonCodeText(text string, cfg ReferenceRenderCfg, workspaceDir string) (string, []placeholderReplacement) {
replacements := make([]placeholderReplacement, 0)
text = replaceProtectedWebMarkdownLinks(text, &replacements)
text = replaceProtectedLinks(text, reBareURL, &replacements)
text = replaceMarkdownLinks(text, &replacements, workspaceDir)
text = replaceLocalReferenceCandidates(text, reAbsOrFileRef, &replacements, workspaceDir)
text = replaceLocalReferenceCandidates(text, reRelativeRef, &replacements, workspaceDir)
text = replaceLocalReferenceCandidates(text, reBasenameFileRef, &replacements, workspaceDir)
return text, replacements
}
func replaceProtectedLinks(text string, re *regexp.Regexp, replacements *[]placeholderReplacement) string {
matches := re.FindAllStringIndex(text, -1)
if len(matches) == 0 {
return text
}
var out strings.Builder
last := 0
for _, m := range matches {
out.WriteString(text[last:m[0]])
token := text[m[0]:m[1]]
placeholder := makeReferencePlaceholder(len(*replacements))
*replacements = append(*replacements, placeholderReplacement{placeholder: placeholder, keepText: token})
out.WriteString(placeholder)
last = m[1]
}
out.WriteString(text[last:])
return out.String()
}
func replaceProtectedWebMarkdownLinks(text string, replacements *[]placeholderReplacement) string {
matches := reMarkdownLink.FindAllStringSubmatchIndex(text, -1)
if len(matches) == 0 {
return text
}
var out strings.Builder
last := 0
for _, m := range matches {
target := text[m[4]:m[5]]
if !isWebURL(target) {
continue
}
out.WriteString(text[last:m[0]])
token := text[m[0]:m[1]]
placeholder := makeReferencePlaceholder(len(*replacements))
*replacements = append(*replacements, placeholderReplacement{placeholder: placeholder, keepText: token})
out.WriteString(placeholder)
last = m[1]
}
out.WriteString(text[last:])
return out.String()
}
func replaceMarkdownLinks(text string, replacements *[]placeholderReplacement, workspaceDir string) string {
matches := reMarkdownLink.FindAllStringSubmatchIndex(text, -1)
if len(matches) == 0 {
return text
}
var out strings.Builder
last := 0
for _, m := range matches {
out.WriteString(text[last:m[0]])
target := text[m[4]:m[5]]
suffix := ""
if m[6] >= 0 {
suffix = text[m[6]:m[7]]
}
ref, ok := parseLocalReference(target+suffix, workspaceDir)
if !ok {
out.WriteString(text[m[0]:m[1]])
last = m[1]
continue
}
placeholder := makeReferencePlaceholder(len(*replacements))
*replacements = append(*replacements, placeholderReplacement{placeholder: placeholder, ref: ref})
out.WriteString(placeholder)
last = m[1]
}
out.WriteString(text[last:])
return out.String()
}
func replaceLocalReferenceCandidates(text string, re *regexp.Regexp, replacements *[]placeholderReplacement, workspaceDir string) string {
matches := re.FindAllStringIndex(text, -1)
if len(matches) == 0 {
return text
}
var out strings.Builder
last := 0
for _, m := range matches {
out.WriteString(text[last:m[0]])
token := text[m[0]:m[1]]
if re == reAbsOrFileRef && !isValidAbsoluteReferenceBoundary(text, m[0]) {
out.WriteString(token)
last = m[1]
continue
}
if re == reRelativeRef && !isValidRelativeReferenceBoundary(text, m[0]) {
out.WriteString(token)
last = m[1]
continue
}
ref, ok := parseLocalReference(token, workspaceDir)
if !ok {
out.WriteString(token)
last = m[1]
continue
}
placeholder := makeReferencePlaceholder(len(*replacements))
*replacements = append(*replacements, placeholderReplacement{placeholder: placeholder, ref: ref})
out.WriteString(placeholder)
last = m[1]
}
out.WriteString(text[last:])
return out.String()
}
func isValidAbsoluteReferenceBoundary(text string, start int) bool {
if start <= 0 {
return true
}
prev, _ := utf8.DecodeLastRuneInString(text[:start])
switch {
case prev == ' ', prev == '\n', prev == '\t', prev == '\r':
return true
case strings.ContainsRune("([<{\"'`、,,;;。!?!?:", prev):
return true
default:
return false
}
}
func isValidRelativeReferenceBoundary(text string, start int) bool {
if start <= 0 {
return true
}
prev, _ := utf8.DecodeLastRuneInString(text[:start])
switch {
case prev == ' ', prev == '\n', prev == '\t', prev == '\r':
return true
case strings.ContainsRune("([<{\"'`、,,;;。!?!?:", prev):
return true
default:
return false
}
}
func replaceReferencePlaceholders(text string, replacements []placeholderReplacement, cfg ReferenceRenderCfg) string {
if len(replacements) == 0 {
return text
}
basenameCounts := make(map[string]int)
for _, rep := range replacements {
if rep.ref == nil {
continue
}
base := refBaseName(rep.ref)
if base != "" {
basenameCounts[base]++
}
}
sort.SliceStable(replacements, func(i, j int) bool {
return len(replacements[i].placeholder) > len(replacements[j].placeholder)
})
for _, rep := range replacements {
replacement := rep.keepText
if rep.ref != nil {
replacement = renderLocalReference(rep.ref, cfg, basenameCounts)
}
text = strings.ReplaceAll(text, rep.placeholder, replacement)
}
return text
}
type splitPart struct {
text string
matched bool
}
func splitWithMatches(text string, re *regexp.Regexp) []splitPart {
matches := re.FindAllStringIndex(text, -1)
if len(matches) == 0 {
return []splitPart{{text: text}}
}
parts := make([]splitPart, 0, len(matches)*2+1)
last := 0
for _, m := range matches {
if m[0] > last {
parts = append(parts, splitPart{text: text[last:m[0]]})
}
parts = append(parts, splitPart{text: text[m[0]:m[1]], matched: true})
last = m[1]
}
if last < len(text) {
parts = append(parts, splitPart{text: text[last:]})
}
return parts
}
func makeReferencePlaceholder(idx int) string {
return fmt.Sprintf("\x00REF_%03d\x00", idx)
}
func refBaseName(ref *localReference) string {
if ref == nil {
return ""
}
p := referenceDisplaySource(ref, "basename")
if p == "" {
return ""
}
return strings.TrimSuffix(p, "/")
}
func renderLocalReference(ref *localReference, cfg ReferenceRenderCfg, basenameCounts map[string]int) string {
body := referenceDisplaySource(ref, cfg.DisplayPath)
if cfg.DisplayPath == "smart" {
base := refBaseName(ref)
if basenameCounts[base] <= 1 {
body = referenceDisplaySource(ref, "basename")
} else {
body = referenceDisplaySource(ref, "dirname_basename")
if body == base {
body = referenceDisplaySource(ref, "relative")
}
}
}
body += renderReferenceLocation(ref)
body = applyReferenceEnclosure(cfg.EnclosureStyle, body)
return applyReferenceMarker(cfg.MarkerStyle, ref.kind, body)
}
func referenceDisplaySource(ref *localReference, mode string) string {
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "absolute":
if ref.pathAbs != "" {
return appendDirSuffix(ref.pathAbs, ref.kind)
}
return appendDirSuffix(cleanDisplayPath(ref.pathOriginal), ref.kind)
case "relative":
if ref.pathRel == "." {
if ref.kind == referenceKindDir {
return "./"
}
return "."
}
if rel := sanitizeRelativeDisplay(ref.pathRel); rel != "" {
return appendDirSuffix(rel, ref.kind)
}
if ref.isRelative {
return appendDirSuffix(cleanDisplayPath(ref.pathOriginal), ref.kind)
}
if ref.pathAbs != "" {
return appendDirSuffix(ref.pathAbs, ref.kind)
}
return appendDirSuffix(cleanDisplayPath(ref.pathOriginal), ref.kind)
case "basename":
return appendDirSuffix(pathTail(ref, 1), ref.kind)
case "dirname_basename":
return appendDirSuffix(pathTail(ref, 2), ref.kind)
case "smart":
return appendDirSuffix(pathTail(ref, 1), ref.kind)
default:
return appendDirSuffix(pathTail(ref, 2), ref.kind)
}
}
func sanitizeRelativeDisplay(rel string) string {
rel = filepath.ToSlash(strings.TrimSpace(rel))
if rel == "" || rel == "." || rel == ".." || strings.HasPrefix(rel, "../") {
return ""
}
return rel
}
func pathTail(ref *localReference, segs int) string {
source := sanitizeRelativeDisplay(ref.pathRel)
if source == "" {
if ref.isRelative {
source = cleanDisplayPath(ref.pathOriginal)
} else if ref.pathAbs != "" {
source = filepath.ToSlash(ref.pathAbs)
} else {
source = cleanDisplayPath(ref.pathOriginal)
}
}
source = strings.TrimSuffix(source, "/")
parts := strings.Split(filepath.ToSlash(source), "/")
if len(parts) == 0 {
return source
}
if segs <= 0 || len(parts) <= segs {
return source
}
return strings.Join(parts[len(parts)-segs:], "/")
}
func cleanDisplayPath(path string) string {
if path == "" {
return ""
}
path = filepath.ToSlash(path)
path = strings.TrimPrefix(path, "./")
return strings.TrimSpace(path)
}
func appendDirSuffix(path string, kind referenceKind) string {
path = filepath.ToSlash(strings.TrimSpace(path))
if path == "" {
return path
}
if kind == referenceKindDir && !strings.HasSuffix(path, "/") {
return path + "/"
}
return strings.TrimSuffix(path, "/")
}
func renderReferenceLocation(ref *localReference) string {
switch ref.locationFormat {
case referenceLocationColonLine:
return fmt.Sprintf(":%d", ref.lineStart)
case referenceLocationColonLineCol:
return fmt.Sprintf(":%d:%d", ref.lineStart, ref.column)
case referenceLocationColonRange:
return fmt.Sprintf(":%d-%d", ref.lineStart, ref.lineEnd)
case referenceLocationHashLine:
return fmt.Sprintf("#L%d", ref.lineStart)
case referenceLocationHashLineCol:
return fmt.Sprintf("#L%dC%d", ref.lineStart, ref.column)
default:
return ""
}
}
func applyReferenceMarker(style string, kind referenceKind, body string) string {
switch strings.ToLower(strings.TrimSpace(style)) {
case "ascii":
if kind == referenceKindDir {
return "[DIR] " + body
}
if kind == referenceKindFile {
return "[FILE] " + body
}
return body
case "emoji":
if kind == referenceKindDir {
return "📁 " + body
}
if kind == referenceKindFile {
return "📄 " + body
}
return body
default:
return body
}
}
func applyReferenceEnclosure(style, body string) string {
switch strings.ToLower(strings.TrimSpace(style)) {
case "bracket":
return "[" + body + "]"
case "angle":
return "<" + body + ">"
case "fullwidth":
return "【" + body + "】"
case "code":
return "`" + body + "`"
default:
return body
}
}