From 2ac88ff0788e737536a7ade37165dbae3b1fdb17 Mon Sep 17 00:00:00 2001 From: barry Date: Wed, 25 Mar 2026 12:57:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E6=A0=B8=E5=BF=83=E8=83=BD=E5=8A=9B=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=A8=A1=E5=9D=97=E4=B8=8E=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/agents/dix-maintainer.agent.md | 28 +++ .github/copilot-instructions.md | 62 +++++++ .../instructions/changelog.instructions.md | 33 ++++ .../documentation.instructions.md | 33 ++++ .../pr-review-only.instructions.md | 13 ++ .../prompts/changelog-maintenance.prompt.md | 54 ++++++ .version/changelog/README.md | 19 ++ .version/changelog/Unreleased.md | 21 +++ .version/changelog/v2.0.0.md | 21 +++ Taskfile.yml | 2 +- dixhttp/server.go | 142 ++++++++++---- dixhttp/server_runtime_test.go | 60 +++++- dixhttp/template.html | 175 +++++++++++------- dixinternal/trace_chain_test.go | 2 +- dixtrace/trace.go | 1 - 15 files changed, 558 insertions(+), 108 deletions(-) create mode 100644 .github/agents/dix-maintainer.agent.md create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/changelog.instructions.md create mode 100644 .github/instructions/documentation.instructions.md create mode 100644 .github/instructions/pr-review-only.instructions.md create mode 100644 .github/prompts/changelog-maintenance.prompt.md create mode 100644 .version/changelog/README.md create mode 100644 .version/changelog/Unreleased.md create mode 100644 .version/changelog/v2.0.0.md diff --git a/.github/agents/dix-maintainer.agent.md b/.github/agents/dix-maintainer.agent.md new file mode 100644 index 0000000..a6437e1 --- /dev/null +++ b/.github/agents/dix-maintainer.agent.md @@ -0,0 +1,28 @@ +--- +name: Dix Maintainer +description: Use when working on dix core implementation, DI behavior changes, provider/inject debugging, trace diagnostics, or module-level README synchronization. +tools: [read, edit, search, execute, todo] +argument-hint: "描述你的目标(如修复 provider 解析、补测试、更新 trace 行为或同步 README)" +user-invocable: true +--- + +你是 `dix` 仓库的维护型工程代理,专注于 Go 依赖注入框架的实现与验证。 + +## 任务边界 + +- 优先处理:`dixinternal/`、`dixtrace/`、`dixhttp/`、`dixcontext/`、`dixglobal/`。 +- 修改以“最小必要变更”为原则,保持公开 API 与现有行为兼容。 +- 若任务属于 PR 评审场景,遵循仓库中的 `pr-review-only.instructions.md`(仅评审,不直接改码)。 + +## 工作方式 + +1. 先定位受影响的包与调用链,再动手修改。 +2. 优先补充或更新测试,尤其是 `*_test.go` 中的回归用例。 +3. 代码改动后,至少执行相关测试;跨包改动时执行全量 `go test ./...`。 +4. 若对外行为变化,提醒同步中英文 README 与模块文档。 + +## 输出要求 + +- 清楚说明:改了什么、为什么改、如何验证。 +- 标注风险点与后续建议(如性能、兼容性、可观测性)。 +- 回答保持简洁、可执行,必要时给出下一步命令建议。 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..e2290c1 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,62 @@ +# dix 项目指引 + +## 适用范围 + +- 本文件是仓库级 always-on 指引,适用于整个 `dix` 工作区。 +- 如存在更细分的 `*.instructions.md`,以细分规则作为补充,而非互相冲突。 + +## 技术栈与目标 + +- 语言:Go(见 `go.mod`,当前 `go 1.24`)。 +- 项目类型:依赖注入框架(DI),核心能力包括 Provider 注册、依赖解析、循环检测、trace 诊断与可视化(`dixhttp`)。 +- 修改实现时优先保持公开 API 与行为兼容,避免无关重构。 + +## 首选开发命令 + +- 测试:`task test` +- 静态检查:`task vet` +- Lint:`task lint` +- Web 示例:`task web-demo` + +如需直接使用 Go 命令: + +- `go test ./...` +- `go vet ./...` + +## 架构边界(修改前先定位) + +- `dix.go`:对外入口 API(创建容器、Provide/Inject 相关能力)。 +- `dixinternal/`:核心实现(provider、inject、cycle-check、diag、logger、option)。 +- `dixtrace/`:内存 trace 存储与查询。 +- `dixhttp/`:HTTP 可视化与 trace API。 +- `dixcontext/`:与 `context.Context` 集成。 +- `dixglobal/`:全局容器便捷封装。 + +## 项目特有约定 + +- 错误优先通过 `TryProvide` / `TryInject` 路径返回,避免在新增逻辑中引入不必要 panic。 +- 涉及 trace/诊断行为时,注意与环境变量约定保持一致(如 `DIX_TRACE_DI`、`DIX_DIAG_FILE`、`DIX_TRACE_FILE`)。 +- 优先补充或复用现有测试文件(尤其 `dixinternal/*_test.go`、`dixtrace/*_test.go`、`dixhttp/server_runtime_test.go`)。 + +## 实施原则(对 AI 代理) + +- 仅做与任务直接相关的最小改动,避免顺手大改。 +- 保持现有命名风格与目录组织,不随意移动包结构。 +- 新增行为时优先补测试,再改实现。 +- 变更对外行为时,同步更新 `README.md`/`README_zh.md` 与对应模块 README(如 `dixhttp/README*.md`)。 + +## 常见坑点 + +- `task test` 当前默认覆盖 `dixinternal/...`,若改动到其他包需补充执行 `go test ./...` 进行全量验证。 +- 项目包含 `go.work`,处理示例工程时注意模块工作区上下文。 + +## 参考文件 + +- `README.md` +- `README_zh.md` +- `Taskfile.yml` +- `dixinternal/dix.go` +- `dixinternal/provider.go` +- `dixinternal/diag_file.go` +- `dixtrace/trace.go` +- `dixhttp/server.go` diff --git a/.github/instructions/changelog.instructions.md b/.github/instructions/changelog.instructions.md new file mode 100644 index 0000000..d6bd7c4 --- /dev/null +++ b/.github/instructions/changelog.instructions.md @@ -0,0 +1,33 @@ +--- +name: Changelog 专项规范 +description: 仅用于维护 .version/changelog,保证 Unreleased 与版本文件结构稳定、分类一致、条目可追溯 +applyTo: ".version/changelog/*.md" +--- + +# dix Changelog 维护规范 + +本规则仅适用于 `.version/changelog/*.md`。 + +## 结构约束 + +- `Unreleased.md` 推荐分类:`新增` / `修复` / `变更` / `文档`。 +- 若某分类暂无内容,写"暂无"。 + +## 内容约束 + +- 仅基于可见改动编写条目,不杜撰能力或影响。 +- 单条应简洁、可读、可追溯,尽量以动词开头。 +- 重复事项需合并去重,避免同义重复。 +- 不改写历史版本文件语义,不重排已发布版本。 + +## 落版约束(release) + +- 版本号来源于 `.version/VERSION`。 +- 落版文件:`.version/changelog/.md`。 +- 文件头格式:`# [] - `。 +- 落版后需重建 `.version/changelog/Unreleased.md` 模板(四个分类)。 +- 落版后需同步更新 `.version/changelog/README.md` 索引。 + +## 协同建议 + +- 建议通过 agent 提示词执行:`/changelog-maintenance draft|release`。 diff --git a/.github/instructions/documentation.instructions.md b/.github/instructions/documentation.instructions.md new file mode 100644 index 0000000..5c8d9a1 --- /dev/null +++ b/.github/instructions/documentation.instructions.md @@ -0,0 +1,33 @@ +--- +name: 文档专项规范 +description: 适用于仓库文档写作与维护(README/docs/模块 README),确保中文技术文风、Mermaid 图示与文档导航一致 +applyTo: "**/*.md" +--- + +# dix 文档专项规范 + +仅在“项目文档内容”场景生效(如 `README.md`、`README_zh.md`、`docs/**`、`dixhttp/README*.md`)。 + +## 基本要求 + +- 默认使用中文技术文风,表达简洁、可执行、可复现。 +- 结构化写作:优先使用二级/三级标题与短列表,避免大段空泛描述。 +- 架构、流程、关系说明优先使用 Mermaid。 +- 仅描述仓库中已实现能力,不杜撰未落地特性。 + +## 导航与链接约束 + +- 导航尽量保持树形自上而下,避免在低层文档大量回链上层形成网状互链。 +- 避免使用 `../` 父目录相对路径,优先使用仓库根相对路径或同目录直达路径。 +- 索引页避免堆叠“上级入口/相关专题”重复导航块。 + +## 内容组织建议 + +- 明确区分“基础组件模块”(如中间件、配置、错误、trace)与“业务功能模块”说明。 +- 模块设计优先沉淀在模块分册,避免把所有设计细节堆到单一中心文档。 +- 若仅做措辞润色,不改变技术语义与行为结论。 + +## 变更联动 + +- 对外行为、配置项或示例有变化时,同步更新 `README.md` 与 `README_zh.md`。 +- `dixhttp` 行为变化需同步更新 `dixhttp/README.md` 与 `dixhttp/README_zh.md`。 diff --git a/.github/instructions/pr-review-only.instructions.md b/.github/instructions/pr-review-only.instructions.md new file mode 100644 index 0000000..362efbb --- /dev/null +++ b/.github/instructions/pr-review-only.instructions.md @@ -0,0 +1,13 @@ +--- +description: "Use when handling pull request review, code review feedback, or PR comment resolution tasks. In review mode, provide analysis and comments only, and do not modify repository files directly." +name: "PR Review Read-Only Mode" +--- +# Pull Request Review: Read-Only Execution + +- When the user asks to review a pull request, review comments, or code diffs, stay in **review-only mode**. +- Do **not** create, edit, rename, or delete repository files in this mode. +- Focus on: + - identifying issues and risks, + - suggesting concrete fixes, + - proposing patch snippets in chat only when explicitly requested. +- If the user explicitly switches from review to implementation (for example: "please apply the fixes now"), confirm intent once and then proceed with edits. diff --git a/.github/prompts/changelog-maintenance.prompt.md b/.github/prompts/changelog-maintenance.prompt.md new file mode 100644 index 0000000..8b9869f --- /dev/null +++ b/.github/prompts/changelog-maintenance.prompt.md @@ -0,0 +1,54 @@ +--- +name: changelog-maintenance +description: 维护 .version/changelog(更新 Unreleased 或执行版本落版) +argument-hint: "模式:draft(更新 Unreleased)或 release(按 .version/VERSION 落版)" +agent: agent +--- + +你是本仓库的 Changelog 维护助手。 + +## 目标 + +- `draft` 模式:根据当前改动更新 `.version/changelog/Unreleased.md`。 +- `release` 模式:将 `Unreleased.md` 落版为 `.version/VERSION` 对应版本文件,并重建空模板。 + +## 必读上下文 + +在开始前先读取并遵循: + +- `.github/copilot-instructions.md` +- `.version/changelog/README.md` +- `.version/changelog/Unreleased.md` +- `.version/VERSION` +- 当前工作区 diff(如可获取) + +## 执行规则 + +1. 只基于可见改动生成条目,不杜撰。 +2. 分类使用:`新增` / `修复` / `变更` / `文档`。 +3. 语言使用中文技术文风,单条简洁,避免重复。 +4. 不改写历史版本块语义与顺序。 + +## 模式细则 + +### draft + +- 仅更新 `.version/changelog/Unreleased.md`。 +- 若缺少分类小节则补齐;无内容的小节写"暂无"。 +- 直接基于当前工作区改动与提交语义生成草稿,不依赖本地脚本输出。 + +### release + +- 读取 `.version/VERSION` 作为目标版本号(当前为 `v2.0.0`)。 +- 将 `Unreleased.md` 内容迁移到新版本文件:`.version/changelog/.md`。 +- 版本文件标题格式:`# [] - `。 +- 重建 `.version/changelog/Unreleased.md` 空模板(四个分类且初始值为"暂无")。 +- 同步更新 `.version/changelog/README.md` 中的版本索引。 + +## 输出要求 + +- 直接给出对 `.version/changelog/` 相关文件的修改(补丁或已应用结果)。 +- 末尾附一段简短自检: + - 是否仅改动允许范围; + - 是否完成分类与去重; + - 是否保持历史版本不变。 diff --git a/.version/changelog/README.md b/.version/changelog/README.md new file mode 100644 index 0000000..1273b59 --- /dev/null +++ b/.version/changelog/README.md @@ -0,0 +1,19 @@ +# Changelog 索引 + +本目录保存项目变更记录,采用"一个版本一个文件"的方式维护。 + +## 文件约定 + +- `Unreleased.md`:当前开发中变更(待发布)。 +- `vX.Y.Z.md`:已发布版本变更(例如 `v2.0.0.md`)。 + +## 当前版本文件 + +- [`Unreleased.md`](Unreleased.md) +- [`v2.0.0.md`](v2.0.0.md) + +## 维护约定 + +- 分类保持:`新增` / `修复` / `变更` / `文档`。 +- 发布时将 `Unreleased.md` 内容迁移到新版本文件,并重建空模板。 +- 历史版本文件只做勘误,不改写语义与顺序。 diff --git a/.version/changelog/Unreleased.md b/.version/changelog/Unreleased.md new file mode 100644 index 0000000..b78bd1a --- /dev/null +++ b/.version/changelog/Unreleased.md @@ -0,0 +1,21 @@ +# [Unreleased] + +> 推荐维护方式: +> +> - 建议通过 agent 提示词执行:`/changelog-maintenance draft|release` + +## 新增 + +暂无 + +## 修复 + +暂无 + +## 变更 + +暂无 + +## 文档 + +暂无 diff --git a/.version/changelog/v2.0.0.md b/.version/changelog/v2.0.0.md new file mode 100644 index 0000000..04b5b7e --- /dev/null +++ b/.version/changelog/v2.0.0.md @@ -0,0 +1,21 @@ +# [v2.0.0] - 2026-03-25 + +## 新增 + +- 新增 v2 依赖注入核心能力:Provider 注册、依赖解析、循环检测,以及 `TryProvide` / `TryInject` 安全调用路径。 +- 新增 `dixtrace` 内存 trace 存储与查询能力,并支持通过环境变量控制 trace 文件落盘。 +- 新增 `dixhttp` 可视化模块,支持依赖关系与 trace 的 HTTP 查看与调试。 +- 新增 `dixcontext` 与 `dixglobal` 模块,分别用于 `context.Context` 集成与全局容器便捷封装。 + +## 修复 + +暂无 + +## 变更 + +- 模块主版本升级到 `v2`(`module github.com/pubgo/dix/v2`),并统一公开入口与示例组织。 +- 诊断与追踪能力统一到 `DIX_TRACE_DI`、`DIX_DIAG_FILE`、`DIX_TRACE_FILE` 等环境变量约定。 + +## 文档 + +- 更新与完善 `README.md`、`README_zh.md`、`dixhttp/README.md`、`dixhttp/README_zh.md` 及 `docs/*` 文档,覆盖 v2 功能与使用方式。 diff --git a/Taskfile.yml b/Taskfile.yml index ce490f2..05b9398 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,4 +1,4 @@ -version: '3' +version: 3 tasks: test: diff --git a/dixhttp/server.go b/dixhttp/server.go index 5a2515c..a69c9ed 100644 --- a/dixhttp/server.go +++ b/dixhttp/server.go @@ -534,48 +534,23 @@ func (s *Server) extractDependencyData(pkgFilter string, limit int) *DependencyD Edges: []EdgeInfo{}, } - // Extract provider information using the public API providerDetails := s.dix.GetProviderDetails() + data.Providers = aggregateProviderInfos(providerDetails, pkgFilter, limit) - count := 0 - for i, detail := range providerDetails { - // Apply package filter if specified - if pkgFilter != "" { - pkg := extractPackage(detail.OutputType) - if pkg != pkgFilter { - continue - } - } - - // Apply limit - if limit > 0 && count >= limit { - break - } - count++ - - providerID := fmt.Sprintf("provider_%s_%d", detail.OutputType, i) - providerInfo := ProviderInfo{ - ID: providerID, - OutputType: detail.OutputType, - OutputPkg: detail.OutputPkg, - FunctionName: detail.FunctionName, - FunctionPkg: detail.FunctionPkg, - FunctionFile: detail.FunctionFile, - FunctionLine: detail.FunctionLine, - InputTypes: detail.InputTypes, - InputPkgs: detail.InputPkgs, + for _, provider := range data.Providers { + outputTypes := provider.OutputTypes + if len(outputTypes) == 0 && provider.OutputType != "" { + outputTypes = []string{provider.OutputType} } - - // Add edges from input types to provider output - for _, inputTypeStr := range detail.InputTypes { - data.Edges = append(data.Edges, EdgeInfo{ - From: inputTypeStr, - To: detail.OutputType, - Type: "provider", - }) + for _, outputType := range outputTypes { + for _, inputTypeStr := range provider.InputTypes { + data.Edges = append(data.Edges, EdgeInfo{ + From: inputTypeStr, + To: outputType, + Type: "provider", + }) + } } - - data.Providers = append(data.Providers, providerInfo) } // Extract object information using the public API @@ -616,6 +591,96 @@ func (s *Server) extractDependencyData(pkgFilter string, limit int) *DependencyD return data } +func aggregateProviderInfos(details []dixinternal.ProviderDetails, pkgFilter string, limit int) []ProviderInfo { + type providerBucket struct { + provider ProviderInfo + outputSeen map[string]bool + inputSeen map[string]bool + } + + buckets := make(map[string]*providerBucket) + order := make([]string, 0, len(details)) + + for _, detail := range details { + if pkgFilter != "" { + pkg := extractPackage(detail.OutputType) + if pkg != pkgFilter { + continue + } + } + + key := providerAggregateKey(detail) + bucket, exists := buckets[key] + if !exists { + bucket = &providerBucket{ + provider: ProviderInfo{ + ID: "provider_" + key, + OutputType: detail.OutputType, + OutputPkg: detail.OutputPkg, + FunctionName: detail.FunctionName, + FunctionPkg: detail.FunctionPkg, + FunctionFile: detail.FunctionFile, + FunctionLine: detail.FunctionLine, + OutputTypes: make([]string, 0, 4), + InputTypes: make([]string, 0, 8), + InputPkgs: make([]string, 0, 8), + }, + outputSeen: make(map[string]bool), + inputSeen: make(map[string]bool), + } + buckets[key] = bucket + order = append(order, key) + } + + if out := strings.TrimSpace(detail.OutputType); out != "" && !bucket.outputSeen[out] { + bucket.outputSeen[out] = true + bucket.provider.OutputTypes = append(bucket.provider.OutputTypes, out) + if bucket.provider.OutputType == "" { + bucket.provider.OutputType = out + } + } + + for i, in := range detail.InputTypes { + in = strings.TrimSpace(in) + if in == "" || bucket.inputSeen[in] { + continue + } + bucket.inputSeen[in] = true + bucket.provider.InputTypes = append(bucket.provider.InputTypes, in) + + pkg := "" + if i < len(detail.InputPkgs) { + pkg = strings.TrimSpace(detail.InputPkgs[i]) + } + bucket.provider.InputPkgs = append(bucket.provider.InputPkgs, pkg) + } + } + + providers := make([]ProviderInfo, 0, len(order)) + for _, key := range order { + providers = append(providers, buckets[key].provider) + } + + if limit > 0 && len(providers) > limit { + providers = providers[:limit] + } + + return providers +} + +func providerAggregateKey(detail dixinternal.ProviderDetails) string { + if detail.FunctionFile != "" && detail.FunctionLine > 0 { + return fmt.Sprintf("%s:%d", detail.FunctionFile, detail.FunctionLine) + } + if detail.FunctionName != "" { + return detail.FunctionName + } + if detail.OutputType != "" { + return detail.OutputType + } + return "unknown" +} + // Data types // StatsData contains summary statistics @@ -667,6 +732,7 @@ type DependencyData struct { type ProviderInfo struct { ID string `json:"id"` OutputType string `json:"output_type"` + OutputTypes []string `json:"output_types,omitempty"` OutputPkg string `json:"output_pkg"` FunctionName string `json:"function_name"` FunctionPkg string `json:"function_pkg"` diff --git a/dixhttp/server_runtime_test.go b/dixhttp/server_runtime_test.go index ccdabde..db1afa3 100644 --- a/dixhttp/server_runtime_test.go +++ b/dixhttp/server_runtime_test.go @@ -12,10 +12,12 @@ import ( "github.com/pubgo/dix/v2/dixinternal" ) -type runtimeStatsDep struct{} -type basePathTypeDep interface { - Name() string -} +type ( + runtimeStatsDep struct{} + basePathTypeDep interface { + Name() string + } +) type basePathTypeDepImpl struct{} @@ -281,3 +283,53 @@ func TestWriteJSONEncodeFailureReturns500(t *testing.T) { t.Fatalf("expected status %d, got %d", http.StatusInternalServerError, rr.Code) } } + +func TestHandleDependenciesAggregatesProviderOutputs(t *testing.T) { + type depA struct{} + type depB struct{} + type depOut struct { + A *depA + B *depB + } + + di := dixinternal.New() + di.Provide(func() depOut { + return depOut{A: &depA{}, B: &depB{}} + }) + + server := NewServer(di) + req := httptest.NewRequest(http.MethodGet, "/api/dependencies", nil) + rr := httptest.NewRecorder() + + server.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code) + } + + var resp DependencyData + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + var target *ProviderInfo + for i := range resp.Providers { + p := &resp.Providers[i] + if len(p.OutputTypes) >= 2 { + target = p + break + } + } + + if target == nil { + t.Fatalf("expected aggregated provider with >=2 output types, got %+v", resp.Providers) + } + + if len(target.OutputTypes) != 2 { + t.Fatalf("expected 2 output types from aggregated provider, got %+v", target.OutputTypes) + } + + if target.FunctionName == "" { + t.Fatalf("expected function_name on aggregated provider, got %+v", target) + } +} diff --git a/dixhttp/template.html b/dixhttp/template.html index ecf4a4a..ab3dad0 100644 --- a/dixhttp/template.html +++ b/dixhttp/template.html @@ -413,9 +413,14 @@

