From e6929a1825d06b8986ee0c4cd3dc9f3b7e48255a Mon Sep 17 00:00:00 2001 From: luwanzhou Date: Sun, 14 Jun 2026 23:56:32 +0800 Subject: [PATCH] feat(examples): add research-team-insight end-to-end workflow (Go) A Go workflow (subtopic 3) that composes existing gitlink-cli commands into a reproducible research-team insight pipeline: [search +users] -> profile statistics x N (+ user +info) -> aggregate (ability matrix / discipline coverage / roles / languages) -> render team report (self-contained HTML, inline SVG: ability radar overlay + collaboration network graph) -> archive (team-insight.md + command_log.json) Implemented in Go to match the main repo's stack (shared go.mod, covered by go build / vet / test / gofmt); standard library only, no third-party deps. Handles Windows .cmd CLI shims, retries transient TLS timeouts, and auto-flags mirror-inflated accounts in the report. Includes unit tests, docs (architecture + runbook + SVG diagram), run_demo.ps1, sample config, and real demo outputs (3 GitLink members, 19 command invocations). Reuses the gitlink-research-profile skill's portrait model. Co-Authored-By: Claude Opus 4.8 --- .../workflows/research-team-insight/README.md | 82 +++ .../config/team.example.json | 4 + .../docs/architecture.md | 42 ++ .../docs/assets/architecture.svg | 56 ++ .../research-team-insight/docs/runbook.md | 70 +++ .../examples/demo_outputs/command_log.json | 230 ++++++++ .../demo_outputs/data/jiangtx/activity.json | 45 ++ .../data/jiangtx/contribution.json | 32 ++ .../demo_outputs/data/jiangtx/develop.json | 25 + .../demo_outputs/data/jiangtx/major.json | 8 + .../demo_outputs/data/jiangtx/role.json | 24 + .../demo_outputs/data/jiangtx/user.json | 36 ++ .../data/wangyue111/activity.json | 45 ++ .../data/wangyue111/contribution.json | 100 ++++ .../demo_outputs/data/wangyue111/develop.json | 31 + .../demo_outputs/data/wangyue111/major.json | 6 + .../demo_outputs/data/wangyue111/role.json | 24 + .../demo_outputs/data/wangyue111/user.json | 36 ++ .../demo_outputs/data/yystopf/activity.json | 45 ++ .../data/yystopf/contribution.json | 544 ++++++++++++++++++ .../demo_outputs/data/yystopf/develop.json | 53 ++ .../demo_outputs/data/yystopf/major.json | 29 + .../demo_outputs/data/yystopf/role.json | 24 + .../demo_outputs/data/yystopf/user.json | 36 ++ .../examples/demo_outputs/team-insight.md | 41 ++ .../examples/demo_outputs/team-report.html | 22 + .../research-team-insight/scripts/insight.go | 536 +++++++++++++++++ .../scripts/insight_test.go | 168 ++++++ .../research-team-insight/scripts/render.go | 348 +++++++++++ .../scripts/run_demo.ps1 | 27 + 30 files changed, 2769 insertions(+) create mode 100644 examples/workflows/research-team-insight/README.md create mode 100644 examples/workflows/research-team-insight/config/team.example.json create mode 100644 examples/workflows/research-team-insight/docs/architecture.md create mode 100644 examples/workflows/research-team-insight/docs/assets/architecture.svg create mode 100644 examples/workflows/research-team-insight/docs/runbook.md create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/command_log.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/activity.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/contribution.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/develop.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/major.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/role.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/user.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/activity.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/contribution.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/develop.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/major.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/role.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/user.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/activity.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/contribution.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/develop.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/major.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/role.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/user.json create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/team-insight.md create mode 100644 examples/workflows/research-team-insight/examples/demo_outputs/team-report.html create mode 100644 examples/workflows/research-team-insight/scripts/insight.go create mode 100644 examples/workflows/research-team-insight/scripts/insight_test.go create mode 100644 examples/workflows/research-team-insight/scripts/render.go create mode 100644 examples/workflows/research-team-insight/scripts/run_demo.ps1 diff --git a/examples/workflows/research-team-insight/README.md b/examples/workflows/research-team-insight/README.md new file mode 100644 index 0000000..1b47020 --- /dev/null +++ b/examples/workflows/research-team-insight/README.md @@ -0,0 +1,82 @@ +# 科研团队洞察工作流(research-team-insight) + +面向 GitLink 竞赛**子赛题三**的端到端自动化工作流示例(Go 实现,与主仓库技术栈一致)。 + +从"一组成员"或"一个研究方向"出发,用 `gitlink-cli` 串联**用户发现 → 多人画像采集 → 聚合分析 → +可视化 → 报告归档**的完整闭环,自动产出团队**可视化报告(HTML)**、**洞察报告(Markdown)**与 +**命令执行日志**。可视化为内联 SVG 的自包含 HTML(能力雷达 + 协作网络图),浏览器直接打开。 + +## 命令链路(≥3 个命令) + +``` +search +users (可选,按方向发现成员) + └─► profile +ability/+major/+role/+activity/+contribution + user +info (每位成员 ×6) + └─► 聚合分析(能力矩阵 / 学科覆盖 / 角色结构) + └─► 渲染团队 HTML(能力雷达 + 协作网络图) + └─► 洞察报告 team-insight.md + command_log.json +``` + +> 底层通过 `gitlink-cli api GET /users/{login}/statistics/*` 采集(无需 `profile` 命令组也可运行)。 + +## 交付物 + +- `scripts/insight.go` — 工作流主程序(编排 + 采集 + 聚合) +- `scripts/render.go` — Markdown / HTML+SVG 渲染 +- `scripts/insight_test.go` — 单元测试 +- `scripts/run_demo.ps1` — 一键复现脚本 +- `config/team.example.json` — 示例团队配置 +- `examples/demo_outputs/` — 真实跑通的固定示例输出(HTML / Markdown / 命令日志 / 原始数据) +- `docs/architecture.md` + `docs/assets/architecture.svg` — 架构与流程图 +- `docs/runbook.md` — 运行手册与复现步骤 + +## 实现语言 + +主实现采用 **Go**,与 `gitlink-cli` 主仓库技术栈一致:复用根 `go.mod`,可被主仓库 CI +(`go build` / `golangci-lint` / `go test`)直接覆盖;仅用标准库生成内联 SVG,无第三方依赖。 + +## 快速开始 + +```bash +# 指定成员 +go run ./scripts --members "yystopf,jiangtx,wangyue111" --team-name "课题组A" --out ./out + +# 团队配置文件 +go run ./scripts --config config/team.example.json --out ./out + +# 按研究方向自动发现成员 +go run ./scripts --keyword "联邦学习" --max 5 --out ./out + +# 离线复现(用已采集数据,不访问网络) +go run ./scripts --config config/team.example.json --out ./out \ + --no-fetch --data-root ./examples/demo_outputs/data +``` + +> **Windows 提示**:`gitlink-cli` 为 npm 的 `.CMD` 包装,请用 `--cli-bin <路径>\gitlink-cli.CMD` +> 或设置环境变量 `GITLINK_CLI_BIN`;`scripts/run_demo.ps1` 已自动处理。 + +输出(`--out` 目录): + +| 文件 | 说明 | +|------|------| +| `team-report.html` | 团队可视化(能力雷达、能力矩阵、共有/独有方向、协作网络图)——浏览器直接打开 | +| `team-insight.md` | 团队洞察报告(能力矩阵、学科覆盖、技术栈、角色结构、协作建议) | +| `command_log.json` | 完整命令执行日志(可复现、可审计) | +| `data//*.json` | 每位成员的原始画像数据 | + +## 依赖 + +- Go 1.21+(仅标准库;复用主仓库 `go.mod`) +- `gitlink-cli`(已登录;用 `api GET` 采集,无需 `profile` 命令组) + +## 真实验证 + +`examples/demo_outputs/` 为对真实账号(@yystopf 何慧·国防科技大学、@jiangtx、@wangyue111) +跑通的固定输出,共 19 条命令调用。工作流对瞬时 TLS 超时自动重试,并对镜像聚合账号在报告中 +自动标注"分布偏泛"。详见 [`docs/runbook.md`](docs/runbook.md)。 + +## 与子赛题二/四的关系 + +- 子赛题四的 [`gitlink-research-profile`](../../../skills/gitlink-research-profile/SKILL.md) Skill 定义 + "画像能力 + 单人/团队报告规范"。 +- 本工作流(子赛题三)把该能力**串联成可复现的端到端解决方案**,加入用户发现、批量编排、聚合分析 + 与归档,产出团队级可视化与洞察成果。 diff --git a/examples/workflows/research-team-insight/config/team.example.json b/examples/workflows/research-team-insight/config/team.example.json new file mode 100644 index 0000000..dc2afe1 --- /dev/null +++ b/examples/workflows/research-team-insight/config/team.example.json @@ -0,0 +1,4 @@ +{ + "team_name": "gitlink-cli 贡献组", + "members": ["yystopf", "jiangtx", "wangyue111"] +} diff --git a/examples/workflows/research-team-insight/docs/architecture.md b/examples/workflows/research-team-insight/docs/architecture.md new file mode 100644 index 0000000..ce294e3 --- /dev/null +++ b/examples/workflows/research-team-insight/docs/architecture.md @@ -0,0 +1,42 @@ +# 架构与流程说明 + +![架构图](assets/architecture.svg) + +## 设计目标 + +把分散的 GitLink 平台统计能力,编排成一个**可复现、可审计、零第三方依赖**的科研团队洞察流水线, +输出供科研评审/组队/选题直接使用的可视化与结构化成果。 + +## 五个阶段 + +| 阶段 | 动作 | 使用的命令 | 产物 | +|------|------|-----------|------| +| ① 成员发现 | 由方向关键词检索候选,或直接使用给定名单 | `gitlink-cli search +users -k`(可选) | 成员 login 列表 | +| ② 画像采集 | 对每位成员拉取 5 类统计 + 基础资料 | `api GET /users/{login}/statistics/{develop,major,role,activity}`、`/headmaps`、`user +info` | `data//*.json` | +| ③ 聚合分析 | 计算能力矩阵均值、学科共有/独有、技术栈、角色结构、最强成员 | `insight.go`(标准库) | 内存聚合结构 | +| ④ 可视化 | 渲染能力雷达(多成员叠加)+ 能力矩阵 + 共有/独有方向 + 协作网络图 | `render.go`(内联 SVG) | `team-report.html` | +| ⑤ 归档 | 生成洞察报告与命令日志 | `insight.go` | `team-insight.md`、`command_log.json` | + +## 关键设计 + +- **与主仓库技术栈一致**:Go 实现,复用根 `go.mod`,可被主仓库 CI 直接覆盖(`go build`/`golangci-lint`/`go test`)。 +- **零第三方依赖**:可视化用标准库拼装内联 SVG 生成自包含 HTML,评审在任何环境用浏览器即可查看。 +- **可复现**:`--no-fetch --data-root` 可基于已归档数据离线重跑,产出一致;`command_log.json` 记录全部命令。 +- **健壮性**:网络命令对瞬时 TLS 超时自动重试(间隔 3s,最多 2 次)。 +- **数据治理**:对持有大量镜像项目的平台/管理员账号(`user +info.mirror_projects_count`),报告自动标注"角色/学科分布偏泛",避免误读。 +- **只读安全**:全流程不写任何远端数据。 + +## 输入 / 输出契约 + +- 输入:`--members`(逗号分隔)/ `--config` / `--keyword` 三选一。 +- 每位成员数据目录需含 `develop/major/role/activity/contribution/user.json`(采集阶段自动生成)。 +- 输出目录固定产出 `team-report.html` / `team-insight.md` / `command_log.json` / `data/`。 + +## 代码结构 + +``` +scripts/ +├── insight.go # main:flag 解析、成员解析、采集编排、命令日志 +├── render.go # 聚合渲染:Markdown 报告 + HTML/SVG(雷达图、协作网络图) +└── insight_test.go # 单元测试(解析、聚合、渲染,全部离线) +``` diff --git a/examples/workflows/research-team-insight/docs/assets/architecture.svg b/examples/workflows/research-team-insight/docs/assets/architecture.svg new file mode 100644 index 0000000..968756c --- /dev/null +++ b/examples/workflows/research-team-insight/docs/assets/architecture.svg @@ -0,0 +1,56 @@ + + + 科研团队洞察工作流 · research-team-insight + + + + + + + + + + ① 成员发现 + ② 画像采集 + ③ 聚合分析 + ④ 可视化 + ⑤ 归档 + + + search +users + / 成员名单 + profile +ability + +major/+role + +activity/+contribution + 能力矩阵 + 学科/角色 + HTML 雷达图 + 协作网络图 + insight.md + command_log + + + + + + + + + + + + + + + + GitLink 平台统计接口(只读) + /api/users/{login}/statistics/* · /headmaps · user +info + + + + Go 渲染器(内联 SVG) + 标准库 · 自包含 HTML + + + 瞬时失败自动重试 · 镜像聚合账号自动标注 · 全程只读 + diff --git a/examples/workflows/research-team-insight/docs/runbook.md b/examples/workflows/research-team-insight/docs/runbook.md new file mode 100644 index 0000000..286294a --- /dev/null +++ b/examples/workflows/research-team-insight/docs/runbook.md @@ -0,0 +1,70 @@ +# 运行手册(Runbook) + +## 1. 前置条件 + +```bash +# 安装并登录 gitlink-cli +npm install -g @gitlink-ai/cli +gitlink-cli auth login # 或 export GITLINK_TOKEN=... + +# Go 1.21+(复用主仓库 go.mod,无需额外依赖) +go version +``` + +## 2. 最短复现路径(离线,使用已归档数据) + +无需网络,基于仓库内固定示例数据复跑,产物应与 `examples/demo_outputs/` 一致: + +```bash +cd examples/workflows/research-team-insight +go run ./scripts --config config/team.example.json \ + --out ./_repro --no-fetch --data-root ./examples/demo_outputs/data +# 打开 ./_repro/team-report.html 查看 +``` + +## 3. 真实运行(在线采集) + +```bash +cd examples/workflows/research-team-insight + +# Linux / macOS +go run ./scripts --members "yystopf,jiangtx,wangyue111" \ + --team-name "gitlink-cli 贡献组" --out ./out + +# Windows(gitlink-cli 为 .CMD 包装) +go run ./scripts --members "yystopf,jiangtx,wangyue111" --out ./out ` + --cli-bin "C:\nvm4w\nodejs\gitlink-cli.CMD" +# 或直接用一键脚本: +.\scripts\run_demo.ps1 +``` + +按方向自动发现成员: + +```bash +go run ./scripts --keyword "联邦学习" --max 5 --out ./out +``` + +## 4. 真实验证记录 + +- 验证对象:`@yystopf`(何慧·国防科技大学)、`@jiangtx`、`@wangyue111` +- 运行结果:退出码 0;采集 3 人 × 6 接口 + 1 次渲染 = **19 条命令**(见 `examples/demo_outputs/command_log.json`) +- 产物: + - `team-report.html`(能力雷达叠加 + 能力矩阵 + 协作网络图) + - `team-insight.md`(识别团队最强项=贡献度、短板=影响力;标注 @yystopf 为镜像聚合账号、分布偏泛) + - `command_log.json` + +## 5. 测试 + +```bash +# 在主仓库根目录 +go test ./examples/workflows/research-team-insight/scripts/ +``` + +## 6. 常见问题 + +| 现象 | 处理 | +|------|------| +| Windows 上 `exec: "gitlink-cli": file not found` | `gitlink-cli` 为 npm `.CMD` 包装;用 `--cli-bin ...\gitlink-cli.CMD` 或设 `GITLINK_CLI_BIN`。程序对 `.cmd/.bat` 会自动用 `cmd /c` 包装 | +| `TLS handshake timeout` | 瞬时网络问题,自动重试 2 次(间隔 3s);仍失败会记录到 `command_log.json` 并退出非零 | +| 某成员数据稀疏 / 学科为空 | 报告中如实标注,不臆造 | +| 某账号项目/学科数异常巨大 | 已结合 `user +info.mirror_projects_count` 自动标注"含镜像聚合,分布偏泛" | diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/command_log.json b/examples/workflows/research-team-insight/examples/demo_outputs/command_log.json new file mode 100644 index 0000000..24cee7d --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/command_log.json @@ -0,0 +1,230 @@ +[ + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/yystopf/statistics/develop", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:17:23+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/yystopf/statistics/major", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:17:29+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/yystopf/statistics/role", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:17:38+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/yystopf/statistics/activity", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:17:41+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/yystopf/headmaps", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:17:46+08:00" + }, + { + "command": [ + "gitlink-cli", + "user", + "+info", + "--login", + "yystopf", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:17:55+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/jiangtx/statistics/develop", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:17:58+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/jiangtx/statistics/major", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:02+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/jiangtx/statistics/role", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:07+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/jiangtx/statistics/activity", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:15+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/jiangtx/headmaps", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:24+08:00" + }, + { + "command": [ + "gitlink-cli", + "user", + "+info", + "--login", + "jiangtx", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:29+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/wangyue111/statistics/develop", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:32+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/wangyue111/statistics/major", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:34+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/wangyue111/statistics/role", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:37+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/wangyue111/statistics/activity", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:42+08:00" + }, + { + "command": [ + "gitlink-cli", + "api", + "GET", + "/users/wangyue111/headmaps", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:46+08:00" + }, + { + "command": [ + "gitlink-cli", + "user", + "+info", + "--login", + "wangyue111", + "--format", + "json" + ], + "status": "ok", + "time": "2026-06-14T23:18:49+08:00" + }, + { + "command": [ + "insight", + "render", + "team-report.html" + ], + "status": "ok", + "time": "2026-06-14T23:18:49+08:00" + } +] \ No newline at end of file diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/activity.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/activity.json new file mode 100644 index 0000000..be73255 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/activity.json @@ -0,0 +1,45 @@ +{ + "ok": true, + "data": { + "commits_count": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "dates": [ + "2026.06.07", + "2026.06.08", + "2026.06.09", + "2026.06.10", + "2026.06.11", + "2026.06.12", + "2026.06.13", + "2026.06.14" + ], + "issues_count": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "pull_requests_count": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/contribution.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/contribution.json new file mode 100644 index 0000000..322e886 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/contribution.json @@ -0,0 +1,32 @@ +{ + "ok": true, + "data": { + "headmaps": [ + { + "contributions": 9, + "date": "2026-05-13" + }, + { + "contributions": 1, + "date": "2026-06-01" + }, + { + "contributions": 11, + "date": "2026-06-02" + }, + { + "contributions": 21, + "date": "2026-06-03" + }, + { + "contributions": 17, + "date": "2026-06-04" + }, + { + "contributions": 2, + "date": "2026-06-05" + } + ], + "total_contributions": 61 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/develop.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/develop.json new file mode 100644 index 0000000..5fcb4c3 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/develop.json @@ -0,0 +1,25 @@ +{ + "ok": true, + "data": { + "platform": { + "activity": 99, + "contribution": 99, + "experience": 99, + "influence": 99, + "language": 98 + }, + "user": { + "activity": 60, + "contribution": 72, + "each_language_score": { + "Go": 71 + }, + "experience": 71, + "influence": 60, + "language": 66, + "languages_percent": { + "Go": 1 + } + } + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/major.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/major.json new file mode 100644 index 0000000..2287a2b --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/major.json @@ -0,0 +1,8 @@ +{ + "ok": true, + "data": { + "categories": [ + "其它" + ] + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/role.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/role.json new file mode 100644 index 0000000..77310a3 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/role.json @@ -0,0 +1,24 @@ +{ + "ok": true, + "data": { + "role": { + "developer": { + "count": 0, + "percent": 0 + }, + "manager": { + "count": 2, + "percent": 0.5 + }, + "owner": { + "count": 2, + "percent": 0.5 + }, + "reporter": { + "count": 0, + "percent": 0 + } + }, + "total_projects_count": 4 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/user.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/user.json new file mode 100644 index 0000000..078a9a4 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/jiangtx/user.json @@ -0,0 +1,36 @@ +{ + "ok": true, + "data": { + "admin": false, + "city": null, + "common_projects_count": 4, + "created_time": "2026-04-28 11:46", + "custom_department": null, + "description": null, + "email": null, + "gender": 1, + "grade": 0, + "image_url": "system/lets/letter_avatars/2/J/67_157_94/120.png", + "is_watch": false, + "login": "jiangtx", + "mirror_projects_count": 0, + "name": "jiangtx", + "province": null, + "real_name": "jiangtx", + "show_super_description": true, + "super_description": null, + "sync_mirror_projects_count": 0, + "undo_events": 0, + "undo_join_projects": 0, + "undo_messages": 0, + "undo_transfer_projects": 0, + "user_composes_count": 0, + "user_id": 148911, + "user_identity": "专业人士", + "user_org_count": 0, + "user_projects_count": 4, + "username": "jiangtx", + "watched_count": 0, + "watching_count": 0 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/activity.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/activity.json new file mode 100644 index 0000000..11126be --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/activity.json @@ -0,0 +1,45 @@ +{ + "ok": true, + "data": { + "commits_count": [ + 0, + 22, + 46, + 48, + 29, + 28, + 8, + 13 + ], + "dates": [ + "2026.06.07", + "2026.06.08", + "2026.06.09", + "2026.06.10", + "2026.06.11", + "2026.06.12", + "2026.06.13", + "2026.06.14" + ], + "issues_count": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "pull_requests_count": [ + 0, + 2, + 15, + 11, + 7, + 4, + 2, + 2 + ] + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/contribution.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/contribution.json new file mode 100644 index 0000000..d370bad --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/contribution.json @@ -0,0 +1,100 @@ +{ + "ok": true, + "data": { + "headmaps": [ + { + "contributions": 7, + "date": "2026-05-11" + }, + { + "contributions": 2, + "date": "2026-05-12" + }, + { + "contributions": 12, + "date": "2026-05-13" + }, + { + "contributions": 5, + "date": "2026-05-14" + }, + { + "contributions": 161, + "date": "2026-05-17" + }, + { + "contributions": 3, + "date": "2026-05-19" + }, + { + "contributions": 9, + "date": "2026-05-20" + }, + { + "contributions": 6, + "date": "2026-05-21" + }, + { + "contributions": 31, + "date": "2026-05-24" + }, + { + "contributions": 18, + "date": "2026-05-25" + }, + { + "contributions": 35, + "date": "2026-05-27" + }, + { + "contributions": 21, + "date": "2026-05-28" + }, + { + "contributions": 9, + "date": "2026-06-02" + }, + { + "contributions": 29, + "date": "2026-06-03" + }, + { + "contributions": 20, + "date": "2026-06-04" + }, + { + "contributions": 6, + "date": "2026-06-05" + }, + { + "contributions": 22, + "date": "2026-06-08" + }, + { + "contributions": 46, + "date": "2026-06-09" + }, + { + "contributions": 48, + "date": "2026-06-10" + }, + { + "contributions": 29, + "date": "2026-06-11" + }, + { + "contributions": 28, + "date": "2026-06-12" + }, + { + "contributions": 8, + "date": "2026-06-13" + }, + { + "contributions": 14, + "date": "2026-06-14" + } + ], + "total_contributions": 569 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/develop.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/develop.json new file mode 100644 index 0000000..2af1176 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/develop.json @@ -0,0 +1,31 @@ +{ + "ok": true, + "data": { + "platform": { + "activity": 99, + "contribution": 99, + "experience": 99, + "influence": 99, + "language": 98 + }, + "user": { + "activity": 62, + "contribution": 92, + "each_language_score": { + "C++": 75, + "Cpp": 66, + "Go": 66, + "Python": 66 + }, + "experience": 81, + "influence": 60, + "language": 77, + "languages_percent": { + "C++": 0.5, + "Cpp": 0.17, + "Go": 0.17, + "Python": 0.17 + } + } + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/major.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/major.json new file mode 100644 index 0000000..07f74c9 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/major.json @@ -0,0 +1,6 @@ +{ + "ok": true, + "data": { + "categories": [] + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/role.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/role.json new file mode 100644 index 0000000..2281637 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/role.json @@ -0,0 +1,24 @@ +{ + "ok": true, + "data": { + "role": { + "developer": { + "count": 0, + "percent": 0 + }, + "manager": { + "count": 0, + "percent": 0 + }, + "owner": { + "count": 6, + "percent": 1 + }, + "reporter": { + "count": 0, + "percent": 0 + } + }, + "total_projects_count": 6 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/user.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/user.json new file mode 100644 index 0000000..ae1d294 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/wangyue111/user.json @@ -0,0 +1,36 @@ +{ + "ok": true, + "data": { + "admin": false, + "city": null, + "common_projects_count": 6, + "created_time": "2026-04-19 15:38", + "custom_department": null, + "description": null, + "email": null, + "gender": null, + "grade": 0, + "image_url": "system/lets/letter_avatars/2/W/43_254_70/120.png", + "is_watch": false, + "login": "wangyue111", + "mirror_projects_count": 0, + "name": "wangyue111", + "province": null, + "real_name": "wangyue111", + "show_super_description": false, + "super_description": null, + "sync_mirror_projects_count": 0, + "undo_events": 0, + "undo_join_projects": 0, + "undo_messages": 0, + "undo_transfer_projects": 0, + "user_composes_count": 0, + "user_id": 148193, + "user_identity": "专业人士", + "user_org_count": 0, + "user_projects_count": 6, + "username": "wangyue111", + "watched_count": 0, + "watching_count": 0 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/activity.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/activity.json new file mode 100644 index 0000000..4935d83 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/activity.json @@ -0,0 +1,45 @@ +{ + "ok": true, + "data": { + "commits_count": [ + 0, + 1, + 0, + 5, + 0, + 2, + 0, + 0 + ], + "dates": [ + "2026.06.07", + "2026.06.08", + "2026.06.09", + "2026.06.10", + "2026.06.11", + "2026.06.12", + "2026.06.13", + "2026.06.14" + ], + "issues_count": [ + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0 + ], + "pull_requests_count": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/contribution.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/contribution.json new file mode 100644 index 0000000..d863966 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/contribution.json @@ -0,0 +1,544 @@ +{ + "ok": true, + "data": { + "headmaps": [ + { + "contributions": 80, + "date": "2025-06-16" + }, + { + "contributions": 1, + "date": "2025-06-17" + }, + { + "contributions": 28, + "date": "2025-06-18" + }, + { + "contributions": 9, + "date": "2025-06-19" + }, + { + "contributions": 7, + "date": "2025-06-20" + }, + { + "contributions": 2, + "date": "2025-06-21" + }, + { + "contributions": 3, + "date": "2025-06-23" + }, + { + "contributions": 16, + "date": "2025-06-24" + }, + { + "contributions": 12, + "date": "2025-06-25" + }, + { + "contributions": 3, + "date": "2025-06-26" + }, + { + "contributions": 2, + "date": "2025-06-27" + }, + { + "contributions": 1, + "date": "2025-06-30" + }, + { + "contributions": 5, + "date": "2025-07-01" + }, + { + "contributions": 3, + "date": "2025-07-02" + }, + { + "contributions": 2, + "date": "2025-07-03" + }, + { + "contributions": 7, + "date": "2025-07-09" + }, + { + "contributions": 7, + "date": "2025-07-10" + }, + { + "contributions": 7, + "date": "2025-07-11" + }, + { + "contributions": 6, + "date": "2025-07-14" + }, + { + "contributions": 8, + "date": "2025-07-15" + }, + { + "contributions": 1, + "date": "2025-07-16" + }, + { + "contributions": 3, + "date": "2025-07-17" + }, + { + "contributions": 7, + "date": "2025-07-18" + }, + { + "contributions": 1, + "date": "2025-07-20" + }, + { + "contributions": 3, + "date": "2025-07-21" + }, + { + "contributions": 3, + "date": "2025-07-22" + }, + { + "contributions": 15, + "date": "2025-07-24" + }, + { + "contributions": 11, + "date": "2025-07-25" + }, + { + "contributions": 11, + "date": "2025-07-28" + }, + { + "contributions": 3, + "date": "2025-07-29" + }, + { + "contributions": 11, + "date": "2025-07-30" + }, + { + "contributions": 8, + "date": "2025-07-31" + }, + { + "contributions": 10, + "date": "2025-08-01" + }, + { + "contributions": 1, + "date": "2025-08-11" + }, + { + "contributions": 1, + "date": "2025-08-22" + }, + { + "contributions": 8, + "date": "2025-08-25" + }, + { + "contributions": 4, + "date": "2025-08-27" + }, + { + "contributions": 3, + "date": "2025-08-28" + }, + { + "contributions": 3, + "date": "2025-08-29" + }, + { + "contributions": 2, + "date": "2025-09-01" + }, + { + "contributions": 2, + "date": "2025-09-02" + }, + { + "contributions": 3, + "date": "2025-09-03" + }, + { + "contributions": 3, + "date": "2025-09-04" + }, + { + "contributions": 3, + "date": "2025-09-08" + }, + { + "contributions": 7, + "date": "2025-09-10" + }, + { + "contributions": 3, + "date": "2025-09-12" + }, + { + "contributions": 5, + "date": "2025-09-19" + }, + { + "contributions": 2, + "date": "2025-09-24" + }, + { + "contributions": 2, + "date": "2025-09-26" + }, + { + "contributions": 1, + "date": "2025-09-29" + }, + { + "contributions": 4, + "date": "2025-10-13" + }, + { + "contributions": 8, + "date": "2025-10-15" + }, + { + "contributions": 3, + "date": "2025-10-16" + }, + { + "contributions": 8, + "date": "2025-10-17" + }, + { + "contributions": 6, + "date": "2025-10-22" + }, + { + "contributions": 3, + "date": "2025-10-24" + }, + { + "contributions": 4, + "date": "2025-10-27" + }, + { + "contributions": 4, + "date": "2025-10-28" + }, + { + "contributions": 2, + "date": "2025-10-30" + }, + { + "contributions": 3, + "date": "2025-10-31" + }, + { + "contributions": 9, + "date": "2025-11-04" + }, + { + "contributions": 11, + "date": "2025-11-05" + }, + { + "contributions": 4, + "date": "2025-11-06" + }, + { + "contributions": 10, + "date": "2025-11-07" + }, + { + "contributions": 5, + "date": "2025-11-10" + }, + { + "contributions": 2, + "date": "2025-11-11" + }, + { + "contributions": 1, + "date": "2025-11-13" + }, + { + "contributions": 4, + "date": "2025-11-17" + }, + { + "contributions": 1, + "date": "2025-11-18" + }, + { + "contributions": 1, + "date": "2025-11-20" + }, + { + "contributions": 1, + "date": "2025-11-24" + }, + { + "contributions": 2, + "date": "2025-11-27" + }, + { + "contributions": 6, + "date": "2025-12-01" + }, + { + "contributions": 1, + "date": "2025-12-04" + }, + { + "contributions": 5, + "date": "2025-12-09" + }, + { + "contributions": 1, + "date": "2025-12-11" + }, + { + "contributions": 1, + "date": "2025-12-25" + }, + { + "contributions": 6, + "date": "2025-12-26" + }, + { + "contributions": 2, + "date": "2025-12-27" + }, + { + "contributions": 1, + "date": "2025-12-29" + }, + { + "contributions": 11, + "date": "2025-12-31" + }, + { + "contributions": 1, + "date": "2026-01-04" + }, + { + "contributions": 3, + "date": "2026-01-05" + }, + { + "contributions": 4, + "date": "2026-01-12" + }, + { + "contributions": 2, + "date": "2026-01-13" + }, + { + "contributions": 2, + "date": "2026-01-14" + }, + { + "contributions": 5, + "date": "2026-01-15" + }, + { + "contributions": 1, + "date": "2026-01-16" + }, + { + "contributions": 1, + "date": "2026-01-18" + }, + { + "contributions": 2, + "date": "2026-01-19" + }, + { + "contributions": 2, + "date": "2026-01-20" + }, + { + "contributions": 17, + "date": "2026-01-22" + }, + { + "contributions": 3, + "date": "2026-01-23" + }, + { + "contributions": 13, + "date": "2026-01-28" + }, + { + "contributions": 5, + "date": "2026-01-29" + }, + { + "contributions": 3, + "date": "2026-02-04" + }, + { + "contributions": 3, + "date": "2026-02-27" + }, + { + "contributions": 5, + "date": "2026-02-28" + }, + { + "contributions": 6, + "date": "2026-03-11" + }, + { + "contributions": 1, + "date": "2026-03-13" + }, + { + "contributions": 2, + "date": "2026-03-20" + }, + { + "contributions": 9, + "date": "2026-03-23" + }, + { + "contributions": 8, + "date": "2026-03-24" + }, + { + "contributions": 7, + "date": "2026-03-25" + }, + { + "contributions": 3, + "date": "2026-03-26" + }, + { + "contributions": 2, + "date": "2026-03-27" + }, + { + "contributions": 8, + "date": "2026-03-30" + }, + { + "contributions": 6, + "date": "2026-03-31" + }, + { + "contributions": 2, + "date": "2026-04-01" + }, + { + "contributions": 3, + "date": "2026-04-02" + }, + { + "contributions": 5, + "date": "2026-04-03" + }, + { + "contributions": 5, + "date": "2026-04-07" + }, + { + "contributions": 3, + "date": "2026-04-08" + }, + { + "contributions": 13, + "date": "2026-04-09" + }, + { + "contributions": 1, + "date": "2026-04-10" + }, + { + "contributions": 4, + "date": "2026-04-13" + }, + { + "contributions": 9, + "date": "2026-04-22" + }, + { + "contributions": 2, + "date": "2026-04-24" + }, + { + "contributions": 3, + "date": "2026-04-27" + }, + { + "contributions": 2, + "date": "2026-04-28" + }, + { + "contributions": 1, + "date": "2026-04-29" + }, + { + "contributions": 8, + "date": "2026-05-07" + }, + { + "contributions": 2, + "date": "2026-05-08" + }, + { + "contributions": 4, + "date": "2026-05-09" + }, + { + "contributions": 1, + "date": "2026-05-12" + }, + { + "contributions": 1, + "date": "2026-05-15" + }, + { + "contributions": 1, + "date": "2026-05-22" + }, + { + "contributions": 47, + "date": "2026-05-27" + }, + { + "contributions": 3, + "date": "2026-05-29" + }, + { + "contributions": 1, + "date": "2026-06-02" + }, + { + "contributions": 5, + "date": "2026-06-05" + }, + { + "contributions": 1, + "date": "2026-06-08" + }, + { + "contributions": 5, + "date": "2026-06-10" + }, + { + "contributions": 2, + "date": "2026-06-12" + } + ], + "total_contributions": 737 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/develop.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/develop.json new file mode 100644 index 0000000..237878a --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/develop.json @@ -0,0 +1,53 @@ +{ + "ok": true, + "data": { + "platform": { + "activity": 99, + "contribution": 99, + "experience": 99, + "influence": 99, + "language": 98 + }, + "user": { + "activity": 94, + "contribution": 97, + "each_language_score": { + "C": 75, + "C++": 71, + "CSS": 66, + "Go": 87, + "HTML": 81, + "JSX": 66, + "Java": 83, + "JavaScript": 80, + "Markdown": 66, + "Python": 80, + "Ruby": 75, + "Rust": 66, + "Shell": 80, + "Tsql": 66, + "Typescript": 75 + }, + "experience": 97, + "influence": 79, + "language": 90, + "languages_percent": { + "C": 0.05, + "C++": 0.04, + "CSS": 0.02, + "Go": 0.2, + "HTML": 0.11, + "JSX": 0.02, + "Java": 0.13, + "JavaScript": 0.09, + "Markdown": 0.02, + "Python": 0.09, + "Ruby": 0.05, + "Rust": 0.02, + "Shell": 0.09, + "Tsql": 0.02, + "Typescript": 0.05 + } + } + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/major.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/major.json new file mode 100644 index 0000000..1325f57 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/major.json @@ -0,0 +1,29 @@ +{ + "ok": true, + "data": { + "categories": [ + "插件和扩展", + "程序开发", + "人工智能", + "其它", + "操作系统", + "大数据", + "DevOps/运维/网管", + "Web应用开发", + "物联网", + "应用工具", + "开发工具", + "游戏/娱乐", + "云计算和大数据", + "数据库", + "企业应用", + "数据存储", + "大气海洋环境", + "天文地球物理", + "工业设计制造", + "新能源新材料", + "生物医药健康", + "泛化工具" + ] + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/role.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/role.json new file mode 100644 index 0000000..5652ef3 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/role.json @@ -0,0 +1,24 @@ +{ + "ok": true, + "data": { + "role": { + "developer": { + "count": 20, + "percent": 0 + }, + "manager": { + "count": 86, + "percent": 0 + }, + "owner": { + "count": 20719, + "percent": 0.99 + }, + "reporter": { + "count": 1, + "percent": 0 + } + }, + "total_projects_count": 20826 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/user.json b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/user.json new file mode 100644 index 0000000..6ccaab9 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/data/yystopf/user.json @@ -0,0 +1,36 @@ +{ + "ok": true, + "data": { + "admin": true, + "city": "北京", + "common_projects_count": 180, + "created_time": "2020-11-02 10:13", + "custom_department": "国防科技大学", + "description": "·", + "email": "yystopf@163.com", + "gender": 1, + "grade": 0, + "image_url": "images/avatars/User/85890?t=1742398112", + "is_watch": false, + "login": "yystopf", + "mirror_projects_count": 11453, + "name": "何慧", + "province": "北京", + "real_name": "何慧", + "show_super_description": true, + "super_description": null, + "sync_mirror_projects_count": 9126, + "undo_events": 0, + "undo_join_projects": 0, + "undo_messages": 0, + "undo_transfer_projects": 0, + "user_composes_count": 0, + "user_id": 85890, + "user_identity": "教授", + "user_org_count": 10, + "user_projects_count": 20759, + "username": "何慧", + "watched_count": 19, + "watching_count": 4 + } +} diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/team-insight.md b/examples/workflows/research-team-insight/examples/demo_outputs/team-insight.md new file mode 100644 index 0000000..3800be7 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/team-insight.md @@ -0,0 +1,41 @@ +# 🔬 科研团队洞察报告:gitlink-cli 贡献组 + +> 成员 3 人 | 数据来源:gitlink-cli(只读) + +## 一、能力矩阵(0–100) + +| 成员 | 影响力 | 贡献度 | 活跃度 | 项目经验 | 语言能力 | +|------|------|------|------|------|------| +| 何慧(@yystopf) | 79 | 97 | 94 | 97 | 90 | +| jiangtx(@jiangtx) | 60 | 72 | 60 | 71 | 66 | +| wangyue111(@wangyue111) | 60 | 92 | 62 | 81 | 77 | +| **团队均值** | **66** | **87** | **72** | **83** | **77** | + +- 团队最强项:**贡献度**(87);短板:**影响力**(66,良好) + +## 二、学科方向覆盖 + +- 覆盖方向总数:**22** +- 共有方向(团队共识):其它 ×2 +- 独有方向(个人特长):DevOps/运维/网管, Web应用开发, 云计算和大数据, 人工智能, 企业应用, 大数据, 大气海洋环境, 天文地球物理, 工业设计制造, 应用工具, 开发工具, 插件和扩展, 操作系统, 数据存储, 数据库 + +## 三、技术栈覆盖 + +- 团队主力语言:Go, C++, Python, Cpp, Java, HTML, JavaScript, Shell + +## 四、角色结构 + +- 何慧:参与 20826 个项目(Owner 20719 / Developer 20) ⚠️ 含大量镜像,分布偏泛 +- jiangtx:参与 4 个项目(Owner 2 / Developer 0) +- wangyue111:参与 6 个项目(Owner 6 / Developer 0) + +## 五、协作与分工建议 + +- **核心建议**:何慧(@yystopf)综合能力最强,适合作为方向负责人。 +- **团队聚焦**:在共有方向「其它」上成员重叠度最高,可作为协作主线。 +- **互补利用**:DevOps/运维/网管, Web应用开发, 云计算和大数据, 人工智能, 企业应用 等为个人独有方向,可作为跨学科延展点。 +- **能力补强**:团队短板为「影响力」,建议引入该维度更强的协作者或专项提升。 + +--- + +*本报告由 research-team-insight 工作流自动生成,链路:search/profile → 聚合 → 可视化 → 归档。配套可视化见同目录 `team-report.html`。* diff --git a/examples/workflows/research-team-insight/examples/demo_outputs/team-report.html b/examples/workflows/research-team-insight/examples/demo_outputs/team-report.html new file mode 100644 index 0000000..ec085b6 --- /dev/null +++ b/examples/workflows/research-team-insight/examples/demo_outputs/team-report.html @@ -0,0 +1,22 @@ +课题组科研画像 - gitlink-cli 贡献组
+

