mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 03:57:06 +08:00
refactor: 0.1.0-beta
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# cursor-proto
|
||||
|
||||
`cursor-proto` 从已安装 Cursor 的 JavaScript bundle 中提取 Protobuf 定义。
|
||||
|
||||
## 目录
|
||||
|
||||
- `extractor/`:Go 提取器。
|
||||
- `scripts/extract.sh`:扫描 Cursor 安装目录并安全更新输出。
|
||||
- `proto/`:提取结果,是项目内唯一的 Proto 输出目录;由脚本重新生成,不提交到 Git。
|
||||
- `scripts/generate.sh`:根据提取结果生成可导入的 Go 消息包。
|
||||
- `gen/`:供其他 Go module 使用的 Go 消息包;由脚本重新生成,不提交到 Git。
|
||||
|
||||
## 使用
|
||||
|
||||
默认从 `/Applications/Cursor.app` 提取:
|
||||
|
||||
```bash
|
||||
./scripts/extract.sh
|
||||
```
|
||||
|
||||
也可以指定 Cursor 应用、bundle 文件和输出目录:
|
||||
|
||||
```bash
|
||||
./scripts/extract.sh /path/to/Cursor.app
|
||||
./scripts/extract.sh /path/to/workbench.desktop.main.js /path/to/output
|
||||
```
|
||||
|
||||
直接运行 Go 提取器时,可重复传入多个 bundle:
|
||||
|
||||
```bash
|
||||
go run ./extractor \
|
||||
-input /path/to/workbench.desktop.main.js \
|
||||
-input /path/to/extensionHostProcess.js \
|
||||
-output ./proto \
|
||||
-strict
|
||||
```
|
||||
|
||||
提取完成后重新生成 Go 消息包:
|
||||
|
||||
```bash
|
||||
./scripts/generate.sh
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
@@ -0,0 +1,192 @@
|
||||
// extractor_test.go 验证压缩 bundle 的字段、别名、服务和合并提取行为。
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestParseFieldObjectSupportsShorthandType 验证字段类型简写可以解析。
|
||||
func TestParseFieldObjectSupportsShorthandType(t *testing.T) {
|
||||
field, err := parseFieldObject(`{no:4,name:"file_not_found",kind:"message",T,oneof:"result"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("parse shorthand T: %v", err)
|
||||
}
|
||||
if field.T != "T" {
|
||||
t.Fatalf("parsed shorthand T as %#v, want T", field.T)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWebpackExportAliasResolvesServiceMessageType 验证 Webpack 导出别名可解析服务消息。
|
||||
func TestWebpackExportAliasResolvesServiceMessageType(t *testing.T) {
|
||||
const bundle = `
|
||||
1:(e,t,n)=>{
|
||||
n.d(t,{KS:()=>T,_B:()=>r});
|
||||
var r;
|
||||
class T {}
|
||||
T.typeName="agent.v1.AgentClientMessage";
|
||||
n.proto3.util.setEnumType(r,"agent.v1.DiagnosticSeverity",[]);
|
||||
},
|
||||
2:(e,t,n)=>{
|
||||
var r=n(1);
|
||||
const service={typeName:"agent.v1.AgentService",methods:{run:{name:"Run",I:r.KS,O:r.KS,kind:n.MethodKind.BiDiStreaming}}};
|
||||
}`
|
||||
|
||||
moduleStarts := buildModuleStarts(bundle)
|
||||
messages := []Message{{
|
||||
TypeName: "agent.v1.AgentClientMessage",
|
||||
VarName: "T",
|
||||
InternalName: "T",
|
||||
Package: "agent.v1",
|
||||
Pos: 35,
|
||||
ModuleStart: moduleStartForPos(moduleStarts, 35),
|
||||
}}
|
||||
enums := []Enum{{
|
||||
TypeName: "agent.v1.DiagnosticSeverity",
|
||||
VarName: "r",
|
||||
Package: "agent.v1",
|
||||
Pos: 100,
|
||||
ModuleStart: moduleStartForPos(moduleStarts, 100),
|
||||
}}
|
||||
|
||||
resolver := newTypeResolver(messages, enums, buildAliasIndex(bundle, moduleStarts), buildWebpackExportAliasIndex(bundle, moduleStarts))
|
||||
resolver.moduleImports = buildModuleImportIndex(bundle, moduleStarts)
|
||||
typeName, ok := resolver.ResolveTypeName("r.KS", len(bundle)-1, moduleStartForPos(moduleStarts, len(bundle)-1), "agent.v1", "message")
|
||||
if !ok {
|
||||
t.Fatal("expected webpack export alias to resolve")
|
||||
}
|
||||
if typeName != "agent.v1.AgentClientMessage" {
|
||||
t.Fatalf("resolved r.KS to %q, want agent.v1.AgentClientMessage", typeName)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolverPrefersExpectedKindOverCurrentPackage 验证类型类别优先于当前包候选。
|
||||
func TestResolverPrefersExpectedKindOverCurrentPackage(t *testing.T) {
|
||||
resolver := &TypeResolver{bySymbol: map[string][]symbolDef{
|
||||
"nt": {
|
||||
{TypeName: "git_forge.v1.GetTagResponse", Kind: "message", Pos: 10, ModuleStart: 1},
|
||||
{TypeName: "origin.v1.TeamGroupKind", Kind: "enum", Pos: 20, ModuleStart: 1},
|
||||
},
|
||||
}}
|
||||
|
||||
typeName, ok := resolver.ResolveTypeName("nt", 30, 1, "origin.v1", "message")
|
||||
if !ok {
|
||||
t.Fatal("expected cross-package message type to resolve")
|
||||
}
|
||||
if typeName != "git_forge.v1.GetTagResponse" {
|
||||
t.Fatalf("resolved nt to %q, want git_forge.v1.GetTagResponse", typeName)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModernFactorySyntaxExtractsInAppAdServiceTypes 验证现代工厂语法提取完整服务类型。
|
||||
func TestModernFactorySyntaxExtractsInAppAdServiceTypes(t *testing.T) {
|
||||
const bundle = `
|
||||
42:(e,t,n)=>{
|
||||
var HasSeenAdRequest=n.makeMessageType("aiserver.v1.HasSeenAdRequest",()=>[{no:1,name:"ad_id",kind:"scalar",T:9}]),
|
||||
HasSeenAdResponse=n.makeMessageType("aiserver.v1.HasSeenAdResponse",()=>[{no:1,name:"has_seen",kind:"scalar",T:8}]),
|
||||
MarkAdAsSeenResponse=n.makeMessageType("aiserver.v1.MarkAdAsSeenResponse",[]),
|
||||
Placement=n.makeEnum("aiserver.v1.InAppAdPlacement",[{no:0,name:"IN_APP_AD_PLACEMENT_UNSPECIFIED",localName:"UNSPECIFIED"}]),
|
||||
InAppAdService={typeName:"aiserver.v1.InAppAdService",methods:{hasSeenAd:{name:"HasSeenAd",I:HasSeenAdRequest,O:HasSeenAdResponse,kind:n.MethodKind.Unary},markAdAsSeen:{name:"MarkAdAsSeen",I:HasSeenAdRequest,O:MarkAdAsSeenResponse,kind:n.MethodKind.Unary}}};
|
||||
}`
|
||||
|
||||
moduleStarts := buildModuleStarts(bundle)
|
||||
messages := extractMessages(bundle, moduleStarts)
|
||||
enums := extractEnums(bundle, moduleStarts)
|
||||
services := extractServices(bundle, moduleStarts)
|
||||
|
||||
if len(messages) != 3 {
|
||||
t.Fatalf("extracted %d messages, want 3", len(messages))
|
||||
}
|
||||
if len(messages[0].Fields) != 1 || messages[0].Fields[0].Name != "ad_id" {
|
||||
t.Fatalf("unexpected request fields: %#v", messages[0].Fields)
|
||||
}
|
||||
if len(enums) != 1 || enums[0].TypeName != "aiserver.v1.InAppAdPlacement" {
|
||||
t.Fatalf("unexpected enums: %#v", enums)
|
||||
}
|
||||
if len(services) != 1 || len(services[0].Methods) != 2 {
|
||||
t.Fatalf("unexpected services: %#v", services)
|
||||
}
|
||||
|
||||
resolver := newTypeResolver(messages, enums, buildAliasIndex(bundle, moduleStarts), buildWebpackExportAliasIndex(bundle, moduleStarts))
|
||||
method := services[0].Methods[0]
|
||||
input, inputOK := resolver.ResolveTypeName(method.InputType, services[0].Pos, services[0].ModuleStart, services[0].Package, "message")
|
||||
output, outputOK := resolver.ResolveTypeName(method.OutputType, services[0].Pos, services[0].ModuleStart, services[0].Package, "message")
|
||||
if !inputOK || input != "aiserver.v1.HasSeenAdRequest" {
|
||||
t.Fatalf("resolved input to %q (ok=%v)", input, inputOK)
|
||||
}
|
||||
if !outputOK || output != "aiserver.v1.HasSeenAdResponse" {
|
||||
t.Fatalf("resolved output to %q (ok=%v)", output, outputOK)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssignmentAliasResolvesStandardProtobufType 验证赋值别名解析标准协议类型。
|
||||
func TestAssignmentAliasResolvesStandardProtobufType(t *testing.T) {
|
||||
const bundle = `
|
||||
1:(e,t,n)=>{
|
||||
var Timestamp=class TimestampMessage extends Base{};
|
||||
Timestamp.typeName="google.protobuf.Timestamp",Timestamp.fields=n.proto3.util.newFieldList(()=>[]),ua=Timestamp;
|
||||
var Request=n.makeMessageType("aiserver.v1.Request",()=>[{no:1,name:"created_at",kind:"message",T:ua}]);
|
||||
}`
|
||||
|
||||
moduleStarts := buildModuleStarts(bundle)
|
||||
messages := extractMessages(bundle, moduleStarts)
|
||||
resolver := newTypeResolver(messages, nil, buildAliasIndex(bundle, moduleStarts), nil)
|
||||
|
||||
typeName, ok := resolver.ResolveTypeName("ua", len(bundle)-1, moduleStartForPos(moduleStarts, len(bundle)-1), "aiserver.v1", "message")
|
||||
if !ok || typeName != "google.protobuf.Timestamp" {
|
||||
t.Fatalf("resolved ua to %q (ok=%v), want google.protobuf.Timestamp", typeName, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeclarationCoverageReportsUnparsedTypesAndIgnoresGoogleTypes 验证覆盖率忽略标准类型并报告遗漏。
|
||||
func TestDeclarationCoverageReportsUnparsedTypesAndIgnoresGoogleTypes(t *testing.T) {
|
||||
const bundle = `
|
||||
var Request=n.makeMessageType("aiserver.v1.Request",()=>[]);
|
||||
var Missing=n.makeMessageType("aiserver.v1.Missing",()=>[]);
|
||||
var Timestamp=n.makeMessageType("google.protobuf.Timestamp",()=>[]);
|
||||
var Service={typeName:"aiserver.v1.TestService",methods:{}};
|
||||
`
|
||||
messages := []Message{{TypeName: "aiserver.v1.Request"}}
|
||||
services := []Service{{TypeName: "aiserver.v1.TestService"}}
|
||||
|
||||
declared, extracted, missing := declarationCoverage(bundle, messages, nil, services)
|
||||
if declared != 3 || extracted != 2 {
|
||||
t.Fatalf("coverage=%d/%d, want 2/3", extracted, declared)
|
||||
}
|
||||
if len(missing) != 1 || missing[0] != "aiserver.v1.Missing" {
|
||||
t.Fatalf("unexpected missing declarations: %#v", missing)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractServicesSupportsAnonymousDescriptors 验证匿名服务描述符可以提取。
|
||||
func TestExtractServicesSupportsAnonymousDescriptors(t *testing.T) {
|
||||
const bundle = `services.push({typeName:"aiserver.v1.FileSyncService",methods:{sync:{name:"Sync",I:Request,O:Response,kind:n.MethodKind.Unary}}})`
|
||||
services := extractServices(bundle, nil)
|
||||
if len(services) != 1 || services[0].TypeName != "aiserver.v1.FileSyncService" {
|
||||
t.Fatalf("unexpected services: %#v", services)
|
||||
}
|
||||
if len(services[0].Methods) != 1 || services[0].Methods[0].Name != "Sync" {
|
||||
t.Fatalf("unexpected methods: %#v", services[0].Methods)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMergeMessagesPrefersPrimaryBundleAndKeepsSupplementalTypes 验证合并优先主 bundle 并保留补充类型。
|
||||
func TestMergeMessagesPrefersPrimaryBundleAndKeepsSupplementalTypes(t *testing.T) {
|
||||
primary := Message{
|
||||
TypeName: "aiserver.v1.Shared",
|
||||
Fields: []Field{{No: 1, Name: "primary", Kind: "scalar", T: 9}},
|
||||
}
|
||||
supplemental := Message{
|
||||
TypeName: "aiserver.v1.Shared",
|
||||
Fields: []Field{{No: 1, Name: "supplemental", Kind: "scalar", T: 9}},
|
||||
}
|
||||
legacy := Message{TypeName: "aiserver.v1.LegacyOnly"}
|
||||
|
||||
merged := mergeMessagesByTypeName([]Message{primary, supplemental, legacy})
|
||||
if len(merged) != 2 {
|
||||
t.Fatalf("merged %d messages, want 2", len(merged))
|
||||
}
|
||||
if merged[0].Fields[0].Name != "primary" {
|
||||
t.Fatalf("duplicate type did not preserve primary definition: %#v", merged[0])
|
||||
}
|
||||
if merged[1].TypeName != "aiserver.v1.LegacyOnly" {
|
||||
t.Fatalf("supplemental-only type missing: %#v", merged)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// generator.go 计算跨包依赖并为各协议包准备完整声明集合。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// generateProtos 按协议包聚合声明并生成对应文件。
|
||||
func generateProtos(messages []Message, enums []Enum, services []Service, resolver *TypeResolver, outputDir string) {
|
||||
os.MkdirAll(outputDir, 0755)
|
||||
|
||||
// 按协议包聚合声明。
|
||||
packages := make(map[string]struct {
|
||||
messages []Message
|
||||
enums []Enum
|
||||
services []Service
|
||||
})
|
||||
|
||||
for _, msg := range messages {
|
||||
pkg := packages[msg.Package]
|
||||
pkg.messages = append(pkg.messages, msg)
|
||||
packages[msg.Package] = pkg
|
||||
}
|
||||
|
||||
for _, enum := range enums {
|
||||
pkg := packages[enum.Package]
|
||||
pkg.enums = append(pkg.enums, enum)
|
||||
packages[enum.Package] = pkg
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
pkg := packages[svc.Package]
|
||||
pkg.services = append(pkg.services, svc)
|
||||
packages[svc.Package] = pkg
|
||||
}
|
||||
|
||||
// 建立跨包复制使用的全局类型索引。
|
||||
allMessages := make(map[string]*Message)
|
||||
allEnums := make(map[string]*Enum)
|
||||
|
||||
for pkgName, pkg := range packages {
|
||||
if isGooglePkg(pkgName) {
|
||||
continue
|
||||
}
|
||||
for i := range pkg.messages {
|
||||
msg := &pkg.messages[i]
|
||||
allMessages[msg.TypeName] = msg
|
||||
}
|
||||
for i := range pkg.enums {
|
||||
enum := &pkg.enums[i]
|
||||
allEnums[enum.TypeName] = enum
|
||||
}
|
||||
}
|
||||
|
||||
// 每轮生成前重置已复制类型索引。
|
||||
copiedTypes = make(map[string]map[string]string)
|
||||
|
||||
for pkgName, pkg := range packages {
|
||||
// Google 标准包直接使用官方协议文件。
|
||||
if isGooglePkg(pkgName) {
|
||||
fmt.Printf("跳过: %s (使用官方 proto 文件)\n", pkgName)
|
||||
continue
|
||||
}
|
||||
|
||||
// 把当前包引用的外部类型复制到本地。
|
||||
augmentedPkg := copyAllExternalTypes(pkgName, pkg, resolver, allMessages, allEnums)
|
||||
generateProtoFile(pkgName, augmentedPkg.messages, augmentedPkg.enums, pkg.services, resolver, outputDir)
|
||||
}
|
||||
}
|
||||
|
||||
// copyAllExternalTypes 递归复制当前包引用的全部外部类型。
|
||||
func copyAllExternalTypes(pkgName string, pkg struct {
|
||||
messages []Message
|
||||
enums []Enum
|
||||
services []Service
|
||||
}, resolver *TypeResolver, allMessages map[string]*Message, allEnums map[string]*Enum) struct {
|
||||
messages []Message
|
||||
enums []Enum
|
||||
services []Service
|
||||
} {
|
||||
if copiedTypes[pkgName] == nil {
|
||||
copiedTypes[pkgName] = make(map[string]string)
|
||||
}
|
||||
|
||||
// 建立当前包已有类型集合,并登记本地名称供字段解析使用。
|
||||
localTypes := make(map[string]bool)
|
||||
for _, msg := range pkg.messages {
|
||||
localTypes[msg.ShortName] = true
|
||||
// 空来源名表示该类型原本就在当前包。
|
||||
if copiedTypes[pkgName][msg.ShortName] == "" {
|
||||
copiedTypes[pkgName][msg.ShortName] = "local:" + msg.TypeName
|
||||
}
|
||||
}
|
||||
for _, enum := range pkg.enums {
|
||||
localTypes[enum.ShortName] = true
|
||||
if copiedTypes[pkgName][enum.ShortName] == "" {
|
||||
copiedTypes[pkgName][enum.ShortName] = "local:" + enum.TypeName
|
||||
}
|
||||
}
|
||||
|
||||
// 结果先保留当前包原始声明。
|
||||
result := struct {
|
||||
messages []Message
|
||||
enums []Enum
|
||||
services []Service
|
||||
}{
|
||||
messages: append([]Message{}, pkg.messages...),
|
||||
enums: append([]Enum{}, pkg.enums...),
|
||||
services: pkg.services,
|
||||
}
|
||||
|
||||
totalCopied := 0
|
||||
|
||||
// 持续迭代,直到不再发现新的外部依赖。
|
||||
for round := 1; ; round++ {
|
||||
// 收集当前消息中的外部类型引用。
|
||||
neededTypes := make(map[string]bool)
|
||||
|
||||
for _, msg := range result.messages {
|
||||
preferredPkg, _ := parseTypeName(msg.TypeName)
|
||||
for _, f := range msg.Fields {
|
||||
collectFieldRefsSimple(f, pkgName, preferredPkg, msg.Pos, msg.ModuleStart, resolver, neededTypes, localTypes)
|
||||
}
|
||||
}
|
||||
for _, svc := range result.services {
|
||||
for _, m := range svc.Methods {
|
||||
collectMethodRefsSimple(m.InputType, pkgName, svc.Pos, svc.ModuleStart, resolver, neededTypes, localTypes)
|
||||
collectMethodRefsSimple(m.OutputType, pkgName, svc.Pos, svc.ModuleStart, resolver, neededTypes, localTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制本轮新增依赖类型。
|
||||
copiedThisRound := 0
|
||||
for typeName := range neededTypes {
|
||||
refPkg, shortName := parseTypeName(typeName)
|
||||
if refPkg == pkgName || isGooglePkg(refPkg) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 已存在于本地时无需重复复制。
|
||||
if localTypes[shortName] {
|
||||
continue
|
||||
}
|
||||
|
||||
// 复制消息声明。
|
||||
if msg, ok := allMessages[typeName]; ok {
|
||||
msgCopy := *msg
|
||||
msgCopy.Package = pkgName
|
||||
// 保留原始完整类型名,用于生成来源注释。
|
||||
result.messages = append(result.messages, msgCopy)
|
||||
copiedTypes[pkgName][shortName] = typeName // 保存原始完整类型名。
|
||||
localTypes[shortName] = true
|
||||
copiedThisRound++
|
||||
fmt.Printf(" [%s] 轮%d 复制: %s\n", pkgName, round, typeName)
|
||||
} else if enum, ok := allEnums[typeName]; ok {
|
||||
// 复制枚举声明。
|
||||
enumCopy := *enum
|
||||
enumCopy.Package = pkgName
|
||||
result.enums = append(result.enums, enumCopy)
|
||||
copiedTypes[pkgName][shortName] = typeName
|
||||
localTypes[shortName] = true
|
||||
copiedThisRound++
|
||||
fmt.Printf(" [%s] 轮%d 复制枚举: %s\n", pkgName, round, typeName)
|
||||
} else {
|
||||
// 未找到声明时仍登记本地引用,兼容提取结果缺少但 bundle 实际存在的类型。
|
||||
copiedTypes[pkgName][shortName] = typeName
|
||||
localTypes[shortName] = true
|
||||
fmt.Printf(" [%s] 轮%d 警告: 类型未找到 %s,标记为本地引用\n", pkgName, round, typeName)
|
||||
}
|
||||
}
|
||||
|
||||
totalCopied += copiedThisRound
|
||||
|
||||
if copiedThisRound == 0 {
|
||||
break // 没有新增依赖时结束迭代。
|
||||
}
|
||||
|
||||
if round > 20 {
|
||||
fmt.Printf(" [%s] 警告: 复制轮次超过20,可能存在问题\n", pkgName)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if totalCopied > 0 {
|
||||
fmt.Printf(" [%s] 共复制 %d 个外部类型\n", pkgName, totalCopied)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// collectFieldRefsSimple 收集单个字段直接引用的外部类型。
|
||||
func collectFieldRefsSimple(f Field, currentPkg string, preferredPkg string, contextPos int, contextModuleStart int, resolver *TypeResolver,
|
||||
neededTypes map[string]bool, localTypes map[string]bool) {
|
||||
|
||||
type refWithKind struct {
|
||||
ref string
|
||||
kind string
|
||||
}
|
||||
|
||||
var refs []refWithKind
|
||||
if f.Kind == "message" || f.Kind == "enum" {
|
||||
if v, ok := f.T.(string); ok {
|
||||
refs = append(refs, refWithKind{ref: v, kind: f.Kind})
|
||||
}
|
||||
}
|
||||
if f.Kind == "map" && (f.MapValueKind == "message" || f.MapValueKind == "enum") {
|
||||
if v, ok := f.MapValueT.(string); ok {
|
||||
refs = append(refs, refWithKind{ref: v, kind: f.MapValueKind})
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range refs {
|
||||
typeName, ok := resolver.ResolveTypeName(item.ref, contextPos, contextModuleStart, preferredPkg, item.kind)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
refPkg, shortName := parseTypeName(typeName)
|
||||
if refPkg == "" || refPkg == currentPkg || isGooglePkg(refPkg) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 已在当前包中的类型无需收集。
|
||||
if localTypes[shortName] {
|
||||
continue
|
||||
}
|
||||
|
||||
neededTypes[typeName] = true
|
||||
}
|
||||
}
|
||||
|
||||
// collectMethodRefsSimple 收集服务方法输入或输出引用的外部类型。
|
||||
func collectMethodRefsSimple(ref string, currentPkg string, contextPos int, contextModuleStart int, resolver *TypeResolver,
|
||||
neededTypes map[string]bool, localTypes map[string]bool) {
|
||||
|
||||
typeName, ok := resolver.ResolveTypeName(ref, contextPos, contextModuleStart, currentPkg, "message")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
refPkg, shortName := parseTypeName(typeName)
|
||||
if refPkg == "" || refPkg == currentPkg || isGooglePkg(refPkg) {
|
||||
return
|
||||
}
|
||||
|
||||
if localTypes[shortName] {
|
||||
return
|
||||
}
|
||||
|
||||
neededTypes[typeName] = true
|
||||
}
|
||||
|
||||
// copiedTypes 按目标包和短名称记录被复制类型的原始全限定名。
|
||||
var copiedTypes = make(map[string]map[string]string)
|
||||
|
||||
// TypeNode 表示嵌套消息与枚举组成的类型树节点。
|
||||
type TypeNode struct {
|
||||
// Name 是当前嵌套层级的类型名。
|
||||
Name string
|
||||
// Message 保存当前节点的消息声明。
|
||||
Message *Message
|
||||
// Enum 保存当前节点的枚举声明。
|
||||
Enum *Enum
|
||||
// Children 保存下一层嵌套类型。
|
||||
Children map[string]*TypeNode
|
||||
}
|
||||
|
||||
// collectImports 只收集 Google 标准依赖,其余类型会复制到本地。
|
||||
func collectImports(currentPkg string, messages []Message, services []Service, resolver *TypeResolver) map[string]bool {
|
||||
imports := make(map[string]bool)
|
||||
|
||||
addImport := func(ref string, contextPos int, contextModuleStart int, expectedKind string) {
|
||||
typeName, ok := resolver.ResolveTypeName(ref, contextPos, contextModuleStart, currentPkg, expectedKind)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
refPkg, shortName := parseTypeName(typeName)
|
||||
// 仅导入 Google 标准类型。
|
||||
if refPkg == "google.protobuf" {
|
||||
var importFile string
|
||||
switch shortName {
|
||||
case "Struct", "Value", "ListValue", "NullValue":
|
||||
importFile = "google/protobuf/struct.proto"
|
||||
case "Timestamp":
|
||||
importFile = "google/protobuf/timestamp.proto"
|
||||
case "Duration":
|
||||
importFile = "google/protobuf/duration.proto"
|
||||
case "Any":
|
||||
importFile = "google/protobuf/any.proto"
|
||||
case "Empty":
|
||||
importFile = "google/protobuf/empty.proto"
|
||||
case "FieldMask":
|
||||
importFile = "google/protobuf/field_mask.proto"
|
||||
case "BoolValue", "BytesValue", "DoubleValue", "FloatValue",
|
||||
"Int32Value", "Int64Value", "StringValue", "UInt32Value", "UInt64Value":
|
||||
importFile = "google/protobuf/wrappers.proto"
|
||||
default:
|
||||
importFile = "google/protobuf/descriptor.proto"
|
||||
}
|
||||
imports[importFile] = true
|
||||
} else if refPkg == "google.rpc" {
|
||||
var importFile string
|
||||
switch shortName {
|
||||
case "Status":
|
||||
importFile = "google/rpc/status.proto"
|
||||
case "Code":
|
||||
importFile = "google/rpc/code.proto"
|
||||
default:
|
||||
importFile = "google/rpc/status.proto"
|
||||
}
|
||||
imports[importFile] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
for _, f := range msg.Fields {
|
||||
if f.Kind == "message" || f.Kind == "enum" {
|
||||
if ref, ok := f.T.(string); ok {
|
||||
addImport(ref, msg.Pos, msg.ModuleStart, f.Kind)
|
||||
}
|
||||
}
|
||||
// map 值类型也可能引用标准包。
|
||||
if f.Kind == "map" && (f.MapValueKind == "message" || f.MapValueKind == "enum") {
|
||||
if ref, ok := f.MapValueT.(string); ok {
|
||||
addImport(ref, msg.Pos, msg.ModuleStart, f.MapValueKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
for _, m := range svc.Methods {
|
||||
addImport(m.InputType, svc.Pos, svc.ModuleStart, "message")
|
||||
addImport(m.OutputType, svc.Pos, svc.ModuleStart, "message")
|
||||
}
|
||||
}
|
||||
|
||||
return imports
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// main.go 提供协议提取命令的参数解析、输入保护和输出调度。
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// inputPaths 支持命令行重复传入 bundle 路径。
|
||||
type inputPaths []string
|
||||
|
||||
// String 返回已经登记的输入路径列表。
|
||||
func (paths *inputPaths) String() string {
|
||||
return fmt.Sprint([]string(*paths))
|
||||
}
|
||||
|
||||
// Set 追加一个去除空白后的输入路径。
|
||||
func (paths *inputPaths) Set(value string) error {
|
||||
*paths = append(*paths, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// bailIf 在不可恢复错误时打印信息并退出。
|
||||
func bailIf(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// findPrettier 定位可用的 prettier 命令。
|
||||
func findPrettier() (string, error) {
|
||||
// 尝试常见的 prettier 命令名
|
||||
names := []string{"prettier", "prettier.cmd", "npx"}
|
||||
for _, name := range names {
|
||||
if path, err := exec.LookPath(name); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("prettier not found in PATH, please install: npm install -g prettier")
|
||||
}
|
||||
|
||||
// main 解析参数、保护原始输入并执行协议提取。
|
||||
func main() {
|
||||
// 命令行参数
|
||||
var inputs inputPaths
|
||||
flag.Var(&inputs, "input", "Path to a JS bundle; repeat to merge multiple bundles")
|
||||
outputDir := flag.String("output", "", "Output directory for proto files (default: ./cursor_proto)")
|
||||
skipFormat := flag.Bool("skip-format", false, "Skip prettier formatting")
|
||||
strict := flag.Bool("strict", true, "Fail when extraction validation detects unresolved/placeholder output")
|
||||
flag.Parse()
|
||||
|
||||
// 如果没有 -input 参数,尝试从位置参数获取
|
||||
if len(inputs) == 0 && flag.NArg() > 0 {
|
||||
inputs = append(inputs, flag.Args()...)
|
||||
}
|
||||
|
||||
if len(inputs) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Usage: ext -input <path-to-js-file> [-input <another-js-file>] [-output <dir>] [-skip-format]")
|
||||
fmt.Fprintln(os.Stderr, " ext <path-to-js-file>")
|
||||
fmt.Fprintln(os.Stderr, "\nExample:")
|
||||
fmt.Fprintln(os.Stderr, " ext -input /path/to/extensionHostProcess.js")
|
||||
fmt.Fprintln(os.Stderr, " ext C:\\Users\\xxx\\AppData\\Local\\Programs\\cursor\\resources\\app\\out\\vs\\workbench\\api\\node\\extensionHostProcess.js")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, inputPath := range inputs {
|
||||
info, err := os.Stat(inputPath)
|
||||
bailIf(err)
|
||||
if info.IsDir() {
|
||||
bailIf(fmt.Errorf("expected %s to be file, is dir", inputPath))
|
||||
}
|
||||
}
|
||||
|
||||
// 设置输出目录
|
||||
if *outputDir == "" {
|
||||
wd, err := os.Getwd()
|
||||
bailIf(err)
|
||||
*outputDir = filepath.Join(wd, "cursor_proto")
|
||||
}
|
||||
|
||||
// 复制到临时文件后再格式化,避免修改 Cursor 安装目录。
|
||||
fmt.Printf("Copying %d source bundle(s) to temp directory...\n", len(inputs))
|
||||
tempFileNames := make([]string, 0, len(inputs))
|
||||
for _, inputPath := range inputs {
|
||||
originalFile, err := os.Open(inputPath)
|
||||
bailIf(err)
|
||||
tempFile, err := os.CreateTemp(os.TempDir(), "cursor-source-*.js")
|
||||
bailIf(err)
|
||||
_, err = io.Copy(tempFile, originalFile)
|
||||
bailIf(err)
|
||||
bailIf(originalFile.Close())
|
||||
bailIf(tempFile.Close())
|
||||
tempFileNames = append(tempFileNames, tempFile.Name())
|
||||
fmt.Printf("Source: %s\n", inputPath)
|
||||
}
|
||||
|
||||
if *skipFormat {
|
||||
fmt.Println("Skipping formatting (--skip-format)")
|
||||
} else if prettierBin, err := findPrettier(); err != nil {
|
||||
fmt.Printf("Warning: %v\n", err)
|
||||
fmt.Println("Skipping formatting, extraction may be less accurate...")
|
||||
} else {
|
||||
fmt.Println("Formatting source bundles (this may take a while)...")
|
||||
for _, tempFileName := range tempFileNames {
|
||||
var prettierCmd *exec.Cmd
|
||||
if filepath.Base(prettierBin) == "npx" {
|
||||
prettierCmd = exec.Command(prettierBin, "prettier", "--write", tempFileName)
|
||||
} else {
|
||||
prettierCmd = exec.Command(prettierBin, "--write", tempFileName)
|
||||
}
|
||||
out, formatErr := prettierCmd.CombinedOutput()
|
||||
if formatErr != nil {
|
||||
fmt.Printf("Prettier output: %s\n", string(out))
|
||||
fmt.Println("Warning: formatting failed for one bundle, continuing anyway...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 运行提取器
|
||||
fmt.Println("Extracting Proto definitions...")
|
||||
SetStrictMode(*strict)
|
||||
ExtractProtosFromFiles(tempFileNames, *outputDir)
|
||||
|
||||
for _, tempFileName := range tempFileNames {
|
||||
_ = os.Remove(tempFileName)
|
||||
}
|
||||
|
||||
fmt.Printf("\nOutput directory: %s\n", *outputDir)
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// messages.go 解析消息声明、字段数组和字段类型信息。
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// extractMessages 从多种 bundle 语法中提取消息声明。
|
||||
func extractMessages(text string, moduleStarts []int) []Message {
|
||||
var messages []Message
|
||||
messageExists := func(typeName, varName string) bool {
|
||||
for _, existing := range messages {
|
||||
if existing.TypeName == typeName && existing.VarName == varName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 形式一:变量引用继承基类并在类体中声明 typeName 和 fields。
|
||||
// 先找所有 "变量名 = class 内部类名" 定义
|
||||
// JS 变量名可以包含 $ 符号,如 B$e, qg 等
|
||||
// 需要同时捕获外部变量名和内部类名,因为字段引用可能用任一个
|
||||
classDefRe := regexp.MustCompile(`([\w$]+)\s*=\s*class\s+([\w$]+)\s+extends\s+[\w$.]+\s*\{`)
|
||||
classMatches := classDefRe.FindAllStringSubmatchIndex(text, -1)
|
||||
|
||||
// 从任意包的 this.typeName 字段读取完整类型名。
|
||||
typeNameRe := regexp.MustCompile(`this\.typeName\s*=\s*"([\w.]+)"`)
|
||||
|
||||
// 从 this.fields 的 newFieldList 回调读取字段数组。
|
||||
fieldsRe := regexp.MustCompile(`this\.fields\s*=\s*\w+(?:\.proto3)?\.util\.newFieldList\s*\(\s*\(\s*\)\s*=>\s*\[`)
|
||||
|
||||
for _, classMatch := range classMatches {
|
||||
varName := text[classMatch[2]:classMatch[3]]
|
||||
internalName := text[classMatch[4]:classMatch[5]]
|
||||
classStart := classMatch[0]
|
||||
|
||||
// 找到类的结束位置(匹配大括号)
|
||||
classEnd := findClassEnd(text, classMatch[1]-1)
|
||||
if classEnd == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
classBody := text[classStart:classEnd]
|
||||
|
||||
// 在类体内查找 typeName
|
||||
typeMatch := typeNameRe.FindStringSubmatch(classBody)
|
||||
if typeMatch == nil {
|
||||
continue
|
||||
}
|
||||
typeName := typeMatch[1]
|
||||
|
||||
// 在类体内查找 fields
|
||||
fieldsMatch := fieldsRe.FindStringIndex(classBody)
|
||||
if fieldsMatch == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 找到 fields 数组的开始位置
|
||||
bracketPos := classStart + fieldsMatch[1] - 1
|
||||
fields := extractFieldArray(text, bracketPos)
|
||||
|
||||
pkg, shortName := parseTypeName(typeName)
|
||||
msg := Message{
|
||||
TypeName: typeName,
|
||||
VarName: varName,
|
||||
InternalName: internalName,
|
||||
Fields: fields,
|
||||
Package: pkg,
|
||||
ShortName: shortName,
|
||||
Pos: classStart,
|
||||
ModuleStart: moduleStartForPos(moduleStarts, classStart),
|
||||
}
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
|
||||
// 形式二:匹配转译或压缩 bundle 中连续赋值的消息声明。
|
||||
// 例如 i.runtime=n.proto3,i.typeName="agent.v1.McpArgs",i.fields=n.proto3.util.newFieldList(()=>[{...}])。
|
||||
assignmentRe := regexp.MustCompile(`([\w$]+)\.typeName\s*=\s*"([\w.]+)"\s*,\s*[\w$]+\.fields\s*=\s*\w+(?:\.\w+)*\.util\.newFieldList\s*\(\s*\(\s*\)\s*=>\s*\[`)
|
||||
assignmentMatches := assignmentRe.FindAllStringSubmatchIndex(text, -1)
|
||||
for _, m := range assignmentMatches {
|
||||
varName := text[m[2]:m[3]]
|
||||
typeName := text[m[4]:m[5]]
|
||||
|
||||
// 跳过已经由类体形式提取的重复消息。
|
||||
if messageExists(typeName, varName) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 正则停在左方括号之前,从匹配尾部定位数组起点。
|
||||
start := m[1] - 1
|
||||
if start < 0 || start >= len(text) || text[start] != '[' {
|
||||
continue
|
||||
}
|
||||
fields := extractFieldArray(text, start)
|
||||
|
||||
pkg, shortName := parseTypeName(typeName)
|
||||
messages = append(messages, Message{
|
||||
TypeName: typeName,
|
||||
VarName: varName,
|
||||
InternalName: "",
|
||||
Fields: fields,
|
||||
Package: pkg,
|
||||
ShortName: shortName,
|
||||
Pos: m[0],
|
||||
ModuleStart: moduleStartForPos(moduleStarts, m[0]),
|
||||
})
|
||||
}
|
||||
|
||||
// 形式三:匹配现代 @bufbuild/protobuf 工厂调用。
|
||||
// 例如 Req=A.makeMessageType("aiserver.v1.HasSeenAdRequest",()=>[{...}])。
|
||||
messageFactoryRe := regexp.MustCompile(`([\w$]+)\s*=\s*[\w$.]+\.makeMessageType\s*\(\s*["']([\w.]+)["']\s*,\s*\(\s*\)\s*=>\s*\[`)
|
||||
factoryMatches := messageFactoryRe.FindAllStringSubmatchIndex(text, -1)
|
||||
for _, m := range factoryMatches {
|
||||
varName := text[m[2]:m[3]]
|
||||
typeName := text[m[4]:m[5]]
|
||||
if messageExists(typeName, varName) {
|
||||
continue
|
||||
}
|
||||
|
||||
bracketStart := m[1] - 1
|
||||
if bracketStart < 0 || bracketStart >= len(text) || text[bracketStart] != '[' {
|
||||
continue
|
||||
}
|
||||
|
||||
pkg, shortName := parseTypeName(typeName)
|
||||
messages = append(messages, Message{
|
||||
TypeName: typeName,
|
||||
VarName: varName,
|
||||
Fields: extractFieldArray(text, bracketStart),
|
||||
Package: pkg,
|
||||
ShortName: shortName,
|
||||
Pos: m[0],
|
||||
ModuleStart: moduleStartForPos(moduleStarts, m[0]),
|
||||
})
|
||||
}
|
||||
|
||||
// 空消息直接传字段数组,不使用延迟回调。
|
||||
// 例如 Res=A.makeMessageType("aiserver.v1.MarkAdAsSeenResponse",[])。
|
||||
emptyMessageFactoryRe := regexp.MustCompile(`([\w$]+)\s*=\s*[\w$.]+\.makeMessageType\s*\(\s*["']([\w.]+)["']\s*,\s*\[`)
|
||||
emptyFactoryMatches := emptyMessageFactoryRe.FindAllStringSubmatchIndex(text, -1)
|
||||
for _, m := range emptyFactoryMatches {
|
||||
varName := text[m[2]:m[3]]
|
||||
typeName := text[m[4]:m[5]]
|
||||
if messageExists(typeName, varName) {
|
||||
continue
|
||||
}
|
||||
|
||||
bracketStart := m[1] - 1
|
||||
if bracketStart < 0 || bracketStart >= len(text) || text[bracketStart] != '[' {
|
||||
continue
|
||||
}
|
||||
|
||||
pkg, shortName := parseTypeName(typeName)
|
||||
messages = append(messages, Message{
|
||||
TypeName: typeName,
|
||||
VarName: varName,
|
||||
Fields: extractFieldArray(text, bracketStart),
|
||||
Package: pkg,
|
||||
ShortName: shortName,
|
||||
Pos: m[0],
|
||||
ModuleStart: moduleStartForPos(moduleStarts, m[0]),
|
||||
})
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// findClassEnd 查找类定义的配对右花括号。
|
||||
func findClassEnd(text string, openBrace int) int {
|
||||
depth := 0
|
||||
for i := openBrace; i < len(text); i++ {
|
||||
if text[i] == '{' {
|
||||
depth++
|
||||
} else if text[i] == '}' {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// extractFieldArray 从左方括号位置解析完整字段数组。
|
||||
func extractFieldArray(text string, start int) []Field {
|
||||
// 查找字段数组的配对右方括号。
|
||||
depth := 0
|
||||
end := start
|
||||
for i := start; i < len(text); i++ {
|
||||
if text[i] == '[' {
|
||||
depth++
|
||||
} else if text[i] == ']' {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
end = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arrayText := text[start:end]
|
||||
|
||||
// 按每个花括号块解析独立字段对象。
|
||||
var fields []Field
|
||||
|
||||
// 依次查找字段对象。
|
||||
fieldObjects := extractFieldObjects(arrayText)
|
||||
|
||||
for _, fieldObj := range fieldObjects {
|
||||
field, parseErr := parseFieldObject(fieldObj)
|
||||
if parseErr != nil {
|
||||
activeDiagnostics.addSkippedField(fieldObj, parseErr)
|
||||
continue
|
||||
}
|
||||
activeDiagnostics.addParsedField()
|
||||
fields = append(fields, *field)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// extractFieldObjects 从数组文本中提取独立字段对象。
|
||||
func extractFieldObjects(arrayText string) []string {
|
||||
var objects []string
|
||||
depth := 0
|
||||
start := -1
|
||||
|
||||
for i := 0; i < len(arrayText); i++ {
|
||||
if arrayText[i] == '{' {
|
||||
if depth == 0 {
|
||||
start = i
|
||||
}
|
||||
depth++
|
||||
} else if arrayText[i] == '}' {
|
||||
depth--
|
||||
if depth == 0 && start >= 0 {
|
||||
objects = append(objects, arrayText[start:i+1])
|
||||
start = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return objects
|
||||
}
|
||||
|
||||
// parseFieldObject 解析包含编号、名称、类型和修饰符的单个字段对象。
|
||||
func parseFieldObject(obj string) (*Field, error) {
|
||||
// 提取字段编号。
|
||||
noMatch := noRe.FindStringSubmatch(obj)
|
||||
if noMatch == nil {
|
||||
return nil, errors.New("missing field no")
|
||||
}
|
||||
no, _ := strconv.Atoi(noMatch[1])
|
||||
|
||||
// 提取字段名称。
|
||||
nameMatch := nameRe.FindStringSubmatch(obj)
|
||||
if nameMatch == nil {
|
||||
return nil, errors.New("missing field name")
|
||||
}
|
||||
name := strings.TrimSpace(nameMatch[1])
|
||||
if !fieldNameRe.MatchString(name) {
|
||||
return nil, fmt.Errorf("invalid field name: %s", name)
|
||||
}
|
||||
|
||||
// 提取字段类别。
|
||||
kindMatch := kindRe.FindStringSubmatch(obj)
|
||||
if kindMatch == nil {
|
||||
return nil, errors.New("missing field kind")
|
||||
}
|
||||
kind := strings.TrimSpace(kindMatch[1])
|
||||
|
||||
field := &Field{
|
||||
No: no,
|
||||
Name: name,
|
||||
Kind: kind,
|
||||
}
|
||||
|
||||
// 类型 T 可以是标量编号、变量名或 getEnumType 枚举调用。
|
||||
|
||||
// 枚举优先匹配 getEnumType 调用。
|
||||
if enumMatch := enumTypeRe.FindStringSubmatch(obj); enumMatch != nil {
|
||||
field.T = enumMatch[1]
|
||||
} else {
|
||||
// 其余类型匹配普通 T 属性值。
|
||||
if tMatch := tRe.FindStringSubmatch(obj); tMatch != nil {
|
||||
if t, err := strconv.Atoi(tMatch[1]); err == nil {
|
||||
field.T = t
|
||||
} else {
|
||||
field.T = tMatch[1]
|
||||
}
|
||||
} else if shorthandTRe.MatchString(obj) {
|
||||
field.T = "T"
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在当前字段对象内检查 oneof 分组。
|
||||
if oneofMatch := oneofRe.FindStringSubmatch(obj); oneofMatch != nil {
|
||||
candidate := strings.TrimSpace(oneofMatch[1])
|
||||
if oneofNameRe.MatchString(candidate) {
|
||||
field.Oneof = candidate
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在当前字段对象内检查 repeated;压缩 JS 中 !0 表示真。
|
||||
if repeatedRe.MatchString(obj) {
|
||||
field.Repeated = true
|
||||
}
|
||||
|
||||
// 仅在当前字段对象内检查 optional。
|
||||
if optRe.MatchString(obj) {
|
||||
field.Opt = true
|
||||
}
|
||||
|
||||
// map 字段通过 K 键类型和 V 值描述共同表示。
|
||||
if field.Kind == "map" {
|
||||
// 提取 map 键类型。
|
||||
if keyMatch := keyRe.FindStringSubmatch(obj); keyMatch != nil {
|
||||
field.MapKey, _ = strconv.Atoi(keyMatch[1])
|
||||
}
|
||||
|
||||
// 提取 map 值类型,兼容属性顺序变化。
|
||||
if valueMatch := mapValueRe.FindStringSubmatch(obj); valueMatch != nil {
|
||||
valueObj := valueMatch[1]
|
||||
if kindMatch := mapValueKRe.FindStringSubmatch(valueObj); kindMatch != nil {
|
||||
field.MapValueKind = kindMatch[1]
|
||||
}
|
||||
if tMatch := mapValueTRe.FindStringSubmatch(valueObj); tMatch != nil {
|
||||
if t, err := strconv.Atoi(tMatch[1]); err == nil {
|
||||
field.MapValueT = t
|
||||
} else {
|
||||
field.MapValueT = tMatch[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return field, nil
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
// modules.go 扫描模块边界、合并声明并执行提取结果校验。
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jhump/protoreflect/desc"
|
||||
"github.com/jhump/protoreflect/desc/protoparse"
|
||||
)
|
||||
|
||||
// moduleStartRe 匹配 Webpack 数字模块的函数起点。
|
||||
var moduleStartRe = regexp.MustCompile(`(?:^|,)\s*(\d+)\s*:\s*(?:function\s*\(\s*[\w$,\s]*\s*\)|\(\s*[\w$,\s]*\s*\)\s*=>)\s*\{`)
|
||||
|
||||
// buildModuleStarts 收集 bundle 内全部模块起始位置。
|
||||
func buildModuleStarts(text string) []int {
|
||||
matches := moduleStartRe.FindAllStringSubmatchIndex(text, -1)
|
||||
starts := make([]int, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
starts = append(starts, match[0])
|
||||
}
|
||||
return starts
|
||||
}
|
||||
|
||||
// moduleStartForPos 查找指定源码位置所属的模块起点。
|
||||
func moduleStartForPos(moduleStarts []int, pos int) int {
|
||||
if len(moduleStarts) == 0 {
|
||||
return 0
|
||||
}
|
||||
index := sort.Search(len(moduleStarts), func(i int) bool {
|
||||
return moduleStarts[i] > pos
|
||||
}) - 1
|
||||
if index < 0 {
|
||||
return 0
|
||||
}
|
||||
return moduleStarts[index]
|
||||
}
|
||||
|
||||
// buildModuleImportIndex 建立模块局部变量到导入模块编号的映射。
|
||||
func buildModuleImportIndex(text string, moduleStarts []int) map[int]map[string]int {
|
||||
if len(moduleStarts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
moduleMatches := moduleStartRe.FindAllStringSubmatchIndex(text, -1)
|
||||
moduleStartByID := make(map[string]int, len(moduleMatches))
|
||||
for _, match := range moduleMatches {
|
||||
moduleStartByID[text[match[2]:match[3]]] = match[0]
|
||||
}
|
||||
|
||||
importsByModule := make(map[int]map[string]int)
|
||||
for index, moduleStart := range moduleStarts {
|
||||
moduleEnd := len(text)
|
||||
if index+1 < len(moduleStarts) {
|
||||
moduleEnd = moduleStarts[index+1]
|
||||
}
|
||||
body := text[moduleStart:moduleEnd]
|
||||
for _, match := range moduleImportRe.FindAllStringSubmatch(body, -1) {
|
||||
targetModuleStart, ok := moduleStartByID[match[2]]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if importsByModule[moduleStart] == nil {
|
||||
importsByModule[moduleStart] = make(map[string]int)
|
||||
}
|
||||
importsByModule[moduleStart][match[1]] = targetModuleStart
|
||||
}
|
||||
}
|
||||
return importsByModule
|
||||
}
|
||||
|
||||
// ExtractProtosFromFiles 分别提取各 bundle,规范化类型引用后按全限定名合并。
|
||||
// 多个 bundle 出现同名声明时优先保留靠前输入。
|
||||
func ExtractProtosFromFiles(inputFiles []string, outputDir string) {
|
||||
activeDiagnostics = newExtractionDiagnostics()
|
||||
defer func() {
|
||||
activeDiagnostics = nil
|
||||
}()
|
||||
|
||||
var allMessages []Message
|
||||
var allEnums []Enum
|
||||
var allServices []Service
|
||||
for _, inputFile := range inputFiles {
|
||||
content, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", inputFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
text := string(content)
|
||||
moduleStarts := buildModuleStarts(text)
|
||||
aliases := buildAliasIndex(text, moduleStarts)
|
||||
exportAliases := buildWebpackExportAliasIndex(text, moduleStarts)
|
||||
|
||||
messages := extractMessages(text, moduleStarts)
|
||||
enums := extractEnums(text, moduleStarts)
|
||||
services := extractServices(text, moduleStarts)
|
||||
declared, extracted, missing := declarationCoverage(text, messages, enums, services)
|
||||
activeDiagnostics.declaredTypes += declared
|
||||
activeDiagnostics.extractedTypes += extracted
|
||||
activeDiagnostics.missingDeclarations = append(activeDiagnostics.missingDeclarations, missing...)
|
||||
|
||||
resolver := newTypeResolver(messages, enums, aliases, exportAliases)
|
||||
resolver.moduleImports = buildModuleImportIndex(text, moduleStarts)
|
||||
normalizeTypeReferences(messages, services, resolver)
|
||||
|
||||
allMessages = append(allMessages, messages...)
|
||||
allEnums = append(allEnums, enums...)
|
||||
allServices = append(allServices, services...)
|
||||
}
|
||||
|
||||
messages := mergeMessagesByTypeName(allMessages)
|
||||
enums := mergeEnumsByTypeName(allEnums)
|
||||
services := mergeServicesByTypeName(allServices)
|
||||
for _, msg := range messages {
|
||||
if len(msg.Fields) == 0 {
|
||||
activeDiagnostics.emptyMessages = append(activeDiagnostics.emptyMessages, msg.TypeName)
|
||||
}
|
||||
}
|
||||
sort.Strings(activeDiagnostics.missingDeclarations)
|
||||
activeDiagnostics.missingDeclarations = compactStrings(activeDiagnostics.missingDeclarations)
|
||||
|
||||
resolver := newTypeResolver(messages, enums, nil, nil)
|
||||
|
||||
generateProtos(messages, enums, services, resolver, outputDir)
|
||||
|
||||
validateErr := validateGeneratedProtos(outputDir, activeDiagnostics)
|
||||
|
||||
printDiagnosticsSummary(activeDiagnostics)
|
||||
|
||||
if strictExtractionValidation && hasValidationFailure(activeDiagnostics, validateErr) {
|
||||
if validateErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Validation failed: %v\n", validateErr)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if validateErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Validation warning: %v\n", validateErr)
|
||||
}
|
||||
|
||||
fmt.Printf("提取完成: %d 个消息, %d 个枚举, %d 个服务\n", len(messages), len(enums), len(services))
|
||||
}
|
||||
|
||||
// normalizeTypeReferences 把字段和方法引用统一转换为全限定类型名。
|
||||
func normalizeTypeReferences(messages []Message, services []Service, resolver *TypeResolver) {
|
||||
resolve := func(ref any, contextPos int, moduleStart int, pkg string, kind string) any {
|
||||
symbol, ok := ref.(string)
|
||||
if !ok || strings.TrimSpace(symbol) == "" {
|
||||
return ref
|
||||
}
|
||||
if typeName, resolved := resolver.ResolveTypeName(symbol, contextPos, moduleStart, pkg, kind); resolved {
|
||||
return typeName
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
for messageIndex := range messages {
|
||||
message := &messages[messageIndex]
|
||||
for fieldIndex := range message.Fields {
|
||||
field := &message.Fields[fieldIndex]
|
||||
if field.Kind == "message" || field.Kind == "enum" {
|
||||
field.T = resolve(field.T, message.Pos, message.ModuleStart, message.Package, field.Kind)
|
||||
}
|
||||
if field.Kind == "map" && (field.MapValueKind == "message" || field.MapValueKind == "enum") {
|
||||
field.MapValueT = resolve(field.MapValueT, message.Pos, message.ModuleStart, message.Package, field.MapValueKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for serviceIndex := range services {
|
||||
service := &services[serviceIndex]
|
||||
for methodIndex := range service.Methods {
|
||||
method := &service.Methods[methodIndex]
|
||||
if typeName, ok := resolve(method.InputType, service.Pos, service.ModuleStart, service.Package, "message").(string); ok {
|
||||
method.InputType = typeName
|
||||
}
|
||||
if typeName, ok := resolve(method.OutputType, service.Pos, service.ModuleStart, service.Package, "message").(string); ok {
|
||||
method.OutputType = typeName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mergeMessagesByTypeName 按全限定名合并消息并保留首次声明。
|
||||
func mergeMessagesByTypeName(messages []Message) []Message {
|
||||
seen := make(map[string]bool)
|
||||
merged := make([]Message, 0, len(messages))
|
||||
for _, message := range messages {
|
||||
if seen[message.TypeName] {
|
||||
continue
|
||||
}
|
||||
seen[message.TypeName] = true
|
||||
merged = append(merged, message)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// mergeEnumsByTypeName 按全限定名合并枚举并保留首次声明。
|
||||
func mergeEnumsByTypeName(enums []Enum) []Enum {
|
||||
seen := make(map[string]bool)
|
||||
merged := make([]Enum, 0, len(enums))
|
||||
for _, enum := range enums {
|
||||
if seen[enum.TypeName] {
|
||||
continue
|
||||
}
|
||||
seen[enum.TypeName] = true
|
||||
merged = append(merged, enum)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// mergeServicesByTypeName 按全限定名合并服务并保留首次声明。
|
||||
func mergeServicesByTypeName(services []Service) []Service {
|
||||
seen := make(map[string]bool)
|
||||
merged := make([]Service, 0, len(services))
|
||||
for _, service := range services {
|
||||
if seen[service.TypeName] {
|
||||
continue
|
||||
}
|
||||
seen[service.TypeName] = true
|
||||
merged = append(merged, service)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// compactStrings 清理、去重并排序诊断字符串。
|
||||
func compactStrings(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
compacted := values[:1]
|
||||
for _, value := range values[1:] {
|
||||
if value != compacted[len(compacted)-1] {
|
||||
compacted = append(compacted, value)
|
||||
}
|
||||
}
|
||||
return compacted
|
||||
}
|
||||
|
||||
// hasValidationFailure 判断诊断结果是否达到失败条件。
|
||||
func hasValidationFailure(diag *extractionDiagnostics, validateErr error) bool {
|
||||
if validateErr != nil {
|
||||
return true
|
||||
}
|
||||
if diag == nil {
|
||||
return false
|
||||
}
|
||||
if diag.skippedFieldObjects > 0 {
|
||||
return true
|
||||
}
|
||||
if len(diag.unresolvedTypeRefs) > 0 {
|
||||
return true
|
||||
}
|
||||
if len(diag.placeholderHits) > 0 {
|
||||
return true
|
||||
}
|
||||
if len(diag.missingDeclarations) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// printDiagnosticsSummary 输出提取覆盖率和异常样本摘要。
|
||||
func printDiagnosticsSummary(diag *extractionDiagnostics) {
|
||||
if diag == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"诊断汇总: fields %d/%d 解析成功, declarations %d/%d 已提取, skipped=%d, unresolved=%d, placeholders=%d, empty_messages=%d\n",
|
||||
diag.parsedFieldObjects,
|
||||
diag.totalFieldObjects,
|
||||
diag.extractedTypes,
|
||||
diag.declaredTypes,
|
||||
diag.skippedFieldObjects,
|
||||
len(diag.unresolvedTypeRefs),
|
||||
len(diag.placeholderHits),
|
||||
len(diag.emptyMessages),
|
||||
)
|
||||
|
||||
if diag.skippedFieldObjects > 0 && len(diag.skippedFieldSamples) > 0 {
|
||||
fmt.Println("字段解析失败样例:")
|
||||
for _, sample := range diag.skippedFieldSamples {
|
||||
fmt.Printf(" - %s\n", sample)
|
||||
}
|
||||
}
|
||||
|
||||
if len(diag.unresolvedTypeRefs) > 0 {
|
||||
keys := make([]string, 0, len(diag.unresolvedTypeRefs))
|
||||
for key := range diag.unresolvedTypeRefs {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
fmt.Println("未解析类型引用:")
|
||||
for _, key := range keys {
|
||||
fmt.Printf(" - %s (%d)\n", key, diag.unresolvedTypeRefs[key])
|
||||
}
|
||||
}
|
||||
|
||||
if len(diag.placeholderHits) > 0 {
|
||||
fmt.Println("占位字段命中:")
|
||||
for i, hit := range diag.placeholderHits {
|
||||
if i >= 20 {
|
||||
fmt.Printf(" - ... and %d more\n", len(diag.placeholderHits)-20)
|
||||
break
|
||||
}
|
||||
fmt.Printf(" - %s\n", hit)
|
||||
}
|
||||
}
|
||||
|
||||
if len(diag.missingDeclarations) > 0 {
|
||||
fmt.Println("未提取的 Proto 声明:")
|
||||
for i, typeName := range diag.missingDeclarations {
|
||||
if i >= 20 {
|
||||
fmt.Printf(" - ... and %d more\n", len(diag.missingDeclarations)-20)
|
||||
break
|
||||
}
|
||||
fmt.Printf(" - %s\n", typeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// declarationCoverage 比较 bundle 声明数量与实际提取数量。
|
||||
func declarationCoverage(text string, messages []Message, enums []Enum, services []Service) (int, int, []string) {
|
||||
declared := make(map[string]bool)
|
||||
collect := func(re *regexp.Regexp) {
|
||||
for _, match := range re.FindAllStringSubmatch(text, -1) {
|
||||
typeName := strings.TrimSpace(match[1])
|
||||
pkg, _ := parseTypeName(typeName)
|
||||
if typeName != "" && !isGooglePkg(pkg) {
|
||||
declared[typeName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
collect(typeNameDeclarationRe)
|
||||
collect(serviceDeclarationRe)
|
||||
collect(messageDeclarationRe)
|
||||
collect(enumDeclarationRe)
|
||||
collect(legacyEnumDeclarationRe)
|
||||
|
||||
extracted := make(map[string]bool)
|
||||
for _, message := range messages {
|
||||
extracted[message.TypeName] = true
|
||||
}
|
||||
for _, enum := range enums {
|
||||
extracted[enum.TypeName] = true
|
||||
}
|
||||
for _, service := range services {
|
||||
extracted[service.TypeName] = true
|
||||
}
|
||||
|
||||
matched := 0
|
||||
missing := make([]string, 0)
|
||||
for typeName := range declared {
|
||||
if extracted[typeName] {
|
||||
matched++
|
||||
continue
|
||||
}
|
||||
missing = append(missing, typeName)
|
||||
}
|
||||
sort.Strings(missing)
|
||||
return len(declared), matched, missing
|
||||
}
|
||||
|
||||
// validateGeneratedProtos 检查生成文件语法占位和关键 Agent 结构。
|
||||
func validateGeneratedProtos(outputDir string, diag *extractionDiagnostics) error {
|
||||
entries, err := os.ReadDir(outputDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read output dir failed: %w", err)
|
||||
}
|
||||
|
||||
protoFiles := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if strings.HasSuffix(name, ".proto") {
|
||||
protoFiles = append(protoFiles, name)
|
||||
}
|
||||
}
|
||||
if len(protoFiles) == 0 {
|
||||
return errors.New("no generated proto files found")
|
||||
}
|
||||
sort.Strings(protoFiles)
|
||||
|
||||
for _, file := range protoFiles {
|
||||
body, readErr := os.ReadFile(filepath.Join(outputDir, file))
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("read generated proto failed: %s: %w", file, readErr)
|
||||
}
|
||||
lines := strings.Split(string(body), "\n")
|
||||
for idx, line := range lines {
|
||||
if placeholderRe.MatchString(line) && diag != nil {
|
||||
hit := fmt.Sprintf("%s:%d: %s", file, idx+1, strings.TrimSpace(line))
|
||||
diag.placeholderHits = append(diag.placeholderHits, hit)
|
||||
}
|
||||
}
|
||||
if err := validateRequiredAgentShapes(file, string(body)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
parser := protoparse.Parser{
|
||||
ImportPaths: []string{outputDir},
|
||||
LookupImport: desc.LoadFileDescriptor,
|
||||
}
|
||||
if _, parseErr := parser.ParseFiles(protoFiles...); parseErr != nil {
|
||||
return fmt.Errorf("parse generated proto failed: %w", parseErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRequiredAgentShapes 校验 Agent 流控消息的必要字段形状。
|
||||
func validateRequiredAgentShapes(file string, body string) error {
|
||||
if strings.Contains(body, "message ExecClientControlMessage") && !streamCloseRe.MatchString(body) {
|
||||
return fmt.Errorf("%s: ExecClientControlMessage.stream_close must be ExecClientStreamClose", file)
|
||||
}
|
||||
if strings.Contains(body, "message ShellStream") && !shellStdoutRe.MatchString(body) {
|
||||
return fmt.Errorf("%s: ShellStream.stdout must be ShellStreamStdout", file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
// renderer.go 把协议声明树渲染为稳定的 proto 文本。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// generateProtoFile 把单个协议包的声明渲染并写入文件。
|
||||
func generateProtoFile(pkgName string, messages []Message, enums []Enum, services []Service, resolver *TypeResolver, outputDir string) {
|
||||
// 先收集全部跨包标准依赖。
|
||||
imports := collectImports(pkgName, messages, services, resolver)
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(`syntax = "proto3";` + "\n\n")
|
||||
sb.WriteString(fmt.Sprintf("package %s;\n\n", pkgName))
|
||||
|
||||
// 按稳定顺序写入 import。
|
||||
if len(imports) > 0 {
|
||||
sortedImports := make([]string, 0, len(imports))
|
||||
for imp := range imports {
|
||||
sortedImports = append(sortedImports, imp)
|
||||
}
|
||||
sort.Strings(sortedImports)
|
||||
for _, imp := range sortedImports {
|
||||
sb.WriteString(fmt.Sprintf("import \"%s\";\n", imp))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
goPackagePath := strings.ReplaceAll(pkgName, ".", "/")
|
||||
goPackageName := strings.ReplaceAll(pkgName, ".", "")
|
||||
sb.WriteString(fmt.Sprintf(`option go_package = "github.com/leookun/cursor-byok/cursor-proto/gen/%s;%s";`+"\n\n", goPackagePath, goPackageName))
|
||||
|
||||
// 建立嵌套类型树。
|
||||
root := &TypeNode{Children: make(map[string]*TypeNode)}
|
||||
|
||||
for i := range messages {
|
||||
msg := &messages[i]
|
||||
path := getNestedPath(msg.ShortName)
|
||||
insertMessage(root, path, msg)
|
||||
}
|
||||
|
||||
for i := range enums {
|
||||
enum := &enums[i]
|
||||
path := getNestedPath(enum.ShortName)
|
||||
insertEnum(root, path, enum)
|
||||
}
|
||||
|
||||
// 写入全部顶层类型。
|
||||
writeTypeTree(root, &sb, resolver, 0, pkgName)
|
||||
|
||||
// 写入服务声明。
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].ShortName < services[j].ShortName
|
||||
})
|
||||
|
||||
for _, svc := range services {
|
||||
// 写入服务来源注释。
|
||||
sb.WriteString(fmt.Sprintf("// Source: %s (var: %s)\n", svc.TypeName, svc.VarName))
|
||||
sb.WriteString(fmt.Sprintf("service %s {\n", svc.ShortName))
|
||||
for _, m := range svc.Methods {
|
||||
inputType := resolveMethodType(m.InputType, resolver, pkgName, svc.Pos, svc.ModuleStart)
|
||||
outputType := resolveMethodType(m.OutputType, resolver, pkgName, svc.Pos, svc.ModuleStart)
|
||||
|
||||
switch m.Kind {
|
||||
case "ServerStreaming":
|
||||
sb.WriteString(fmt.Sprintf(" rpc %s(%s) returns (stream %s) {}\n", m.Name, inputType, outputType))
|
||||
case "ClientStreaming":
|
||||
sb.WriteString(fmt.Sprintf(" rpc %s(stream %s) returns (%s) {}\n", m.Name, inputType, outputType))
|
||||
case "BiDiStreaming":
|
||||
sb.WriteString(fmt.Sprintf(" rpc %s(stream %s) returns (stream %s) {}\n", m.Name, inputType, outputType))
|
||||
default: // 默认为一元调用。
|
||||
sb.WriteString(fmt.Sprintf(" rpc %s(%s) returns (%s) {}\n", m.Name, inputType, outputType))
|
||||
}
|
||||
}
|
||||
sb.WriteString("}\n\n")
|
||||
}
|
||||
|
||||
// 每个协议包写入扁平输出目录中的单个文件。
|
||||
fileName := strings.ReplaceAll(pkgName, ".", "_") + ".proto"
|
||||
filePath := filepath.Join(outputDir, fileName)
|
||||
|
||||
os.WriteFile(filePath, []byte(sb.String()), 0644)
|
||||
fmt.Printf("Generated: %s (%d messages, %d enums, %d services)\n", filePath, len(messages), len(enums), len(services))
|
||||
}
|
||||
|
||||
// resolveMethodType 解析方法消息类型并处理本地复制类型。
|
||||
func resolveMethodType(ref string, resolver *TypeResolver, currentPkg string, contextPos int, contextModuleStart int) string {
|
||||
typeName, ok := resolver.ResolveTypeName(ref, contextPos, contextModuleStart, currentPkg, "message")
|
||||
if !ok {
|
||||
activeDiagnostics.addUnresolvedType("method:" + ref)
|
||||
return fallbackTypeToken(ref)
|
||||
}
|
||||
|
||||
refPkg, shortName := parseTypeName(typeName)
|
||||
if refPkg == currentPkg || refPkg == "" {
|
||||
return shortName
|
||||
}
|
||||
// 检查类型是否由其他包复制到当前包。
|
||||
if copied := copiedTypes[currentPkg]; copied != nil {
|
||||
if _, isCopied := copied[shortName]; isCopied {
|
||||
return shortName
|
||||
}
|
||||
}
|
||||
return refPkg + "." + shortName
|
||||
}
|
||||
|
||||
// insertMessage 把消息插入嵌套类型树。
|
||||
func insertMessage(node *TypeNode, path []string, msg *Message) {
|
||||
if len(path) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
name := path[0]
|
||||
if node.Children == nil {
|
||||
node.Children = make(map[string]*TypeNode)
|
||||
}
|
||||
|
||||
child, exists := node.Children[name]
|
||||
if !exists {
|
||||
child = &TypeNode{Name: name, Children: make(map[string]*TypeNode)}
|
||||
node.Children[name] = child
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
child.Message = msg
|
||||
} else {
|
||||
insertMessage(child, path[1:], msg)
|
||||
}
|
||||
}
|
||||
|
||||
// insertEnum 把枚举插入嵌套类型树。
|
||||
func insertEnum(node *TypeNode, path []string, enum *Enum) {
|
||||
if len(path) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
name := path[0]
|
||||
if node.Children == nil {
|
||||
node.Children = make(map[string]*TypeNode)
|
||||
}
|
||||
|
||||
child, exists := node.Children[name]
|
||||
if !exists {
|
||||
child = &TypeNode{Name: name, Children: make(map[string]*TypeNode)}
|
||||
node.Children[name] = child
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
child.Enum = enum
|
||||
} else {
|
||||
insertEnum(child, path[1:], enum)
|
||||
}
|
||||
}
|
||||
|
||||
// writeTypeTree 按名称稳定输出嵌套消息和枚举。
|
||||
func writeTypeTree(node *TypeNode, sb *strings.Builder, resolver *TypeResolver, indent int, currentPkg string) {
|
||||
// 对子节点排序以保证输出稳定。
|
||||
var names []string
|
||||
for name := range node.Children {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
indentStr := strings.Repeat(" ", indent)
|
||||
|
||||
for _, name := range names {
|
||||
child := node.Children[name]
|
||||
|
||||
if child.Enum != nil {
|
||||
// 检查枚举是否来自其他包。
|
||||
originalType := ""
|
||||
if copied := copiedTypes[currentPkg]; copied != nil {
|
||||
if orig, ok := copied[child.Enum.ShortName]; ok {
|
||||
originalType = orig
|
||||
}
|
||||
}
|
||||
|
||||
// 写入枚举来源注释。
|
||||
if originalType != "" {
|
||||
sb.WriteString(fmt.Sprintf("%s// Copied from: %s (var: %s)\n", indentStr, originalType, child.Enum.VarName))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s// Source: %s (var: %s)\n", indentStr, child.Enum.TypeName, child.Enum.VarName))
|
||||
}
|
||||
// 写入枚举声明。
|
||||
sb.WriteString(fmt.Sprintf("%senum %s {\n", indentStr, name))
|
||||
for _, v := range child.Enum.Values {
|
||||
sb.WriteString(fmt.Sprintf("%s %s = %d;\n", indentStr, v.Name, v.No))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n\n", indentStr))
|
||||
} else if child.Message != nil || len(child.Children) > 0 {
|
||||
// 写入消息来源注释。
|
||||
if child.Message != nil {
|
||||
varInfo := child.Message.VarName
|
||||
if child.Message.InternalName != "" && child.Message.InternalName != child.Message.VarName {
|
||||
varInfo = fmt.Sprintf("%s, class: %s", child.Message.VarName, child.Message.InternalName)
|
||||
}
|
||||
|
||||
// 检查消息是否来自其他包。
|
||||
originalType := ""
|
||||
if copied := copiedTypes[currentPkg]; copied != nil {
|
||||
if orig, ok := copied[child.Message.ShortName]; ok {
|
||||
originalType = orig
|
||||
}
|
||||
}
|
||||
|
||||
if originalType != "" {
|
||||
sb.WriteString(fmt.Sprintf("%s// Copied from: %s (var: %s)\n", indentStr, originalType, varInfo))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%s// Source: %s (var: %s)\n", indentStr, child.Message.TypeName, varInfo))
|
||||
}
|
||||
}
|
||||
// 即使节点只承载嵌套类型,也要写入消息容器。
|
||||
sb.WriteString(fmt.Sprintf("%smessage %s {\n", indentStr, name))
|
||||
|
||||
// 先写入嵌套类型。
|
||||
writeTypeTree(child, sb, resolver, indent+1, currentPkg)
|
||||
|
||||
// 当前节点有消息声明时再写字段。
|
||||
if child.Message != nil {
|
||||
writeMessageFields(child.Message, sb, resolver, indent+1)
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s}\n\n", indentStr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeMessageFields 输出普通字段和 oneof 分组。
|
||||
func writeMessageFields(msg *Message, sb *strings.Builder, resolver *TypeResolver, indent int) {
|
||||
indentStr := strings.Repeat(" ", indent)
|
||||
|
||||
// 获取当前消息路径,用于解析相对嵌套类型。
|
||||
msgPath := msg.ShortName
|
||||
currentPkg := msg.Package
|
||||
preferredPkg, _ := parseTypeName(msg.TypeName)
|
||||
|
||||
// 按 oneof 分组字段。
|
||||
oneofGroups := make(map[string][]Field)
|
||||
var regularFields []Field
|
||||
|
||||
for _, f := range msg.Fields {
|
||||
if f.Oneof != "" {
|
||||
oneofGroups[f.Oneof] = append(oneofGroups[f.Oneof], f)
|
||||
} else {
|
||||
regularFields = append(regularFields, f)
|
||||
}
|
||||
}
|
||||
|
||||
// 先写普通字段。
|
||||
for _, f := range regularFields {
|
||||
fieldType := resolveFieldTypeWithPkg(f, resolver, msgPath, currentPkg, preferredPkg, msg.Pos, msg.ModuleStart)
|
||||
prefix := ""
|
||||
if f.Repeated {
|
||||
prefix = "repeated "
|
||||
} else if f.Opt {
|
||||
prefix = "optional "
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s%s%s %s = %d;\n", indentStr, prefix, fieldType, f.Name, f.No))
|
||||
}
|
||||
|
||||
// 再写 oneof 字段组。
|
||||
var oneofNames []string
|
||||
for name := range oneofGroups {
|
||||
oneofNames = append(oneofNames, name)
|
||||
}
|
||||
sort.Strings(oneofNames)
|
||||
|
||||
for _, oneofName := range oneofNames {
|
||||
fields := oneofGroups[oneofName]
|
||||
sb.WriteString(fmt.Sprintf("%soneof %s {\n", indentStr, oneofName))
|
||||
for _, f := range fields {
|
||||
fieldType := resolveFieldTypeWithPkg(f, resolver, msgPath, currentPkg, preferredPkg, msg.Pos, msg.ModuleStart)
|
||||
sb.WriteString(fmt.Sprintf("%s %s %s = %d;\n", indentStr, fieldType, f.Name, f.No))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s}\n", indentStr))
|
||||
}
|
||||
}
|
||||
|
||||
// parseTypeName 从全限定类型名拆出协议包和完整嵌套路径。
|
||||
func parseTypeName(typeName string) (pkg, shortName string) {
|
||||
// 优先匹配 xxx.vN.Rest 形式的版本化协议包。
|
||||
versionRe := regexp.MustCompile(`^([\w.]+\.v\d+)\.(.+)$`)
|
||||
if match := versionRe.FindStringSubmatch(typeName); match != nil {
|
||||
return match[1], match[2]
|
||||
}
|
||||
|
||||
// 单独处理 google.protobuf 标准类型。
|
||||
if strings.HasPrefix(typeName, "google.protobuf.") {
|
||||
rest := strings.TrimPrefix(typeName, "google.protobuf.")
|
||||
return "google.protobuf", rest
|
||||
}
|
||||
|
||||
// 单独处理 google.rpc 标准类型。
|
||||
if strings.HasPrefix(typeName, "google.rpc.") {
|
||||
rest := strings.TrimPrefix(typeName, "google.rpc.")
|
||||
return "google.rpc", rest
|
||||
}
|
||||
|
||||
// 无法识别包版本时按最后一个点回退拆分。
|
||||
parts := strings.Split(typeName, ".")
|
||||
if len(parts) > 1 {
|
||||
return strings.Join(parts[:len(parts)-1], "."), parts[len(parts)-1]
|
||||
}
|
||||
return "", typeName
|
||||
}
|
||||
|
||||
// getNestedPath 把嵌套类型名拆成逐级路径。
|
||||
func getNestedPath(shortName string) []string {
|
||||
return strings.Split(shortName, ".")
|
||||
}
|
||||
|
||||
// resolveFieldTypeWithPkg 结合当前包和父消息路径解析字段类型。
|
||||
func resolveFieldTypeWithPkg(f Field, resolver *TypeResolver, parentPath string, currentPkg string, preferredPkg string, contextPos int, contextModuleStart int) string {
|
||||
resolveNamedType := func(ref string, expectedKind string) string {
|
||||
typeName, ok := resolver.ResolveTypeName(ref, contextPos, contextModuleStart, preferredPkg, expectedKind)
|
||||
if !ok {
|
||||
activeDiagnostics.addUnresolvedType(expectedKind + ":" + ref)
|
||||
return fallbackTypeToken(ref)
|
||||
}
|
||||
|
||||
refPkg, shortName := parseTypeName(typeName)
|
||||
|
||||
// 类型位于同一父消息下时使用相对路径。
|
||||
if parentPath != "" && strings.HasPrefix(shortName, parentPath+".") {
|
||||
// 例如消息内部将 ConversationMessage.CodeChunk 缩短为 CodeChunk。
|
||||
return strings.TrimPrefix(shortName, parentPath+".")
|
||||
}
|
||||
|
||||
// 同包类型只使用短名称。
|
||||
if refPkg == currentPkg || refPkg == "" {
|
||||
return shortName
|
||||
}
|
||||
|
||||
// 循环依赖中优先使用已经复制到当前包的类型。
|
||||
if copied := copiedTypes[currentPkg]; copied != nil {
|
||||
if _, isCopied := copied[shortName]; isCopied {
|
||||
// 本地存在复制类型时使用短名称。
|
||||
return shortName
|
||||
}
|
||||
}
|
||||
|
||||
// 其余跨包引用保留全限定类型名。
|
||||
return refPkg + "." + shortName
|
||||
}
|
||||
|
||||
if f.Kind == "scalar" {
|
||||
if t, ok := f.T.(int); ok {
|
||||
return scalarTypes[t]
|
||||
}
|
||||
if t, ok := f.T.(float64); ok {
|
||||
return scalarTypes[int(t)]
|
||||
}
|
||||
}
|
||||
|
||||
if f.Kind == "message" || f.Kind == "enum" {
|
||||
if ref, ok := f.T.(string); ok {
|
||||
return resolveNamedType(ref, f.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
if f.Kind == "map" {
|
||||
// map 字段分别解析键和值类型。
|
||||
keyType := scalarTypes[f.MapKey]
|
||||
if keyType == "" {
|
||||
keyType = "string" // 未知标量默认使用字符串。
|
||||
}
|
||||
|
||||
var valueType string
|
||||
if f.MapValueKind == "scalar" {
|
||||
if t, ok := f.MapValueT.(int); ok {
|
||||
valueType = scalarTypes[t]
|
||||
} else if t, ok := f.MapValueT.(float64); ok {
|
||||
valueType = scalarTypes[int(t)]
|
||||
}
|
||||
} else if f.MapValueKind == "message" || f.MapValueKind == "enum" {
|
||||
if ref, ok := f.MapValueT.(string); ok {
|
||||
valueType = resolveNamedType(ref, f.MapValueKind)
|
||||
}
|
||||
}
|
||||
if valueType == "" {
|
||||
valueType = "bytes"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("map<%s, %s>", keyType, valueType)
|
||||
}
|
||||
|
||||
return "bytes" // 未识别字段类型时回退为字节串。
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
// resolver.go 解析压缩 bundle 中的局部符号、模块别名和导出别名。
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// newTypeResolver 建立消息、枚举和模块别名的统一索引。
|
||||
func newTypeResolver(messages []Message, enums []Enum, aliases aliasIndex, exportAliases aliasIndex) *TypeResolver {
|
||||
resolver := &TypeResolver{
|
||||
bySymbol: make(map[string][]symbolDef),
|
||||
byAlias: make(map[string][]symbolDef),
|
||||
byShort: make(map[string][]symbolDef),
|
||||
}
|
||||
|
||||
add := func(symbol, typeName string, pos int, moduleStart int, kind string) {
|
||||
symbol = strings.TrimSpace(symbol)
|
||||
typeName = strings.TrimSpace(typeName)
|
||||
if symbol == "" || typeName == "" {
|
||||
return
|
||||
}
|
||||
def := symbolDef{TypeName: typeName, Pos: pos, ModuleStart: moduleStart, Kind: kind}
|
||||
resolver.bySymbol[symbol] = append(resolver.bySymbol[symbol], def)
|
||||
_, shortName := parseTypeName(typeName)
|
||||
if shortName != "" {
|
||||
resolver.byShort[shortName] = append(resolver.byShort[shortName], def)
|
||||
underscoreAlias := strings.ReplaceAll(shortName, ".", "_")
|
||||
if underscoreAlias != shortName {
|
||||
resolver.byShort[underscoreAlias] = append(resolver.byShort[underscoreAlias], def)
|
||||
}
|
||||
if idx := strings.LastIndex(shortName, "."); idx > 0 && idx+1 < len(shortName) {
|
||||
resolver.byShort[shortName[idx+1:]] = append(resolver.byShort[shortName[idx+1:]], def)
|
||||
}
|
||||
if idx := strings.LastIndex(underscoreAlias, "_"); idx > 0 && idx+1 < len(underscoreAlias) {
|
||||
resolver.byShort[underscoreAlias[idx+1:]] = append(resolver.byShort[underscoreAlias[idx+1:]], def)
|
||||
}
|
||||
}
|
||||
}
|
||||
addAlias := func(symbol, typeName string, pos int, moduleStart int, kind string) {
|
||||
symbol = strings.TrimSpace(symbol)
|
||||
typeName = strings.TrimSpace(typeName)
|
||||
if symbol == "" || typeName == "" {
|
||||
return
|
||||
}
|
||||
resolver.byAlias[symbol] = append(resolver.byAlias[symbol], symbolDef{
|
||||
TypeName: typeName, Pos: pos, ModuleStart: moduleStart, Kind: kind,
|
||||
})
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
add(msg.VarName, msg.TypeName, msg.Pos, msg.ModuleStart, "message")
|
||||
if msg.InternalName != "" && msg.InternalName != msg.VarName {
|
||||
add(msg.InternalName, msg.TypeName, msg.Pos, msg.ModuleStart, "message")
|
||||
}
|
||||
for _, alias := range aliasesForSymbols(aliases[msg.ModuleStart], msg.VarName, msg.InternalName) {
|
||||
addAlias(alias, msg.TypeName, msg.Pos, msg.ModuleStart, "message")
|
||||
}
|
||||
}
|
||||
for _, enum := range enums {
|
||||
add(enum.VarName, enum.TypeName, enum.Pos, enum.ModuleStart, "enum")
|
||||
for _, alias := range aliasesForSymbols(aliases[enum.ModuleStart], enum.VarName) {
|
||||
addAlias(alias, enum.TypeName, enum.Pos, enum.ModuleStart, "enum")
|
||||
}
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
for _, alias := range aliasesForSymbols(exportAliases[msg.ModuleStart], msg.VarName, msg.InternalName) {
|
||||
addAlias(alias, msg.TypeName, msg.Pos, msg.ModuleStart, "message")
|
||||
}
|
||||
}
|
||||
for _, enum := range enums {
|
||||
for _, alias := range aliasesForSymbols(exportAliases[enum.ModuleStart], enum.VarName) {
|
||||
addAlias(alias, enum.TypeName, enum.Pos, enum.ModuleStart, "enum")
|
||||
}
|
||||
}
|
||||
|
||||
return resolver
|
||||
}
|
||||
|
||||
// buildAliasIndex 提取变量声明和赋值形成的局部别名。
|
||||
func buildAliasIndex(text string, moduleStarts []int) aliasIndex {
|
||||
directByModule := make(map[int]map[string]string)
|
||||
addMatches := func(matches [][]int) {
|
||||
for _, match := range matches {
|
||||
alias := strings.TrimSpace(text[match[2]:match[3]])
|
||||
target := strings.TrimSpace(text[match[4]:match[5]])
|
||||
if alias == "" || target == "" || alias == target {
|
||||
continue
|
||||
}
|
||||
moduleStart := moduleStartForPos(moduleStarts, match[0])
|
||||
if directByModule[moduleStart] == nil {
|
||||
directByModule[moduleStart] = make(map[string]string)
|
||||
}
|
||||
directByModule[moduleStart][alias] = target
|
||||
}
|
||||
}
|
||||
|
||||
addMatches(varAliasRe.FindAllStringSubmatchIndex(text, -1))
|
||||
addMatches(assignmentAliasRe.FindAllStringSubmatchIndex(text, -1))
|
||||
|
||||
resolveRoot := func(direct map[string]string, symbol string) string {
|
||||
seen := make(map[string]bool)
|
||||
current := symbol
|
||||
for {
|
||||
if seen[current] {
|
||||
return symbol
|
||||
}
|
||||
seen[current] = true
|
||||
next := direct[current]
|
||||
if next == "" {
|
||||
return current
|
||||
}
|
||||
current = next
|
||||
}
|
||||
}
|
||||
|
||||
aliasSets := make(map[int]map[string]map[string]bool)
|
||||
addAlias := func(moduleStart int, root string, alias string) {
|
||||
root = strings.TrimSpace(root)
|
||||
alias = strings.TrimSpace(alias)
|
||||
if root == "" || alias == "" || root == alias {
|
||||
return
|
||||
}
|
||||
if aliasSets[moduleStart] == nil {
|
||||
aliasSets[moduleStart] = make(map[string]map[string]bool)
|
||||
}
|
||||
if aliasSets[moduleStart][root] == nil {
|
||||
aliasSets[moduleStart][root] = make(map[string]bool)
|
||||
}
|
||||
aliasSets[moduleStart][root][alias] = true
|
||||
}
|
||||
|
||||
for moduleStart, direct := range directByModule {
|
||||
for alias := range direct {
|
||||
root := resolveRoot(direct, alias)
|
||||
addAlias(moduleStart, root, alias)
|
||||
}
|
||||
}
|
||||
|
||||
if len(aliasSets) == 0 {
|
||||
return nil
|
||||
}
|
||||
aliases := make(aliasIndex, len(aliasSets))
|
||||
for moduleStart, roots := range aliasSets {
|
||||
aliases[moduleStart] = make(map[string][]string, len(roots))
|
||||
for root, set := range roots {
|
||||
for alias := range set {
|
||||
aliases[moduleStart][root] = append(aliases[moduleStart][root], alias)
|
||||
}
|
||||
sort.Strings(aliases[moduleStart][root])
|
||||
}
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
// buildWebpackExportAliasIndex 提取 Webpack 导出表中的符号别名。
|
||||
func buildWebpackExportAliasIndex(text string, moduleStarts []int) aliasIndex {
|
||||
aliasSets := make(map[int]map[string]map[string]bool)
|
||||
addAlias := func(moduleStart int, root string, alias string) {
|
||||
root = strings.TrimSpace(root)
|
||||
alias = strings.TrimSpace(alias)
|
||||
if root == "" || alias == "" || root == alias {
|
||||
return
|
||||
}
|
||||
if aliasSets[moduleStart] == nil {
|
||||
aliasSets[moduleStart] = make(map[string]map[string]bool)
|
||||
}
|
||||
if aliasSets[moduleStart][root] == nil {
|
||||
aliasSets[moduleStart][root] = make(map[string]bool)
|
||||
}
|
||||
aliasSets[moduleStart][root][alias] = true
|
||||
}
|
||||
|
||||
// Webpack 通过 n.d(t, { KS: () => T }) 暴露成员;服务使用 r.KS,消息定义使用局部符号 T。
|
||||
for _, blockMatch := range webpackExportBlockRe.FindAllStringIndex(text, -1) {
|
||||
moduleStart := moduleStartForPos(moduleStarts, blockMatch[0])
|
||||
blockStart := blockMatch[1] - 1
|
||||
blockEnd := findMatchingBrace(text, blockStart)
|
||||
if blockEnd == -1 {
|
||||
continue
|
||||
}
|
||||
block := text[blockStart:blockEnd]
|
||||
for _, entry := range webpackExportEntryRe.FindAllStringSubmatch(block, -1) {
|
||||
addAlias(moduleStart, entry[2], entry[1])
|
||||
}
|
||||
}
|
||||
|
||||
if len(aliasSets) == 0 {
|
||||
return nil
|
||||
}
|
||||
aliases := make(aliasIndex, len(aliasSets))
|
||||
for moduleStart, roots := range aliasSets {
|
||||
aliases[moduleStart] = make(map[string][]string, len(roots))
|
||||
for root, set := range roots {
|
||||
for alias := range set {
|
||||
aliases[moduleStart][root] = append(aliases[moduleStart][root], alias)
|
||||
}
|
||||
sort.Strings(aliases[moduleStart][root])
|
||||
}
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
// aliasesForSymbols 返回目标符号集合对应的去重别名。
|
||||
func aliasesForSymbols(aliases map[string][]string, symbols ...string) []string {
|
||||
if len(aliases) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
var result []string
|
||||
for _, symbol := range symbols {
|
||||
for _, alias := range aliases[strings.TrimSpace(symbol)] {
|
||||
if alias == "" || seen[alias] {
|
||||
continue
|
||||
}
|
||||
seen[alias] = true
|
||||
result = append(result, alias)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// looksLikeFullTypeName 判断引用是否已经是全限定协议类型名。
|
||||
func looksLikeFullTypeName(ref string) bool {
|
||||
trimmed := strings.TrimSpace(ref)
|
||||
if strings.HasPrefix(trimmed, "google.protobuf.") || strings.HasPrefix(trimmed, "google.rpc.") {
|
||||
return true
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^[\w.]+\.v\d+\.[\w.]+$`, trimmed)
|
||||
return matched
|
||||
}
|
||||
|
||||
// pickBestDefinition 按模块、类别、首选包和源码距离选择定义。
|
||||
func pickBestDefinition(candidates []symbolDef, contextPos int, contextModuleStart int, preferredPkg string, expectedKind string) (symbolDef, bool) {
|
||||
if len(candidates) == 0 {
|
||||
return symbolDef{}, false
|
||||
}
|
||||
|
||||
filtered := candidates
|
||||
if strings.TrimSpace(expectedKind) != "" {
|
||||
tmp := make([]symbolDef, 0, len(candidates))
|
||||
for _, item := range candidates {
|
||||
if item.Kind == expectedKind {
|
||||
tmp = append(tmp, item)
|
||||
}
|
||||
}
|
||||
if len(tmp) > 0 {
|
||||
filtered = tmp
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(preferredPkg) != "" {
|
||||
tmp := make([]symbolDef, 0, len(filtered))
|
||||
for _, item := range filtered {
|
||||
pkg, _ := parseTypeName(item.TypeName)
|
||||
if pkg == preferredPkg {
|
||||
tmp = append(tmp, item)
|
||||
}
|
||||
}
|
||||
if len(tmp) > 0 {
|
||||
filtered = tmp
|
||||
}
|
||||
}
|
||||
|
||||
if contextModuleStart > 0 {
|
||||
tmp := make([]symbolDef, 0, len(filtered))
|
||||
for _, item := range filtered {
|
||||
if item.ModuleStart == contextModuleStart {
|
||||
tmp = append(tmp, item)
|
||||
}
|
||||
}
|
||||
if len(tmp) > 0 {
|
||||
filtered = tmp
|
||||
}
|
||||
}
|
||||
|
||||
// 选择绝对距离最近的定义,距离相同时优先前向定义。
|
||||
bestIndex := -1
|
||||
bestDistance := 0
|
||||
bestIsFuture := false
|
||||
for index, item := range filtered {
|
||||
distance := absInt(item.Pos - contextPos)
|
||||
isFuture := item.Pos > contextPos
|
||||
if bestIndex == -1 {
|
||||
bestIndex = index
|
||||
bestDistance = distance
|
||||
bestIsFuture = isFuture
|
||||
continue
|
||||
}
|
||||
if distance < bestDistance {
|
||||
bestIndex = index
|
||||
bestDistance = distance
|
||||
bestIsFuture = isFuture
|
||||
continue
|
||||
}
|
||||
if distance == bestDistance {
|
||||
// 距离相同时优先当前位置之前的定义。
|
||||
if bestIsFuture && !isFuture {
|
||||
bestIndex = index
|
||||
bestIsFuture = isFuture
|
||||
}
|
||||
}
|
||||
}
|
||||
if bestIndex < 0 {
|
||||
return symbolDef{}, false
|
||||
}
|
||||
return filtered[bestIndex], true
|
||||
}
|
||||
|
||||
// ResolveTypeName 把局部变量、别名或短名称解析为全限定类型名。
|
||||
func (resolver *TypeResolver) ResolveTypeName(ref string, contextPos int, contextModuleStart int, preferredPkg string, expectedKind string) (string, bool) {
|
||||
if resolver == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
trimmed := strings.TrimSpace(ref)
|
||||
if trimmed == "" {
|
||||
return "", false
|
||||
}
|
||||
if looksLikeFullTypeName(trimmed) {
|
||||
return trimmed, true
|
||||
}
|
||||
|
||||
resolveBySymbol := func(symbol string, preferSameModule bool) (string, bool) {
|
||||
candidates := resolver.bySymbol[symbol]
|
||||
if len(candidates) == 0 {
|
||||
return "", false
|
||||
}
|
||||
moduleStart := 0
|
||||
if preferSameModule {
|
||||
moduleStart = contextModuleStart
|
||||
}
|
||||
best, ok := pickBestDefinition(candidates, contextPos, moduleStart, preferredPkg, expectedKind)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return best.TypeName, true
|
||||
}
|
||||
resolveByAlias := func(symbol string, targetModuleStart int) (string, bool) {
|
||||
candidates := resolver.byAlias[symbol]
|
||||
if len(candidates) == 0 {
|
||||
return "", false
|
||||
}
|
||||
best, ok := pickBestDefinition(candidates, contextPos, targetModuleStart, preferredPkg, expectedKind)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return best.TypeName, true
|
||||
}
|
||||
resolveByShort := func(symbol string, preferSameModule bool) (string, bool) {
|
||||
candidates := resolver.byShort[symbol]
|
||||
if len(candidates) == 0 {
|
||||
return "", false
|
||||
}
|
||||
moduleStart := 0
|
||||
if preferSameModule {
|
||||
moduleStart = contextModuleStart
|
||||
}
|
||||
best, ok := pickBestDefinition(candidates, contextPos, moduleStart, preferredPkg, expectedKind)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return best.TypeName, true
|
||||
}
|
||||
|
||||
if typeName, ok := resolveBySymbol(trimmed, !strings.Contains(trimmed, ".")); ok {
|
||||
return typeName, true
|
||||
}
|
||||
if typeName, ok := resolveByAlias(trimmed, 0); ok {
|
||||
return typeName, true
|
||||
}
|
||||
if typeName, ok := resolveByShort(trimmed, !strings.Contains(trimmed, ".")); ok {
|
||||
return typeName, true
|
||||
}
|
||||
|
||||
if strings.Contains(trimmed, ".") {
|
||||
parts := strings.Split(trimmed, ".")
|
||||
first := parts[0]
|
||||
last := parts[len(parts)-1]
|
||||
targetModuleStart := 0
|
||||
if imports := resolver.moduleImports[contextModuleStart]; imports != nil {
|
||||
targetModuleStart = imports[first]
|
||||
}
|
||||
if typeName, ok := resolveByAlias(last, targetModuleStart); ok {
|
||||
return typeName, true
|
||||
}
|
||||
if typeName, ok := resolveBySymbol(last, false); ok {
|
||||
return typeName, true
|
||||
}
|
||||
if typeName, ok := resolveByShort(last, false); ok {
|
||||
return typeName, true
|
||||
}
|
||||
if typeName, ok := resolveBySymbol(first, false); ok {
|
||||
return typeName, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// fallbackTypeToken 从无法解析的引用生成合法类型占位名。
|
||||
func fallbackTypeToken(ref string) string {
|
||||
token := strings.TrimSpace(ref)
|
||||
if token == "" {
|
||||
return token
|
||||
}
|
||||
if strings.Contains(token, ".") {
|
||||
parts := strings.Split(token, ".")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// absInt 返回整数绝对值。
|
||||
func absInt(value int) int {
|
||||
if value < 0 {
|
||||
return -value
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// services.go 解析枚举、服务方法和压缩对象的配对括号。
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// extractEnums 从旧式和工厂式声明中提取枚举。
|
||||
func extractEnums(text string, moduleStarts []int) []Enum {
|
||||
var enums []Enum
|
||||
enumExists := func(typeName, varName string) bool {
|
||||
for _, existing := range enums {
|
||||
if existing.TypeName == typeName && existing.VarName == varName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 匹配任意包中的 setEnumType(XXX, "xxx.v1.EnumName", [...]) 枚举声明。
|
||||
// JS 变量名可以包含 $ 符号
|
||||
enumRe := regexp.MustCompile(`setEnumType\s*\(\s*([\w$]+)\s*,\s*"([\w.]+)"\s*,\s*\[`)
|
||||
|
||||
matches := enumRe.FindAllStringSubmatchIndex(text, -1)
|
||||
for _, match := range matches {
|
||||
varName := text[match[2]:match[3]]
|
||||
typeName := text[match[4]:match[5]]
|
||||
|
||||
// 提取枚举值数组。
|
||||
bracketStart := match[1] - 1
|
||||
values := extractEnumValues(text, bracketStart)
|
||||
|
||||
pkg, shortName := parseTypeName(typeName)
|
||||
enum := Enum{
|
||||
TypeName: typeName,
|
||||
VarName: varName,
|
||||
Values: values,
|
||||
Package: pkg,
|
||||
ShortName: shortName,
|
||||
Pos: match[0],
|
||||
ModuleStart: moduleStartForPos(moduleStarts, match[0]),
|
||||
}
|
||||
enums = append(enums, enum)
|
||||
}
|
||||
|
||||
// 匹配现代 @bufbuild/protobuf 工厂形式,例如 Role=A.makeEnum("aiserver.v1.InferenceMessageRole",[{...}])。
|
||||
enumFactoryRe := regexp.MustCompile(`([\w$]+)\s*=\s*[\w$.]+\.makeEnum\s*\(\s*["']([\w.]+)["']\s*,\s*\[`)
|
||||
factoryMatches := enumFactoryRe.FindAllStringSubmatchIndex(text, -1)
|
||||
for _, match := range factoryMatches {
|
||||
varName := text[match[2]:match[3]]
|
||||
typeName := text[match[4]:match[5]]
|
||||
if enumExists(typeName, varName) {
|
||||
continue
|
||||
}
|
||||
|
||||
bracketStart := match[1] - 1
|
||||
if bracketStart < 0 || bracketStart >= len(text) || text[bracketStart] != '[' {
|
||||
continue
|
||||
}
|
||||
|
||||
pkg, shortName := parseTypeName(typeName)
|
||||
enums = append(enums, Enum{
|
||||
TypeName: typeName,
|
||||
VarName: varName,
|
||||
Values: extractEnumValues(text, bracketStart),
|
||||
Package: pkg,
|
||||
ShortName: shortName,
|
||||
Pos: match[0],
|
||||
ModuleStart: moduleStartForPos(moduleStarts, match[0]),
|
||||
})
|
||||
}
|
||||
|
||||
return enums
|
||||
}
|
||||
|
||||
// extractServices 从命名或匿名描述符中提取服务。
|
||||
func extractServices(text string, moduleStarts []int) []Service {
|
||||
var services []Service
|
||||
seenTypeNames := make(map[string]bool)
|
||||
appendService := func(varName, typeName string, pos, methodsStart int) {
|
||||
if seenTypeNames[typeName] {
|
||||
return
|
||||
}
|
||||
methodsEnd := findMatchingBrace(text, methodsStart)
|
||||
if methodsEnd == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
pkg, shortName := parseTypeName(typeName)
|
||||
services = append(services, Service{
|
||||
TypeName: typeName,
|
||||
VarName: varName,
|
||||
Methods: extractMethods(text[methodsStart:methodsEnd]),
|
||||
Package: pkg,
|
||||
ShortName: shortName,
|
||||
Pos: pos,
|
||||
ModuleStart: moduleStartForPos(moduleStarts, pos),
|
||||
})
|
||||
seenTypeNames[typeName] = true
|
||||
}
|
||||
|
||||
// 匹配 VarName = { typeName: "xxx.v1.ServiceName", methods: { ... } } 服务对象。
|
||||
serviceRe := regexp.MustCompile(`([\w$]+)\s*=\s*\{\s*typeName:\s*"([\w.]+)"\s*,\s*methods:\s*\{`)
|
||||
|
||||
matches := serviceRe.FindAllStringSubmatchIndex(text, -1)
|
||||
for _, match := range matches {
|
||||
varName := text[match[2]:match[3]]
|
||||
typeName := text[match[4]:match[5]]
|
||||
|
||||
appendService(varName, typeName, match[0], match[1]-1)
|
||||
}
|
||||
|
||||
// 部分 bundle 把服务描述符直接放入数组,不预先赋给变量。
|
||||
anonymousServiceRe := regexp.MustCompile(`\{\s*typeName:\s*["']([\w.]+)["']\s*,\s*methods:\s*\{`)
|
||||
for _, match := range anonymousServiceRe.FindAllStringSubmatchIndex(text, -1) {
|
||||
typeName := text[match[2]:match[3]]
|
||||
appendService("", typeName, match[0], match[1]-1)
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// extractMethods 解析服务对象中的 RPC 方法列表。
|
||||
func extractMethods(methodsText string) []Method {
|
||||
var methods []Method
|
||||
|
||||
// 匹配包含方法名、输入、输出和调用类型的方法对象。
|
||||
methodRe := regexp.MustCompile(`\w+:\s*\{\s*name:\s*"([^"]+)"\s*,\s*I:\s*([\w$.]+)\s*,\s*O:\s*([\w$.]+)\s*,\s*kind:\s*[\w$.]+\.(Unary|ServerStreaming|ClientStreaming|BiDiStreaming)`)
|
||||
|
||||
matches := methodRe.FindAllStringSubmatch(methodsText, -1)
|
||||
for _, m := range matches {
|
||||
method := Method{
|
||||
Name: m[1],
|
||||
InputType: m[2],
|
||||
OutputType: m[3],
|
||||
Kind: m[4],
|
||||
}
|
||||
methods = append(methods, method)
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
// findMatchingBrace 查找花括号块的结束位置。
|
||||
func findMatchingBrace(text string, start int) int {
|
||||
depth := 0
|
||||
for i := start; i < len(text); i++ {
|
||||
if text[i] == '{' {
|
||||
depth++
|
||||
} else if text[i] == '}' {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// extractEnumValues 从数组起点解析枚举值。
|
||||
func extractEnumValues(text string, start int) []EnumValue {
|
||||
// 查找数组的配对结束括号。
|
||||
depth := 0
|
||||
end := start
|
||||
for i := start; i < len(text); i++ {
|
||||
if text[i] == '[' {
|
||||
depth++
|
||||
} else if text[i] == ']' {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
end = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arrayText := text[start:end]
|
||||
|
||||
var values []EnumValue
|
||||
valueRe := regexp.MustCompile(`\{\s*no:\s*(\d+)\s*,\s*name:\s*"([^"]+)"`)
|
||||
|
||||
matches := valueRe.FindAllStringSubmatch(arrayText, -1)
|
||||
for _, m := range matches {
|
||||
no, _ := strconv.Atoi(m[1])
|
||||
values = append(values, EnumValue{No: no, Name: m[2]})
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// types.go 定义协议提取器的领域结构、诊断状态和基础类型映射。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isGooglePkg 判断是否为无需重复生成的 Google 标准包。
|
||||
func isGooglePkg(pkg string) bool {
|
||||
return pkg == "google.protobuf" || pkg == "google.rpc"
|
||||
}
|
||||
|
||||
// scalarTypes 把运行时标量编号映射为 proto 类型。
|
||||
var scalarTypes = map[int]string{
|
||||
1: "double",
|
||||
2: "float",
|
||||
3: "int64",
|
||||
4: "uint64",
|
||||
5: "int32",
|
||||
6: "fixed64",
|
||||
7: "fixed32",
|
||||
8: "bool",
|
||||
9: "string",
|
||||
12: "bytes",
|
||||
13: "uint32",
|
||||
15: "sfixed32",
|
||||
16: "sfixed64",
|
||||
17: "sint32",
|
||||
18: "sint64",
|
||||
}
|
||||
|
||||
// strictExtractionValidation 控制校验失败是否终止提取。
|
||||
var strictExtractionValidation = true
|
||||
|
||||
// extractionDiagnostics 汇总字段解析和类型解析诊断。
|
||||
type extractionDiagnostics struct {
|
||||
totalFieldObjects int
|
||||
parsedFieldObjects int
|
||||
skippedFieldObjects int
|
||||
skippedFieldSamples []string
|
||||
unresolvedTypeRefs map[string]int
|
||||
emptyMessages []string
|
||||
placeholderHits []string
|
||||
declaredTypes int
|
||||
extractedTypes int
|
||||
missingDeclarations []string
|
||||
}
|
||||
|
||||
// newExtractionDiagnostics 创建一次提取任务的诊断容器。
|
||||
func newExtractionDiagnostics() *extractionDiagnostics {
|
||||
return &extractionDiagnostics{
|
||||
unresolvedTypeRefs: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
// addSkippedField 记录未能解析的字段样本和原因。
|
||||
func (d *extractionDiagnostics) addSkippedField(fieldObject string, reason error) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
d.totalFieldObjects++
|
||||
d.skippedFieldObjects++
|
||||
if len(d.skippedFieldSamples) < 20 {
|
||||
trimmed := strings.TrimSpace(fieldObject)
|
||||
if len(trimmed) > 140 {
|
||||
trimmed = trimmed[:140] + "..."
|
||||
}
|
||||
if reason != nil {
|
||||
d.skippedFieldSamples = append(d.skippedFieldSamples, fmt.Sprintf("%s | %s", reason.Error(), trimmed))
|
||||
} else {
|
||||
d.skippedFieldSamples = append(d.skippedFieldSamples, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addParsedField 累计成功解析的字段数量。
|
||||
func (d *extractionDiagnostics) addParsedField() {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
d.totalFieldObjects++
|
||||
d.parsedFieldObjects++
|
||||
}
|
||||
|
||||
// addUnresolvedType 按引用名称累计类型解析失败次数。
|
||||
func (d *extractionDiagnostics) addUnresolvedType(ref string) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
key := strings.TrimSpace(ref)
|
||||
if key == "" {
|
||||
key = "<empty>"
|
||||
}
|
||||
d.unresolvedTypeRefs[key]++
|
||||
}
|
||||
|
||||
// SetStrictMode 设置校验失败是否终止提取。
|
||||
func SetStrictMode(enabled bool) {
|
||||
strictExtractionValidation = enabled
|
||||
}
|
||||
|
||||
// activeDiagnostics 指向当前提取任务的诊断状态。
|
||||
var activeDiagnostics *extractionDiagnostics
|
||||
|
||||
// 字段解析正则覆盖压缩 bundle 的各类声明形式。
|
||||
var (
|
||||
noRe = regexp.MustCompile(`(?:^|[,{]\s*)no:\s*(\d+)`)
|
||||
nameRe = regexp.MustCompile(`(?:^|[,{]\s*)name:\s*["']([^"']+)["']`)
|
||||
kindRe = regexp.MustCompile(`(?:^|[,{]\s*)kind:\s*["']([^"']+)["']`)
|
||||
enumTypeRe = regexp.MustCompile(`[,\s]T:\s*[\w$.]+\.getEnumType\s*\(\s*([\w$.]+)\s*\)`)
|
||||
tRe = regexp.MustCompile(`[,\s]T:\s*([\w$.]+)`)
|
||||
oneofRe = regexp.MustCompile(`oneof:\s*["']([^"']+)["']`)
|
||||
repeatedRe = regexp.MustCompile(`repeated:\s*(!0|true)`)
|
||||
optRe = regexp.MustCompile(`opt:\s*(!0|true)`)
|
||||
keyRe = regexp.MustCompile(`[,\s]K:\s*(\d+)`)
|
||||
mapValueRe = regexp.MustCompile(`V:\s*\{([^}]*)\}`)
|
||||
mapValueKRe = regexp.MustCompile(`(?:^|[,{]\s*)kind:\s*["'](\w+)["']`)
|
||||
mapValueTRe = regexp.MustCompile(`[,\s]T:\s*([\w$.]+)`)
|
||||
shorthandTRe = regexp.MustCompile(`(?:^|[,\{])\s*T\s*(?:[,\}])`)
|
||||
oneofNameRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
fieldNameRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
placeholderRe = regexp.MustCompile(`^\s*(optional\s+|repeated\s+)?[A-Za-z_][A-Za-z0-9_.<>]*\s+(field_\d+|unknown(?:_[A-Za-z0-9_]+)?)\s*=\s*\d+\s*;`)
|
||||
varAliasRe = regexp.MustCompile(`\b(?:let|const|var)\s+([\w$]+)\s*=\s*([\w$]+)\s*(?:[,;])`)
|
||||
assignmentAliasRe = regexp.MustCompile(`(?:^|[;,({])\s*([\w$]+)\s*=\s*([\w$]+)\s*([,;}])`)
|
||||
webpackExportBlockRe = regexp.MustCompile(`[\w$]+\.d\(\s*[\w$]+\s*,\s*\{`)
|
||||
webpackExportEntryRe = regexp.MustCompile(`(?:^|[,\{])\s*([\w$]+)\s*:\s*\(\s*\)\s*=>\s*([\w$]+)`)
|
||||
moduleImportRe = regexp.MustCompile(`(?:\b(?:var|let|const)\s+|,)\s*([\w$]+)\s*=\s*[\w$]+\(\s*(\d+)\s*\)`)
|
||||
typeNameDeclarationRe = regexp.MustCompile(`(?:\bthis|[\w$]+)\.typeName\s*=\s*["']([\w.]+)["']`)
|
||||
serviceDeclarationRe = regexp.MustCompile(`\{\s*typeName\s*:\s*["']([\w.]+)["']\s*,\s*methods\s*:`)
|
||||
messageDeclarationRe = regexp.MustCompile(`\.makeMessageType\s*\(\s*["']([\w.]+)["']`)
|
||||
enumDeclarationRe = regexp.MustCompile(`\.makeEnum\s*\(\s*["']([\w.]+)["']`)
|
||||
legacyEnumDeclarationRe = regexp.MustCompile(`\.setEnumType\s*\(\s*[\w$]+\s*,\s*["']([\w.]+)["']`)
|
||||
streamCloseRe = regexp.MustCompile(`(?s)message\s+ExecClientControlMessage\s*\{.*?ExecClientStreamClose\s+stream_close\s*=\s*1\s*;`)
|
||||
shellStdoutRe = regexp.MustCompile(`(?s)message\s+ShellStream\s*\{.*?ShellStreamStdout\s+stdout\s*=\s*1\s*;`)
|
||||
)
|
||||
|
||||
// Field 描述一个待渲染的 protobuf 字段。
|
||||
type Field struct {
|
||||
// No 是字段编号。
|
||||
No int `json:"no"`
|
||||
// Name 是字段名称。
|
||||
Name string `json:"name"`
|
||||
// Kind 是标量、消息、枚举或映射类别。
|
||||
Kind string `json:"kind"`
|
||||
// T 保存标量编号或消息引用变量。
|
||||
T any `json:"T"`
|
||||
// Oneof 是字段所属的互斥分组。
|
||||
Oneof string `json:"oneof"`
|
||||
// Repeated 表示字段可以重复。
|
||||
Repeated bool `json:"repeated"`
|
||||
// Opt 表示字段为显式可选。
|
||||
Opt bool `json:"opt"`
|
||||
// MapKey 是映射键的标量编号。
|
||||
MapKey int `json:"K"`
|
||||
// MapValueKind 是映射值的标量或消息类别。
|
||||
MapValueKind string
|
||||
// MapValueT 保存映射值的标量编号或消息引用。
|
||||
MapValueT any
|
||||
}
|
||||
|
||||
// Message 描述提取出的消息及其源码位置。
|
||||
type Message struct {
|
||||
// TypeName 是消息的全限定类型名。
|
||||
TypeName string
|
||||
// VarName 是 JS 外部变量名。
|
||||
VarName string
|
||||
// InternalName 是 JS 内部类名。
|
||||
InternalName string
|
||||
// Fields 是消息字段列表。
|
||||
Fields []Field
|
||||
// Package 是消息所属协议包。
|
||||
Package string
|
||||
// ShortName 是包内嵌套类型名。
|
||||
ShortName string
|
||||
// Pos 是消息在 bundle 中的字节位置。
|
||||
Pos int
|
||||
// ModuleStart 是消息所在模块的起始位置。
|
||||
ModuleStart int
|
||||
}
|
||||
|
||||
// Enum 描述提取出的枚举及其源码位置。
|
||||
type Enum struct {
|
||||
// TypeName 是枚举的全限定类型名。
|
||||
TypeName string
|
||||
// VarName 是枚举对应的 JS 变量名。
|
||||
VarName string
|
||||
// Values 是枚举值列表。
|
||||
Values []EnumValue
|
||||
// Package 是枚举所属协议包。
|
||||
Package string
|
||||
// ShortName 是包内嵌套类型名。
|
||||
ShortName string
|
||||
// Pos 是枚举在 bundle 中的字节位置。
|
||||
Pos int
|
||||
// ModuleStart 是枚举所在模块的起始位置。
|
||||
ModuleStart int
|
||||
}
|
||||
|
||||
// EnumValue 描述单个枚举编号和名称。
|
||||
type EnumValue struct {
|
||||
// No 是枚举编号。
|
||||
No int
|
||||
// Name 是枚举名称。
|
||||
Name string
|
||||
}
|
||||
|
||||
// Service 描述提取出的服务及其源码位置。
|
||||
type Service struct {
|
||||
// TypeName 是服务的全限定类型名。
|
||||
TypeName string
|
||||
// VarName 是服务对应的 JS 变量名。
|
||||
VarName string
|
||||
// Methods 是服务方法列表。
|
||||
Methods []Method
|
||||
// Package 是服务所属协议包。
|
||||
Package string
|
||||
// ShortName 是服务包内名称。
|
||||
ShortName string
|
||||
// Pos 是服务在 bundle 中的字节位置。
|
||||
Pos int
|
||||
// ModuleStart 是服务所在模块的起始位置。
|
||||
ModuleStart int
|
||||
}
|
||||
|
||||
// Method 描述一个 RPC 方法的输入、输出和流模式。
|
||||
type Method struct {
|
||||
// Name 是 RPC 方法名。
|
||||
Name string
|
||||
// InputType 是输入消息引用变量。
|
||||
InputType string
|
||||
// OutputType 是输出消息引用变量。
|
||||
OutputType string
|
||||
// Kind 是一元或不同方向的流式调用类型。
|
||||
Kind string
|
||||
}
|
||||
|
||||
// symbolDef 保存符号对应的类型、类别和模块位置。
|
||||
type symbolDef struct {
|
||||
// TypeName 是符号对应的全限定类型名。
|
||||
TypeName string
|
||||
// Pos 是符号定义位置。
|
||||
Pos int
|
||||
// Kind 是消息或枚举类别。
|
||||
Kind string
|
||||
// ModuleStart 是符号所在模块起点。
|
||||
ModuleStart int
|
||||
}
|
||||
|
||||
// TypeResolver 通过局部符号、别名和短名称解析协议类型。
|
||||
type TypeResolver struct {
|
||||
bySymbol map[string][]symbolDef
|
||||
byAlias map[string][]symbolDef
|
||||
byShort map[string][]symbolDef
|
||||
moduleImports map[int]map[string]int
|
||||
}
|
||||
|
||||
// aliasIndex 按模块和目标符号保存别名集合。
|
||||
type aliasIndex map[int]map[string][]string
|
||||
@@ -0,0 +1,15 @@
|
||||
module github.com/leookun/cursor-byok/cursor-proto
|
||||
|
||||
go 1.25.8
|
||||
|
||||
require (
|
||||
github.com/jhump/protoreflect v1.18.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/jhump/protoreflect v1.18.0 h1:TOz0MSR/0JOZ5kECB/0ufGnC2jdsgZ123Rd/k4Z5/2w=
|
||||
github.com/jhump/protoreflect v1.18.0/go.mod h1:ezWcltJIVF4zYdIFM+D/sHV4Oh5LNU08ORzCGfwvTz8=
|
||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.1 h1:Dw1rslK/VotaUGYsv53XVWITr+5RCPXfvvlGrM/+B6w=
|
||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.1/go.mod h1:D9LBEowZyv8/iSu97FU2zmXG3JxVTmNw21mu63niFzU=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
|
||||
google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# extract.sh 从 Cursor 安装目录安全提取 Proto 文件。
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
INSTALLED_CURSOR_DEFAULT="/Applications/Cursor.app"
|
||||
INPUT_DEFAULT="$INSTALLED_CURSOR_DEFAULT"
|
||||
OUTPUT_DEFAULT="$PROJECT_DIR/proto"
|
||||
|
||||
INPUT_ROOT="${1:-$INPUT_DEFAULT}"
|
||||
OUTPUT_DIR="${2:-$OUTPUT_DEFAULT}"
|
||||
|
||||
canonicalize_path() {
|
||||
local path="$1"
|
||||
local parent
|
||||
local base
|
||||
if [[ -d "$path" ]]; then
|
||||
(cd "$path" && pwd -P)
|
||||
return
|
||||
fi
|
||||
parent="$(dirname "$path")"
|
||||
base="$(basename "$path")"
|
||||
if [[ ! -d "$parent" ]]; then
|
||||
echo "Parent directory does not exist: $parent" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s/%s\n' "$(cd "$parent" && pwd -P)" "$base"
|
||||
}
|
||||
|
||||
# 文件输入只提取自身;目录输入扫描工作台、扩展宿主和扩展产物。
|
||||
INPUT_PATHS=()
|
||||
|
||||
add_input() {
|
||||
local candidate="$1"
|
||||
local existing
|
||||
if [[ ! -f "$candidate" ]]; then
|
||||
return 0
|
||||
fi
|
||||
for existing in "${INPUT_PATHS[@]-}"; do
|
||||
[[ "$existing" == "$candidate" ]] && return
|
||||
done
|
||||
INPUT_PATHS+=("$candidate")
|
||||
}
|
||||
|
||||
if [[ -f "$INPUT_ROOT" ]]; then
|
||||
add_input "$INPUT_ROOT"
|
||||
elif [[ -d "$INPUT_ROOT" ]]; then
|
||||
CANDIDATES=(
|
||||
"$INPUT_ROOT/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js"
|
||||
"$INPUT_ROOT/Resources/app/out/vs/workbench/workbench.desktop.main.js"
|
||||
"$INPUT_ROOT/out/vs/workbench/workbench.desktop.main.js"
|
||||
"$INPUT_ROOT/workbench.desktop.main.js"
|
||||
"$INPUT_ROOT/Contents/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js"
|
||||
"$INPUT_ROOT/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js"
|
||||
"$INPUT_ROOT/out/vs/workbench/api/node/extensionHostProcess.js"
|
||||
"$INPUT_ROOT/extensionHostProcess.js"
|
||||
"$INPUT_ROOT/Contents/Resources/app/extensions/cursor-always-local/dist/main.js"
|
||||
"$INPUT_ROOT/Resources/app/extensions/cursor-always-local/dist/main.js"
|
||||
"$INPUT_ROOT/extensions/cursor-always-local/dist/main.js"
|
||||
"$INPUT_ROOT/cursor-always-local/dist/main.js"
|
||||
)
|
||||
for CANDIDATE in "${CANDIDATES[@]}"; do
|
||||
add_input "$CANDIDATE"
|
||||
done
|
||||
while IFS= read -r JS_FILE; do
|
||||
add_input "$JS_FILE"
|
||||
done < <(find "$INPUT_ROOT" -type f ! -path "*/node_modules/*" \( -name "workbench.desktop.main.js" -o -name "extensionHostProcess.js" -o -path "*/extensions/*/dist/main.js" \) | sort)
|
||||
fi
|
||||
|
||||
if [[ -z "${INPUT_PATHS[*]-}" ]]; then
|
||||
echo "No supported Cursor JS bundle found under: $INPUT_ROOT" >&2
|
||||
echo "Install/update Cursor, or pass an explicit input bundle:" >&2
|
||||
echo " $0 /path/to/Cursor.app [output-dir]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for INDEX in "${!INPUT_PATHS[@]}"; do
|
||||
INPUT_PATHS[$INDEX]="$(canonicalize_path "${INPUT_PATHS[$INDEX]}")"
|
||||
done
|
||||
OUTPUT_DIR="$(canonicalize_path "$OUTPUT_DIR")"
|
||||
CURRENT_DIR="$(pwd -P)"
|
||||
|
||||
case "$OUTPUT_DIR" in
|
||||
"/"|"$HOME"|"$PROJECT_DIR"|"$SCRIPT_DIR"|"$CURRENT_DIR")
|
||||
echo "Refusing unsafe output directory: $OUTPUT_DIR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
for INPUT_PATH in "${INPUT_PATHS[@]}"; do
|
||||
case "$INPUT_PATH" in
|
||||
"$OUTPUT_DIR"|"$OUTPUT_DIR"/*)
|
||||
echo "Refusing output directory that contains an input bundle: $OUTPUT_DIR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
OUTPUT_PARENT="$(dirname "$OUTPUT_DIR")"
|
||||
OUTPUT_BASENAME="$(basename "$OUTPUT_DIR")"
|
||||
TEMP_DIR="$(mktemp -d "$OUTPUT_PARENT/.${OUTPUT_BASENAME}.tmp.XXXXXX")"
|
||||
BACKUP_DIR=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
|
||||
rm -rf "$TEMP_DIR"
|
||||
fi
|
||||
if [[ -n "$BACKUP_DIR" && -e "$BACKUP_DIR" ]]; then
|
||||
if [[ ! -e "$OUTPUT_DIR" ]]; then
|
||||
mv "$BACKUP_DIR" "$OUTPUT_DIR"
|
||||
else
|
||||
rm -rf "$BACKUP_DIR"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
EXTRACT_ARGS=()
|
||||
for INPUT_PATH in "${INPUT_PATHS[@]}"; do
|
||||
EXTRACT_ARGS+=( -input "$INPUT_PATH" )
|
||||
done
|
||||
|
||||
(
|
||||
cd "$PROJECT_DIR"
|
||||
go run ./extractor \
|
||||
"${EXTRACT_ARGS[@]}" \
|
||||
-output "$TEMP_DIR" \
|
||||
-skip-format \
|
||||
-strict
|
||||
)
|
||||
|
||||
if [[ -e "$OUTPUT_DIR" ]]; then
|
||||
BACKUP_DIR="$(mktemp -d "$OUTPUT_PARENT/.${OUTPUT_BASENAME}.backup.XXXXXX")"
|
||||
rmdir "$BACKUP_DIR"
|
||||
mv "$OUTPUT_DIR" "$BACKUP_DIR"
|
||||
fi
|
||||
mv "$TEMP_DIR" "$OUTPUT_DIR"
|
||||
TEMP_DIR=""
|
||||
if [[ -n "$BACKUP_DIR" ]]; then
|
||||
rm -rf "$BACKUP_DIR"
|
||||
BACKUP_DIR=""
|
||||
fi
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# generate.sh 根据提取的 Proto 定义生成可供其他 Go module 使用的消息包。
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PROTO_DIR="$PROJECT_DIR/proto"
|
||||
MODULE_PATH="github.com/leookun/cursor-byok/cursor-proto"
|
||||
|
||||
command -v protoc >/dev/null 2>&1 || {
|
||||
echo "protoc is required" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v protoc-gen-go >/dev/null 2>&1 || {
|
||||
echo "protoc-gen-go is required" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for PROTO_FILE in agent_v1.proto aiserver_v1.proto; do
|
||||
if [[ ! -f "$PROTO_DIR/$PROTO_FILE" ]]; then
|
||||
echo "Missing Proto source: $PROTO_DIR/$PROTO_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
protoc \
|
||||
--proto_path="$PROTO_DIR" \
|
||||
--go_out="$PROJECT_DIR" \
|
||||
--go_opt="module=$MODULE_PATH" \
|
||||
"$PROTO_DIR/agent_v1.proto" \
|
||||
"$PROTO_DIR/aiserver_v1.proto"
|
||||
|
||||
echo "Generated Go packages under: $PROJECT_DIR/gen"
|
||||
Reference in New Issue
Block a user