函数名

输出类型

-
+
+ +

函数包路径

@@ -474,7 +479,7 @@

启动耗时

- @@ -597,8 +602,12 @@

图例

节点信息
-
+
+ +
@@ -2403,9 +2412,11 @@

图例

// Search in providers this.allData.providers.forEach(provider => { const fnName = provider.function_name || ''; - const outputType = provider.output_type || ''; + const outputTypes = this.providerOutputTypes(provider); + const outputType = outputTypes[0] || ''; + const outputsText = outputTypes.join(' | '); - if (fnName.toLowerCase().includes(query) || outputType.toLowerCase().includes(query)) { + if (fnName.toLowerCase().includes(query) || outputsText.toLowerCase().includes(query)) { if (!seen.has(provider.id)) { seen.add(provider.id); results.push({ @@ -2433,17 +2444,20 @@

图例

} }); - // Search output type - if (outputType.toLowerCase().includes(query) && !seen.has(outputType)) { - seen.add(outputType); + // Search output types + outputTypes.forEach(outType => { + if (!outType || !outType.toLowerCase().includes(query) || seen.has(outType)) { + return; + } + seen.add(outType); results.push({ - id: outputType, + id: outType, type: 'type', - label: this.formatTypeName(outputType), - fullName: outputType, - data: { type: 'type', fullType: outputType } + label: this.formatTypeName(outType), + fullName: outType, + data: { type: 'type', fullType: outType } }); - } + }); }); // Limit results @@ -2459,7 +2473,10 @@