🔬 课题组科研画像:gitlink-cli 贡献组

+
成员 3 人 · 数据来源 gitlink-cli(只读)
+
+

开发能力雷达

影响力贡献度活跃度项目经验语言能力
何慧  jiangtx  wangyue111  
+

能力矩阵(0–100)

成员影响力贡献度活跃度项目经验语言能力
何慧 @yystopf7997949790
jiangtx @jiangtx6072607166
wangyue111 @wangyue1116092628177
团队均值6687728377
+
+
+

共有研究方向

其它 ×2
+

独有研究方向

DevOps/运维/网管Web应用开发云计算和大数据人工智能企业应用大数据大气海洋环境天文地球物理工业设计制造应用工具开发工具插件和扩展操作系统数据存储数据库新能源新材料泛化工具游戏/娱乐物联网生物医药健康程序开发
+
+

协作网络图(连线=共同方向数)

yystopf ↔ jiangtx: 1 个共同方向何慧jianwang
+

由 research-team-insight 工作流生成 · gitlink-cli profile(只读)

\ No newline at end of file diff --git a/examples/workflows/research-team-insight/scripts/insight.go b/examples/workflows/research-team-insight/scripts/insight.go new file mode 100644 index 0000000..37af4ec --- /dev/null +++ b/examples/workflows/research-team-insight/scripts/insight.go @@ -0,0 +1,536 @@ +// Command insight is an end-to-end research-team insight workflow (subtopic 3). +// +// It composes existing gitlink-cli commands to, for a set of members (or a +// research keyword), collect each member's GitLink platform statistics, aggregate +// them, and emit a team insight report (Markdown), a self-contained visual report +// (HTML with inline SVG: ability radar + collaboration network) and a command log. +// +// Chain: [search +users] -> profile statistics x N -> aggregate -> visualize -> archive. +// All operations are read-only. +package main + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "time" +) + +// ---------- data model ---------- + +type envelope struct { + Data json.RawMessage `json:"data"` +} + +type abilityScores struct { + Influence int `json:"influence"` + Contribution int `json:"contribution"` + Activity int `json:"activity"` + Experience int `json:"experience"` + Language int `json:"language"` +} + +func (a abilityScores) get(key string) int { + switch key { + case "influence": + return a.Influence + case "contribution": + return a.Contribution + case "activity": + return a.Activity + case "experience": + return a.Experience + case "language": + return a.Language + } + return 0 +} + +type developData struct { + Platform abilityScores `json:"platform"` + User struct { + abilityScores + LanguagesPercent map[string]float64 `json:"languages_percent"` + EachLanguageScore map[string]int `json:"each_language_score"` + } `json:"user"` +} + +type majorData struct { + Categories []string `json:"categories"` +} + +type roleEntry struct { + Count int `json:"count"` + Percent float64 `json:"percent"` +} + +type roleData struct { + Role map[string]roleEntry `json:"role"` + TotalProjectsCount int `json:"total_projects_count"` +} + +type activityData struct { + Dates []string `json:"dates"` + CommitsCount []int `json:"commits_count"` + IssuesCount []int `json:"issues_count"` + PullRequestsCount []int `json:"pull_requests_count"` +} + +type headmapEntry struct { + Date string `json:"date"` + Contributions int `json:"contributions"` +} + +type headmapsData struct { + TotalContributions int `json:"total_contributions"` + Headmaps []headmapEntry `json:"headmaps"` +} + +type userInfo struct { + Login string `json:"login"` + Name string `json:"name"` + RealName string `json:"real_name"` + CustomDepartment string `json:"custom_department"` + City string `json:"city"` + Province string `json:"province"` + CreatedTime string `json:"created_time"` + CommonProjectsCount int `json:"common_projects_count"` + MirrorProjectsCount int `json:"mirror_projects_count"` +} + +type subject struct { + Login string + Info userInfo + Dev developData + Major []string + Role roleData + Act activityData + Heat headmapsData +} + +func (s subject) displayName() string { + if s.Info.RealName != "" { + return s.Info.RealName + } + if s.Info.Name != "" { + return s.Info.Name + } + return s.Login +} + +var abilityDims = []struct{ Key, ZH string }{ + {"influence", "影响力"}, + {"contribution", "贡献度"}, + {"activity", "活跃度"}, + {"experience", "项目经验"}, + {"language", "语言能力"}, +} + +// ---------- config / flags ---------- + +type options struct { + Config string + Members string + Keyword string + Max int + TeamName string + Out string + CLIBin string + NoFetch bool + DataRoot string +} + +type teamConfig struct { + TeamName string `json:"team_name"` + Members []string `json:"members"` +} + +// ---------- command execution + logging ---------- + +type commandResult struct { + Command []string `json:"command"` + Status string `json:"status"` + Time string `json:"time"` + Error string `json:"error,omitempty"` +} + +var commandLog []commandResult + +func runCLI(cliBin string, args []string, retries int) (string, error) { + display := append([]string{"gitlink-cli"}, args...) + var stdout, stderr bytes.Buffer + var lastErr error + for attempt := 0; attempt <= retries; attempt++ { + stdout.Reset() + stderr.Reset() + cmd := exec.Command(cliBin, args...) // #nosec G204 - cliBin is operator-provided + low := strings.ToLower(cliBin) + if strings.HasSuffix(low, ".cmd") || strings.HasSuffix(low, ".bat") { + cmd = exec.Command("cmd", append([]string{"/c", cliBin}, args...)...) // #nosec G204 + } + cmd.Stdout = &stdout + cmd.Stderr = &stderr + lastErr = cmd.Run() + if lastErr == nil { + commandLog = append(commandLog, commandResult{Command: display, Status: "ok", Time: now()}) + return stdout.String(), nil + } + if attempt < retries { + time.Sleep(3 * time.Second) + } + } + msg := strings.TrimSpace(stderr.String()) + commandLog = append(commandLog, commandResult{Command: display, Status: "failed", Time: now(), Error: truncate(msg, 300)}) + return "", fmt.Errorf("命令失败 gitlink-cli %s: %v: %s", strings.Join(args, " "), lastErr, msg) +} + +func now() string { return time.Now().Format(time.RFC3339) } + +func truncate(s string, n int) string { + if len(s) > n { + return s[:n] + } + return s +} + +// ---------- fetch + load ---------- + +var statEndpoints = [][2]string{ + {"develop.json", "statistics/develop"}, + {"major.json", "statistics/major"}, + {"role.json", "statistics/role"}, + {"activity.json", "statistics/activity"}, + {"contribution.json", "headmaps"}, +} + +func fetchMember(cliBin, login, dir string) error { + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + for _, ep := range statEndpoints { + out, err := runCLI(cliBin, []string{"api", "GET", fmt.Sprintf("/users/%s/%s", login, ep[1]), "--format", "json"}, 2) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, ep[0]), []byte(out), 0o644); err != nil { + return err + } + } + out, err := runCLI(cliBin, []string{"user", "+info", "--login", login, "--format", "json"}, 2) + if err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "user.json"), []byte(out), 0o644) +} + +func parseInto(path string, v interface{}) error { + raw, err := os.ReadFile(path) // #nosec G304 - path derived from operator inputs + if err != nil { + return err + } + var env envelope + if err := json.Unmarshal(raw, &env); err != nil { + return fmt.Errorf("%s: %w", path, err) + } + payload := env.Data + if len(payload) == 0 { + payload = raw // tolerate already-unwrapped payloads + } + if err := json.Unmarshal(payload, v); err != nil { + return fmt.Errorf("%s: %w", path, err) + } + return nil +} + +func loadSubject(dir string) (subject, error) { + s := subject{Login: filepath.Base(dir)} + if err := parseInto(filepath.Join(dir, "develop.json"), &s.Dev); err != nil { + return s, err + } + var mj majorData + if err := parseInto(filepath.Join(dir, "major.json"), &mj); err == nil { + s.Major = mj.Categories + } + _ = parseInto(filepath.Join(dir, "role.json"), &s.Role) + _ = parseInto(filepath.Join(dir, "activity.json"), &s.Act) + _ = parseInto(filepath.Join(dir, "contribution.json"), &s.Heat) + if err := parseInto(filepath.Join(dir, "user.json"), &s.Info); err == nil && s.Info.Login != "" { + s.Login = s.Info.Login + } + return s, nil +} + +// ---------- aggregation helpers ---------- + +func teamAverages(subs []subject) map[string]int { + avg := map[string]int{} + if len(subs) == 0 { + return avg + } + for _, d := range abilityDims { + sum := 0 + for _, s := range subs { + sum += s.Dev.User.get(d.Key) + } + avg[d.Key] = sum / len(subs) + } + return avg +} + +func grade(v int) string { + switch { + case v >= 90: + return "卓越" + case v >= 75: + return "优秀" + case v >= 60: + return "良好" + case v >= 40: + return "一般" + default: + return "较弱" + } +} + +// disciplineCoverage returns shared (count>1) and unique categories. +func disciplineCoverage(subs []subject) (shared map[string][]string, unique []string) { + all := map[string][]string{} + for _, s := range subs { + for _, c := range s.Major { + all[c] = append(all[c], s.Login) + } + } + shared = map[string][]string{} + for c, who := range all { + if len(who) > 1 { + shared[c] = who + } else { + unique = append(unique, c) + } + } + sort.Strings(unique) + return shared, unique +} + +func topLanguages(subs []subject, n int) []string { + agg := map[string]float64{} + for _, s := range subs { + for lang, pct := range s.Dev.User.LanguagesPercent { + agg[lang] += pct + } + } + type kv struct { + k string + v float64 + } + var list []kv + for k, v := range agg { + list = append(list, kv{k, v}) + } + sort.Slice(list, func(i, j int) bool { return list[i].v > list[j].v }) + var out []string + for i, e := range list { + if i >= n { + break + } + out = append(out, e.k) + } + return out +} + +func activitySum(a activityData) (commits, issues, prs int) { + for _, v := range a.CommitsCount { + commits += v + } + for _, v := range a.IssuesCount { + issues += v + } + for _, v := range a.PullRequestsCount { + prs += v + } + return +} + +func strongest(subs []subject) subject { + best := subs[0] + bestScore := -1 + for _, s := range subs { + total := 0 + for _, d := range abilityDims { + total += s.Dev.User.get(d.Key) + } + if total > bestScore { + bestScore = total + best = s + } + } + return best +} + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "错误:", err) + os.Exit(1) + } +} + +func run() error { + opts := parseFlags() + + members, teamName, err := resolveMembers(opts) + if err != nil { + return err + } + fmt.Printf("成员:%s\n", strings.Join(members, ", ")) + + dataRoot := opts.DataRoot + if dataRoot == "" { + dataRoot = filepath.Join(opts.Out, "data") + } + if err := os.MkdirAll(opts.Out, 0o755); err != nil { + return err + } + + var subs []subject + for i, login := range members { + dir := filepath.Join(dataRoot, login) + if !opts.NoFetch { + fmt.Printf("[2/4] 采集画像 %d/%d:%s\n", i+1, len(members), login) + if err := fetchMember(opts.CLIBin, login, dir); err != nil { + return err + } + } + s, err := loadSubject(dir) + if err != nil { + fmt.Printf(" ⚠️ 跳过 %s:%v\n", login, err) + continue + } + subs = append(subs, s) + } + if len(subs) == 0 { + return errors.New("无可用成员数据") + } + + fmt.Println("[3/4] 生成团队可视化报告与洞察报告…") + html := buildHTML(teamName, subs) + if err := os.WriteFile(filepath.Join(opts.Out, "team-report.html"), []byte(html), 0o644); err != nil { + return err + } + md := buildInsightMarkdown(teamName, subs) + if err := os.WriteFile(filepath.Join(opts.Out, "team-insight.md"), []byte(md), 0o644); err != nil { + return err + } + + fmt.Println("[4/4] 写入命令日志…") + commandLog = append(commandLog, commandResult{Command: []string{"insight", "render", "team-report.html"}, Status: "ok", Time: now()}) + logJSON, err := json.MarshalIndent(commandLog, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(opts.Out, "command_log.json"), logJSON, 0o644); err != nil { + return err + } + + fmt.Printf("\n完成 输出目录:%s\n - team-report.html\n - team-insight.md\n - command_log.json(%d 条)\n", opts.Out, len(commandLog)) + return nil +} + +func parseFlags() options { + var opts options + fs := flag.CommandLine + fs.StringVar(&opts.Config, "config", "", "团队配置 JSON:{\"team_name\":..,\"members\":[..]}") + fs.StringVar(&opts.Members, "members", "", "成员 login,逗号分隔") + fs.StringVar(&opts.Keyword, "keyword", "", "按研究方向发现成员(search +users)") + fs.IntVar(&opts.Max, "max", 5, "--keyword 模式下最多成员数") + fs.StringVar(&opts.TeamName, "team-name", "科研团队", "团队名称") + fs.StringVar(&opts.Out, "out", "./out", "输出目录") + fs.StringVar(&opts.CLIBin, "cli-bin", firstNonEmpty(os.Getenv("GITLINK_CLI_BIN"), "gitlink-cli"), "gitlink-cli 可执行文件路径") + fs.BoolVar(&opts.NoFetch, "no-fetch", false, "离线复现:不调用网络") + fs.StringVar(&opts.DataRoot, "data-root", "", "--no-fetch 时成员数据根目录(默认 /data)") + flag.Parse() + return opts +} + +func firstNonEmpty(vals ...string) string { + for _, v := range vals { + if v != "" { + return v + } + } + return "" +} + +func resolveMembers(opts options) ([]string, string, error) { + teamName := opts.TeamName + var members []string + if opts.Members != "" { + for _, m := range strings.Split(opts.Members, ",") { + if m = strings.TrimSpace(m); m != "" { + members = append(members, m) + } + } + } + if opts.Config != "" { + raw, err := os.ReadFile(opts.Config) // #nosec G304 + if err != nil { + return nil, "", err + } + var cfg teamConfig + if err := json.Unmarshal(raw, &cfg); err != nil { + return nil, "", err + } + if len(members) == 0 { + members = cfg.Members + } + if cfg.TeamName != "" && opts.TeamName == "科研团队" { + teamName = cfg.TeamName + } + } + if len(members) == 0 && opts.Keyword != "" { + fmt.Printf("[1/4] 按方向「%s」发现成员…\n", opts.Keyword) + found, err := discoverMembers(opts.CLIBin, opts.Keyword, opts.Max) + if err != nil { + return nil, "", err + } + members = found + } + if len(members) == 0 { + return nil, "", errors.New("需要 --members、--config 或 --keyword") + } + return members, teamName, nil +} + +func discoverMembers(cliBin, keyword string, limit int) ([]string, error) { + out, err := runCLI(cliBin, []string{"search", "+users", "-k", keyword, "--format", "json"}, 2) + if err != nil { + return nil, err + } + var env envelope + if err := json.Unmarshal([]byte(out), &env); err != nil { + return nil, err + } + var sd struct { + Users []userInfo `json:"users"` + } + _ = json.Unmarshal(env.Data, &sd) + var logins []string + for _, u := range sd.Users { + if u.Login != "" { + logins = append(logins, u.Login) + } + if len(logins) >= limit { + break + } + } + if len(logins) == 0 { + return nil, fmt.Errorf("方向「%s」未发现可用成员", keyword) + } + return logins, nil +} diff --git a/examples/workflows/research-team-insight/scripts/insight_test.go b/examples/workflows/research-team-insight/scripts/insight_test.go new file mode 100644 index 0000000..4056375 --- /dev/null +++ b/examples/workflows/research-team-insight/scripts/insight_test.go @@ -0,0 +1,168 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func writeFixture(t *testing.T, dir, name, body string) { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { + t.Fatal(err) + } +} + +func makeMember(t *testing.T, root, login string, infl, contrib, lang int, cats string, mirror int) string { + dir := filepath.Join(root, login) + writeFixture(t, dir, "develop.json", `{"ok":true,"data":{"platform":{"influence":99,"contribution":99,"activity":99,"experience":99,"language":98},"user":{"influence":`+itoa(infl)+`,"contribution":`+itoa(contrib)+`,"activity":80,"experience":85,"language":`+itoa(lang)+`,"languages_percent":{"Go":0.5,"Python":0.3},"each_language_score":{"Go":88,"Python":80}}}}`) + writeFixture(t, dir, "major.json", `{"ok":true,"data":{"categories":[`+cats+`]}}`) + writeFixture(t, dir, "role.json", `{"ok":true,"data":{"role":{"owner":{"count":5,"percent":0.8},"developer":{"count":2,"percent":0.2},"manager":{"count":0,"percent":0},"reporter":{"count":0,"percent":0}},"total_projects_count":7}}`) + writeFixture(t, dir, "activity.json", `{"ok":true,"data":{"dates":["2026.06.13","2026.06.14"],"commits_count":[1,2],"issues_count":[0,1],"pull_requests_count":[0,0]}}`) + writeFixture(t, dir, "contribution.json", `{"ok":true,"data":{"total_contributions":123,"headmaps":[{"date":"2026-06-14","contributions":5}]}}`) + writeFixture(t, dir, "user.json", `{"ok":true,"data":{"login":"`+login+`","name":"`+login+`","real_name":"研究员`+login+`","custom_department":"某大学","mirror_projects_count":`+itoa(mirror)+`}}`) + return dir +} + +func itoa(n int) string { + if n == 0 { + return "0" + } + neg := n < 0 + if neg { + n = -n + } + var b []byte + for n > 0 { + b = append([]byte{byte('0' + n%10)}, b...) + n /= 10 + } + if neg { + b = append([]byte{'-'}, b...) + } + return string(b) +} + +func TestLoadSubject(t *testing.T) { + root := t.TempDir() + dir := makeMember(t, root, "alice", 70, 90, 66, `"人工智能","大数据"`, 0) + s, err := loadSubject(dir) + if err != nil { + t.Fatalf("loadSubject: %v", err) + } + if s.Login != "alice" { + t.Fatalf("login=%q", s.Login) + } + if s.Dev.User.Contribution != 90 { + t.Fatalf("contribution=%d want 90", s.Dev.User.Contribution) + } + if s.Dev.Platform.Influence != 99 { + t.Fatalf("platform influence=%d want 99", s.Dev.Platform.Influence) + } + if len(s.Major) != 2 || s.Major[0] != "人工智能" { + t.Fatalf("major=%v", s.Major) + } + if s.Role.TotalProjectsCount != 7 { + t.Fatalf("total projects=%d", s.Role.TotalProjectsCount) + } + if s.displayName() != "研究员alice" { + t.Fatalf("displayName=%q", s.displayName()) + } +} + +func TestAbilityGetAndAverages(t *testing.T) { + root := t.TempDir() + a, _ := loadSubject(makeMember(t, root, "a", 60, 80, 60, `"AI"`, 0)) + b, _ := loadSubject(makeMember(t, root, "b", 80, 100, 80, `"AI"`, 0)) + avg := teamAverages([]subject{a, b}) + if avg["influence"] != 70 { + t.Fatalf("avg influence=%d want 70", avg["influence"]) + } + if avg["contribution"] != 90 { + t.Fatalf("avg contribution=%d want 90", avg["contribution"]) + } + if a.Dev.User.get("language") != 60 { + t.Fatalf("get language=%d", a.Dev.User.get("language")) + } +} + +func TestDisciplineCoverage(t *testing.T) { + root := t.TempDir() + a, _ := loadSubject(makeMember(t, root, "a", 60, 60, 60, `"AI","大数据"`, 0)) + b, _ := loadSubject(makeMember(t, root, "b", 60, 60, 60, `"AI","物联网"`, 0)) + shared, unique := disciplineCoverage([]subject{a, b}) + if _, ok := shared["AI"]; !ok { + t.Fatalf("expected AI shared, got %v", shared) + } + found := map[string]bool{} + for _, u := range unique { + found[u] = true + } + if !found["大数据"] || !found["物联网"] { + t.Fatalf("unique=%v", unique) + } +} + +func TestGradeAndActivitySum(t *testing.T) { + cases := map[int]string{95: "卓越", 80: "优秀", 65: "良好", 45: "一般", 10: "较弱"} + for v, want := range cases { + if got := grade(v); got != want { + t.Fatalf("grade(%d)=%s want %s", v, got, want) + } + } + c, i, p := activitySum(activityData{CommitsCount: []int{1, 2}, IssuesCount: []int{0, 3}, PullRequestsCount: []int{1}}) + if c != 3 || i != 3 || p != 1 { + t.Fatalf("activitySum=%d,%d,%d", c, i, p) + } +} + +func TestStrongestAndTopLanguages(t *testing.T) { + root := t.TempDir() + a, _ := loadSubject(makeMember(t, root, "weak", 50, 50, 50, `"AI"`, 0)) + b, _ := loadSubject(makeMember(t, root, "strong", 90, 90, 90, `"AI"`, 0)) + if strongest([]subject{a, b}).Login != "strong" { + t.Fatal("expected strong to be strongest") + } + langs := topLanguages([]subject{a, b}, 5) + if len(langs) == 0 || langs[0] != "Go" { + t.Fatalf("top languages=%v", langs) + } +} + +func TestBuildMarkdownAndHTML(t *testing.T) { + root := t.TempDir() + a, _ := loadSubject(makeMember(t, root, "alice", 70, 95, 66, `"人工智能","大数据"`, 0)) + b, _ := loadSubject(makeMember(t, root, "bob", 60, 72, 77, `"人工智能","物联网"`, 9000)) + subs := []subject{a, b} + + md := buildInsightMarkdown("测试组", subs) + for _, want := range []string{"科研团队洞察报告:测试组", "能力矩阵", "团队均值", "学科方向覆盖", "协作与分工建议", "含大量镜像"} { + if !strings.Contains(md, want) { + t.Fatalf("markdown missing %q", want) + } + } + + htmlOut := buildHTML("测试组", subs) + for _, want := range []string{" 成员 %d 人 | 数据来源:gitlink-cli(只读)\n\n", n) + + b.WriteString("## 一、能力矩阵(0–100)\n\n") + b.WriteString("| 成员 |") + for _, d := range abilityDims { + fmt.Fprintf(&b, " %s |", d.ZH) + } + b.WriteString("\n|------|") + for range abilityDims { + b.WriteString("------|") + } + b.WriteString("\n") + for _, s := range subs { + fmt.Fprintf(&b, "| %s(@%s) |", s.displayName(), s.Login) + for _, d := range abilityDims { + fmt.Fprintf(&b, " %d |", s.Dev.User.get(d.Key)) + } + b.WriteString("\n") + } + b.WriteString("| **团队均值** |") + for _, d := range abilityDims { + fmt.Fprintf(&b, " **%d** |", avg[d.Key]) + } + b.WriteString("\n\n") + + // strongest / weakest dimension + type dimAvg struct { + zh string + v int + } + var das []dimAvg + for _, d := range abilityDims { + das = append(das, dimAvg{d.ZH, avg[d.Key]}) + } + sort.Slice(das, func(i, j int) bool { return das[i].v > das[j].v }) + fmt.Fprintf(&b, "- 团队最强项:**%s**(%d);短板:**%s**(%d,%s)\n\n", + das[0].zh, das[0].v, das[len(das)-1].zh, das[len(das)-1].v, grade(das[len(das)-1].v)) + + // disciplines + shared, unique := disciplineCoverage(subs) + b.WriteString("## 二、学科方向覆盖\n\n") + fmt.Fprintf(&b, "- 覆盖方向总数:**%d**\n", len(shared)+len(unique)) + b.WriteString("- 共有方向(团队共识):" + sharedSummary(shared, 10) + "\n") + b.WriteString("- 独有方向(个人特长):" + joinLimit(unique, 15) + "\n\n") + + // languages + b.WriteString("## 三、技术栈覆盖\n\n") + langs := topLanguages(subs, 8) + b.WriteString("- 团队主力语言:" + emptyOr(strings.Join(langs, ", "), "无") + "\n\n") + + // roles + b.WriteString("## 四、角色结构\n\n") + for _, s := range subs { + owner := s.Role.Role["owner"].Count + dev := s.Role.Role["developer"].Count + note := "" + if s.Info.MirrorProjectsCount > 100 { + note = " ⚠️ 含大量镜像,分布偏泛" + } + fmt.Fprintf(&b, "- %s:参与 %s 个项目(Owner %d / Developer %d)%s\n", + s.displayName(), dashIfZero(s.Role.TotalProjectsCount), owner, dev, note) + } + b.WriteString("\n") + + // suggestions + b.WriteString("## 五、协作与分工建议\n\n") + lead := strongest(subs) + fmt.Fprintf(&b, "- **核心建议**:%s(@%s)综合能力最强,适合作为方向负责人。\n", lead.displayName(), lead.Login) + if top := topShared(shared); top != "" { + fmt.Fprintf(&b, "- **团队聚焦**:在共有方向「%s」上成员重叠度最高,可作为协作主线。\n", top) + } + if len(unique) > 0 { + fmt.Fprintf(&b, "- **互补利用**:%s 等为个人独有方向,可作为跨学科延展点。\n", joinLimit(unique, 5)) + } + fmt.Fprintf(&b, "- **能力补强**:团队短板为「%s」,建议引入该维度更强的协作者或专项提升。\n\n", das[len(das)-1].zh) + + b.WriteString("---\n\n*本报告由 research-team-insight 工作流自动生成,链路:search/profile → 聚合 → 可视化 → 归档。配套可视化见同目录 `team-report.html`。*\n") + return b.String() +} + +// ---------- HTML report ---------- + +func buildHTML(team string, subs []subject) string { + avg := teamAverages(subs) + shared, unique := disciplineCoverage(subs) + + var matrix strings.Builder + matrix.WriteString("成员") + for _, d := range abilityDims { + fmt.Fprintf(&matrix, "%s", d.ZH) + } + matrix.WriteString("") + for i, s := range subs { + color := palette[i%len(palette)] + fmt.Fprintf(&matrix, `%s @%s`, + color, html.EscapeString(s.displayName()), html.EscapeString(s.Login)) + for _, d := range abilityDims { + fmt.Fprintf(&matrix, "%d", s.Dev.User.get(d.Key)) + } + matrix.WriteString("") + } + matrix.WriteString("团队均值") + for _, d := range abilityDims { + fmt.Fprintf(&matrix, "%d", avg[d.Key]) + } + matrix.WriteString("") + + sharedTags := tagsHTML(sharedTagList(shared)) + uniqueTags := tagsHTML(unique) + + body := fmt.Sprintf(` +

🔬 课题组科研画像:%s

+
成员 %d 人 · 数据来源 gitlink-cli(只读)
+
+

开发能力雷达

%s%s
+

能力矩阵(0–100)

%s
+
+
+

共有研究方向

%s
+

独有研究方向

%s
+
+

协作网络图(连线=共同方向数)

%s
+`, + html.EscapeString(team), len(subs), + radarSVG(subs), radarLegend(subs), + matrix.String(), sharedTags, uniqueTags, networkSVG(subs)) + + return page(fmt.Sprintf("课题组科研画像 - %s", team), body) +} + +func page(title, body string) string { + css := `body{font-family:-apple-system,'Segoe UI','Microsoft YaHei',sans-serif;margin:0;background:#f6f8fa;color:#24292f} +.wrap{max-width:980px;margin:0 auto;padding:24px} +h1{font-size:22px;margin:0 0 4px}h2{font-size:16px;margin:18px 0 10px;border-left:4px solid #2f6feb;padding-left:8px} +.sub{color:#57606a;font-size:13px;margin-bottom:16px} +.card{background:#fff;border:1px solid #e6e8eb;border-radius:10px;padding:16px;margin-bottom:16px} +.grid{display:flex;gap:16px;flex-wrap:wrap}.grid>.card{flex:1;min-width:300px} +.tag{display:inline-block;background:#eaf1ff;color:#2f6feb;border-radius:12px;padding:3px 10px;margin:3px;font-size:12px} +.muted{color:#8b949e;font-size:12px}.legend{font-size:12px;color:#57606a;margin-top:8px} +table{border-collapse:collapse;width:100%}td,th{border:1px solid #eaecef;padding:6px 8px;text-align:left;font-size:13px}th{background:#f6f8fa} +.dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px;vertical-align:middle}` + return fmt.Sprintf(``+ + `%s`+ + `
%s

由 research-team-insight 工作流生成 · gitlink-cli profile(只读)

`, + html.EscapeString(title), css, body) +} + +// ---------- SVG ---------- + +func radarSVG(subs []subject) string { + const size = 340.0 + cx, cy := size/2, size/2 + r := size/2 - 50 + n := len(abilityDims) + var sb strings.Builder + fmt.Fprintf(&sb, ``, size, size, size, size) + for k := 1; k <= 4; k++ { + fmt.Fprintf(&sb, ``, cx, cy, r*float64(k)/4) + } + for i, d := range abilityDims { + ang := -math.Pi/2 + 2*math.Pi*float64(i)/float64(n) + x, y := cx+r*math.Cos(ang), cy+r*math.Sin(ang) + fmt.Fprintf(&sb, ``, cx, cy, x, y) + lx, ly := cx+(r+20)*math.Cos(ang), cy+(r+20)*math.Sin(ang) + anchor := "middle" + if math.Cos(ang) > 0.3 { + anchor = "start" + } else if math.Cos(ang) < -0.3 { + anchor = "end" + } + fmt.Fprintf(&sb, `%s`, lx, ly+4, anchor, d.ZH) + } + for i, s := range subs { + color := palette[i%len(palette)] + var pts strings.Builder + for j, d := range abilityDims { + ang := -math.Pi/2 + 2*math.Pi*float64(j)/float64(n) + rr := r * float64(clamp(s.Dev.User.get(d.Key), 0, 100)) / 100 + if j > 0 { + pts.WriteString(" ") + } + fmt.Fprintf(&pts, "%.1f,%.1f", cx+rr*math.Cos(ang), cy+rr*math.Sin(ang)) + } + fmt.Fprintf(&sb, ``, pts.String(), color, color) + } + sb.WriteString("") + return sb.String() +} + +func radarLegend(subs []subject) string { + var sb strings.Builder + sb.WriteString(`
`) + for i, s := range subs { + color := palette[i%len(palette)] + fmt.Fprintf(&sb, `%s  `, color, html.EscapeString(s.displayName())) + } + sb.WriteString("
") + return sb.String() +} + +func networkSVG(subs []subject) string { + n := len(subs) + const size = 420.0 + if n < 2 { + return `

成员不足 2 人,无协作网络。

` + } + cx, cy := size/2, size/2 + r := size/2 - 64 + type pt struct{ x, y float64 } + pos := make([]pt, n) + for i := range subs { + ang := -math.Pi/2 + 2*math.Pi*float64(i)/float64(n) + pos[i] = pt{cx + r*math.Cos(ang), cy + r*math.Sin(ang)} + } + var sb strings.Builder + fmt.Fprintf(&sb, ``, size, size, size, size) + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + shared := sharedCount(subs[i].Major, subs[j].Major) + if shared > 0 { + w := math.Min(6, 1+float64(shared)/3) + fmt.Fprintf(&sb, `%s ↔ %s: %d 个共同方向`, + pos[i].x, pos[i].y, pos[j].x, pos[j].y, w, html.EscapeString(subs[i].Login), html.EscapeString(subs[j].Login), shared) + } + } + } + for i, s := range subs { + color := palette[i%len(palette)] + label := []rune(s.displayName()) + if len(label) > 4 { + label = label[:4] + } + fmt.Fprintf(&sb, `%s`, + pos[i].x, pos[i].y, color, pos[i].x, pos[i].y+4, html.EscapeString(string(label))) + } + sb.WriteString("") + return sb.String() +} + +// ---------- small helpers ---------- + +func clamp(v, lo, hi int) int { + if v < lo { + return lo + } + if v > hi { + return hi + } + return v +} + +func sharedCount(a, b []string) int { + set := map[string]bool{} + for _, x := range a { + set[x] = true + } + c := 0 + for _, y := range b { + if set[y] { + c++ + } + } + return c +} + +func tagsHTML(items []string) string { + if len(items) == 0 { + return `` + } + var sb strings.Builder + for _, it := range items { + fmt.Fprintf(&sb, `%s`, html.EscapeString(it)) + } + return sb.String() +} + +func sharedTagList(shared map[string][]string) []string { + type kv struct { + k string + n int + } + var list []kv + for c, who := range shared { + list = append(list, kv{c, len(who)}) + } + sort.Slice(list, func(i, j int) bool { return list[i].n > list[j].n }) + var out []string + for _, e := range list { + out = append(out, fmt.Sprintf("%s ×%d", e.k, e.n)) + } + return out +} + +func sharedSummary(shared map[string][]string, limit int) string { + list := sharedTagList(shared) + return emptyOr(joinLimit(list, limit), "无") +} + +func topShared(shared map[string][]string) string { + best, bestN := "", 0 + for c, who := range shared { + if len(who) > bestN { + bestN, best = len(who), c + } + } + return best +} + +func joinLimit(items []string, limit int) string { + if len(items) > limit { + items = items[:limit] + } + return strings.Join(items, ", ") +} + +func emptyOr(s, fallback string) string { + if strings.TrimSpace(s) == "" { + return fallback + } + return s +} + +func dashIfZero(n int) string { + if n == 0 { + return "—" + } + return fmt.Sprintf("%d", n) +} diff --git a/examples/workflows/research-team-insight/scripts/run_demo.ps1 b/examples/workflows/research-team-insight/scripts/run_demo.ps1 new file mode 100644 index 0000000..7515004 --- /dev/null +++ b/examples/workflows/research-team-insight/scripts/run_demo.ps1 @@ -0,0 +1,27 @@ +<# +一键复现脚本:运行科研团队洞察工作流并生成示例输出。 +用法: .\scripts\run_demo.ps1 [-Out ] [-NoFetch] + -NoFetch 使用 examples/demo_outputs/data 下已采集数据离线复现 +#> +param( + [string]$Out = "./examples/demo_outputs", + [switch]$NoFetch +) + +$ErrorActionPreference = "Stop" +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location (Join-Path $here "..") + +# 解析 gitlink-cli 可执行文件(Windows 上为 .CMD 包装) +$cliBin = "gitlink-cli" +$src = (Get-Command gitlink-cli -ErrorAction SilentlyContinue).Source +if ($src -and $src.ToLower().EndsWith(".ps1")) { $cliBin = ($src -replace '\.ps1$', '.CMD') } + +$args = @("run", "./scripts", + "--config", "config/team.example.json", + "--out", $Out, + "--cli-bin", $cliBin) +if ($NoFetch) { $args += @("--no-fetch", "--data-root", (Join-Path $Out "data")) } + +Write-Host "运行:go $($args -join ' ')" +& go @args