图例

this.showDependencyGraph(result.fullName, 'type'); } else if (result.type === 'provider') { // Provider:以其输出类型为中心显示依赖图 - this.showDependencyGraph(result.outputType, 'type'); + const outputType = this.primaryOutputType(result.data || {}) || result.outputType; + if (outputType) { + this.showDependencyGraph(outputType, 'type'); + } this.selectedNode = { data: result.data }; } }, @@ -2489,13 +2506,13 @@

图例

const typeToConsumers = new Map(); // inputType -> providers that use it this.allData.providers.forEach(provider => { - const outputType = provider.output_type; - if (outputType) { + const outputTypes = this.providerOutputTypes(provider); + outputTypes.forEach(outputType => { if (!typeToProviders.has(outputType)) { typeToProviders.set(outputType, []); } typeToProviders.get(outputType).push(provider); - } + }); (provider.input_types || []).forEach(inputType => { if (!typeToConsumers.has(inputType)) { @@ -2549,10 +2566,11 @@

图例

// 找到使用这个类型作为输入的 providers,获取它们的输出类型 const consumers = typeToConsumers.get(type) || []; consumers.forEach(p => { - const outputType = p.output_type; - if (outputType && !visited.has(outputType)) { - queue.push({ type: outputType, level: level + 1 }); - } + this.providerOutputTypes(p).forEach(outputType => { + if (outputType && !visited.has(outputType)) { + queue.push({ type: outputType, level: level + 1 }); + } + }); }); } return result; @@ -2678,7 +2696,7 @@

图例

this.allData.providers.forEach((provider) => { const providerNodeId = provider.id; const fnName = this.providerNodeLabel(provider); - const outputType = this.formatTypeName(provider.output_type); + const outputTypes = this.providerOutputTypes(provider); // Provider node if (!nodeMap.has(providerNodeId)) { @@ -2694,26 +2712,27 @@

图例

nodeMap.set(providerNodeId, true); } - // Output type node - if (!nodeMap.has(provider.output_type)) { - nodes.push({ - id: provider.output_type, - label: outputType, - title: '类型: ' + provider.output_type, - color: { background: '#bfdbfe', border: '#3b82f6' }, - shape: 'ellipse', - font: { size: 10 }, - data: { type: 'type', fullType: provider.output_type, packagePath: typePkgMap.get(provider.output_type) || provider.output_pkg || '' } - }); - nodeMap.set(provider.output_type, true); - } + // Output type nodes + Provider -> Output edges + outputTypes.forEach((outType) => { + if (!nodeMap.has(outType)) { + nodes.push({ + id: outType, + label: this.formatTypeName(outType), + title: '类型: ' + outType, + color: { background: '#bfdbfe', border: '#3b82f6' }, + shape: 'ellipse', + font: { size: 10 }, + data: { type: 'type', fullType: outType, packagePath: typePkgMap.get(outType) || provider.output_pkg || '' } + }); + nodeMap.set(outType, true); + } - // Provider -> Output edge - edges.push({ - from: providerNodeId, - to: provider.output_type, - arrows: 'to', - color: { color: '#22c55e' } + edges.push({ + from: providerNodeId, + to: outType, + arrows: 'to', + color: { color: '#22c55e' } + }); }); // Input types @@ -2990,25 +3009,26 @@

图例

nodeMap.set(providerNodeId, true); } - const outputType = provider.output_type; - if (!nodeMap.has(outputType)) { - nodes.push({ - id: outputType, - label: this.formatTypeName(outputType), - title: '类型: ' + outputType, - color: { background: '#bfdbfe', border: '#3b82f6' }, - shape: 'ellipse', - font: { size: 10 }, - data: { type: 'type', fullType: outputType, packagePath: typePkgMap.get(outputType) || provider.output_pkg || '' } - }); - nodeMap.set(outputType, true); - } + this.providerOutputTypes(provider).forEach((outType) => { + if (!nodeMap.has(outType)) { + nodes.push({ + id: outType, + label: this.formatTypeName(outType), + title: '类型: ' + outType, + color: { background: '#bfdbfe', border: '#3b82f6' }, + shape: 'ellipse', + font: { size: 10 }, + data: { type: 'type', fullType: outType, packagePath: typePkgMap.get(outType) || provider.output_pkg || '' } + }); + nodeMap.set(outType, true); + } - edges.push({ - from: providerNodeId, - to: outputType, - arrows: 'to', - color: { color: '#22c55e' } + edges.push({ + from: providerNodeId, + to: outType, + arrows: 'to', + color: { color: '#22c55e' } + }); }); (provider.input_types || []).forEach(inputType => { @@ -3118,6 +3138,25 @@

图例

return name; }, + providerOutputTypes(provider) { + if (!provider || typeof provider !== 'object') { + return []; + } + const fromList = Array.isArray(provider.output_types) + ? provider.output_types.map(v => String(v || '').trim()).filter(v => v.length > 0) + : []; + if (fromList.length > 0) { + return fromList; + } + const fallback = String(provider.output_type || '').trim(); + return fallback ? [fallback] : []; + }, + + primaryOutputType(provider) { + const list = this.providerOutputTypes(provider); + return list.length > 0 ? list[0] : ''; + }, + formatDurationNs(v) { const ns = Number(v || 0); if (!Number.isFinite(ns) || ns <= 0) return '0 ms'; @@ -3229,6 +3268,8 @@

图例

findProviderByRuntimeStat(stat) { return (this.allData?.providers || []).find(p => p.function_name === stat.function_name && p.output_type === stat.output_type + ) || (this.allData?.providers || []).find(p => + p.function_name === stat.function_name && this.providerOutputTypes(p).includes(stat.output_type) ) || (this.allData?.providers || []).find(p => p.function_name === stat.function_name) || null; }, @@ -3290,7 +3331,15 @@

图例

buildProviderTooltip(provider) { let tip = '函数: ' + provider.function_name + '\n'; - tip += '输出: ' + provider.output_type + '\n'; + const outputTypes = this.providerOutputTypes(provider); + if (outputTypes.length <= 1) { + tip += '输出: ' + (outputTypes[0] || '-') + '\n'; + } else { + tip += `输出(${outputTypes.length}):\n`; + outputTypes.forEach((t, i) => { + tip += ' ' + (i + 1) + '. ' + t + '\n'; + }); + } if (provider.input_types && provider.input_types.length > 0) { tip += '输入:\n'; provider.input_types.forEach((t, i) => { diff --git a/dixinternal/trace_chain_test.go b/dixinternal/trace_chain_test.go index db1e8c0..69d9c6a 100644 --- a/dixinternal/trace_chain_test.go +++ b/dixinternal/trace_chain_test.go @@ -541,7 +541,7 @@ func TestResolveProviderSpanContainsProviderFunctionSignature(t *testing.T) { func TestResolveProviderSpanContainsProviderFunctionListForMultiProviders(t *testing.T) { dixtrace.ResetForTest() - type multiProviderSvc interface{} + type multiProviderSvc any type implA struct{} type implB struct{} diff --git a/dixtrace/trace.go b/dixtrace/trace.go index 4bd9fb0..05c8999 100644 --- a/dixtrace/trace.go +++ b/dixtrace/trace.go @@ -149,7 +149,6 @@ func (s *Span) End(err error, args ...any) { OccurredAt: time.Now().UnixNano(), Attrs: attrs, }) - } func nextTraceID() string {