Merge pull request 'feat(workflows): 新增科研团队洞察端到端工作流(research-team-insight)' (#249) from luwanzhou/gitlink-cli:feat/research-team-insight into master
This commit is contained in:
commit
8af07a94c1
|
|
@ -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/<login>/*.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 定义
|
||||
"画像能力 + 单人/团队报告规范"。
|
||||
- 本工作流(子赛题三)把该能力**串联成可复现的端到端解决方案**,加入用户发现、批量编排、聚合分析
|
||||
与归档,产出团队级可视化与洞察成果。
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"team_name": "gitlink-cli 贡献组",
|
||||
"members": ["yystopf", "jiangtx", "wangyue111"]
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# 架构与流程说明
|
||||
|
||||

|
||||
|
||||
## 设计目标
|
||||
|
||||
把分散的 GitLink 平台统计能力,编排成一个**可复现、可审计、零第三方依赖**的科研团队洞察流水线,
|
||||
输出供科研评审/组队/选题直接使用的可视化与结构化成果。
|
||||
|
||||
## 五个阶段
|
||||
|
||||
| 阶段 | 动作 | 使用的命令 | 产物 |
|
||||
|------|------|-----------|------|
|
||||
| ① 成员发现 | 由方向关键词检索候选,或直接使用给定名单 | `gitlink-cli search +users -k`(可选) | 成员 login 列表 |
|
||||
| ② 画像采集 | 对每位成员拉取 5 类统计 + 基础资料 | `api GET /users/{login}/statistics/{develop,major,role,activity}`、`/headmaps`、`user +info` | `data/<login>/*.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 # 单元测试(解析、聚合、渲染,全部离线)
|
||||
```
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="300" viewBox="0 0 900 300" font-family="-apple-system,Segoe UI,Microsoft YaHei,sans-serif">
|
||||
<rect width="900" height="300" fill="#f6f8fa"/>
|
||||
<text x="450" y="32" text-anchor="middle" font-size="18" fill="#24292f" font-weight="bold">科研团队洞察工作流 · research-team-insight</text>
|
||||
|
||||
<g font-size="13" fill="#fff">
|
||||
<rect x="24" y="70" width="150" height="80" rx="10" fill="#6f42c1"/>
|
||||
<rect x="206" y="70" width="160" height="80" rx="10" fill="#2f6feb"/>
|
||||
<rect x="398" y="70" width="150" height="80" rx="10" fill="#1f883d"/>
|
||||
<rect x="580" y="70" width="150" height="80" rx="10" fill="#bf8700"/>
|
||||
<rect x="762" y="70" width="114" height="80" rx="10" fill="#cf222e"/>
|
||||
</g>
|
||||
<g font-size="13" fill="#fff" text-anchor="middle">
|
||||
<text x="99" y="104">① 成员发现</text>
|
||||
<text x="286" y="104">② 画像采集</text>
|
||||
<text x="473" y="104">③ 聚合分析</text>
|
||||
<text x="655" y="104">④ 可视化</text>
|
||||
<text x="819" y="104">⑤ 归档</text>
|
||||
</g>
|
||||
<g font-size="10.5" fill="#eef" text-anchor="middle">
|
||||
<text x="99" y="126">search +users</text>
|
||||
<text x="99" y="140">/ 成员名单</text>
|
||||
<text x="286" y="124">profile +ability</text>
|
||||
<text x="286" y="137">+major/+role</text>
|
||||
<text x="286" y="150">+activity/+contribution</text>
|
||||
<text x="473" y="126">能力矩阵</text>
|
||||
<text x="473" y="140">学科/角色</text>
|
||||
<text x="655" y="126">HTML 雷达图</text>
|
||||
<text x="655" y="140">协作网络图</text>
|
||||
<text x="819" y="124">insight.md</text>
|
||||
<text x="819" y="138">command_log</text>
|
||||
</g>
|
||||
|
||||
<g stroke="#9aa0a6" stroke-width="2" fill="none" marker-end="url(#a)">
|
||||
<line x1="174" y1="110" x2="204" y2="110"/>
|
||||
<line x1="366" y1="110" x2="396" y2="110"/>
|
||||
<line x1="548" y1="110" x2="578" y2="110"/>
|
||||
<line x1="730" y1="110" x2="760" y2="110"/>
|
||||
</g>
|
||||
<defs>
|
||||
<marker id="a" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||||
<path d="M0,0 L6,3 L0,6 Z" fill="#9aa0a6"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="206" y="190" width="342" height="56" rx="8" fill="#eef1f5" stroke="#d0d7de"/>
|
||||
<text x="377" y="214" text-anchor="middle" font-size="12" fill="#57606a">GitLink 平台统计接口(只读)</text>
|
||||
<text x="377" y="232" text-anchor="middle" font-size="11" fill="#8b949e">/api/users/{login}/statistics/* · /headmaps · user +info</text>
|
||||
<line x1="286" y1="150" x2="286" y2="190" stroke="#c0c4cc" stroke-width="1.5" stroke-dasharray="4 3"/>
|
||||
|
||||
<rect x="580" y="190" width="296" height="56" rx="8" fill="#eef1f5" stroke="#d0d7de"/>
|
||||
<text x="728" y="214" text-anchor="middle" font-size="12" fill="#57606a">Go 渲染器(内联 SVG)</text>
|
||||
<text x="728" y="232" text-anchor="middle" font-size="11" fill="#8b949e">标准库 · 自包含 HTML</text>
|
||||
<line x1="655" y1="150" x2="655" y2="190" stroke="#c0c4cc" stroke-width="1.5" stroke-dasharray="4 3"/>
|
||||
|
||||
<text x="450" y="280" text-anchor="middle" font-size="11" fill="#8b949e">瞬时失败自动重试 · 镜像聚合账号自动标注 · 全程只读</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -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` 自动标注"含镜像聚合,分布偏泛" |
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"categories": [
|
||||
"其它"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"categories": []
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"categories": [
|
||||
"插件和扩展",
|
||||
"程序开发",
|
||||
"人工智能",
|
||||
"其它",
|
||||
"操作系统",
|
||||
"大数据",
|
||||
"DevOps/运维/网管",
|
||||
"Web应用开发",
|
||||
"物联网",
|
||||
"应用工具",
|
||||
"开发工具",
|
||||
"游戏/娱乐",
|
||||
"云计算和大数据",
|
||||
"数据库",
|
||||
"企业应用",
|
||||
"数据存储",
|
||||
"大气海洋环境",
|
||||
"天文地球物理",
|
||||
"工业设计制造",
|
||||
"新能源新材料",
|
||||
"生物医药健康",
|
||||
"泛化工具"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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`。*
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>课题组科研画像 - gitlink-cli 贡献组</title><style>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}</style></head><body><div class="wrap">
|
||||
<h1>🔬 课题组科研画像:gitlink-cli 贡献组</h1>
|
||||
<div class="sub">成员 3 人 · 数据来源 gitlink-cli(只读)</div>
|
||||
<div class="grid">
|
||||
<div class="card"><h2>开发能力雷达</h2><svg viewBox="0 0 340 340" width="340" height="340" xmlns="http://www.w3.org/2000/svg"><circle cx="170" cy="170" r="30.0" fill="none" stroke="#e6e8eb"/><circle cx="170" cy="170" r="60.0" fill="none" stroke="#e6e8eb"/><circle cx="170" cy="170" r="90.0" fill="none" stroke="#e6e8eb"/><circle cx="170" cy="170" r="120.0" fill="none" stroke="#e6e8eb"/><line x1="170" y1="170" x2="170.0" y2="50.0" stroke="#e6e8eb"/><text x="170.0" y="34.0" text-anchor="middle" font-size="12" fill="#444">影响力</text><line x1="170" y1="170" x2="284.1" y2="132.9" stroke="#e6e8eb"/><text x="303.1" y="130.7" text-anchor="start" font-size="12" fill="#444">贡献度</text><line x1="170" y1="170" x2="240.5" y2="267.1" stroke="#e6e8eb"/><text x="252.3" y="287.3" text-anchor="start" font-size="12" fill="#444">活跃度</text><line x1="170" y1="170" x2="99.5" y2="267.1" stroke="#e6e8eb"/><text x="87.7" y="287.3" text-anchor="end" font-size="12" fill="#444">项目经验</text><line x1="170" y1="170" x2="55.9" y2="132.9" stroke="#e6e8eb"/><text x="36.9" y="130.7" text-anchor="end" font-size="12" fill="#444">语言能力</text><polygon points="170.0,75.2 280.7,134.0 236.3,261.3 101.6,264.2 67.3,136.6" fill="#2f6feb22" stroke="#2f6feb" stroke-width="2"/><polygon points="170.0,98.0 252.2,143.3 212.3,228.2 119.9,238.9 94.7,145.5" fill="#1f883d22" stroke="#1f883d" stroke-width="2"/><polygon points="170.0,98.0 275.0,135.9 213.7,230.2 112.9,248.6 82.1,141.4" fill="#bf870022" stroke="#bf8700" stroke-width="2"/></svg><div class="legend"><span class="dot" style="background:#2f6feb"></span>何慧 <span class="dot" style="background:#1f883d"></span>jiangtx <span class="dot" style="background:#bf8700"></span>wangyue111 </div></div>
|
||||
<div class="card"><h2>能力矩阵(0–100)</h2><table><tr><th>成员</th><th>影响力</th><th>贡献度</th><th>活跃度</th><th>项目经验</th><th>语言能力</th></tr><tr><td><span class="dot" style="background:#2f6feb"></span>何慧 <span class="muted">@yystopf</span></td><td>79</td><td>97</td><td>94</td><td>97</td><td>90</td></tr><tr><td><span class="dot" style="background:#1f883d"></span>jiangtx <span class="muted">@jiangtx</span></td><td>60</td><td>72</td><td>60</td><td>71</td><td>66</td></tr><tr><td><span class="dot" style="background:#bf8700"></span>wangyue111 <span class="muted">@wangyue111</span></td><td>60</td><td>92</td><td>62</td><td>81</td><td>77</td></tr><tr><td><b>团队均值</b></td><td><b>66</b></td><td><b>87</b></td><td><b>72</b></td><td><b>83</b></td><td><b>77</b></td></tr></table></div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="card"><h2>共有研究方向</h2><span class="tag">其它 ×2</span></div>
|
||||
<div class="card"><h2>独有研究方向</h2><span class="tag">DevOps/运维/网管</span><span class="tag">Web应用开发</span><span class="tag">云计算和大数据</span><span class="tag">人工智能</span><span class="tag">企业应用</span><span class="tag">大数据</span><span class="tag">大气海洋环境</span><span class="tag">天文地球物理</span><span class="tag">工业设计制造</span><span class="tag">应用工具</span><span class="tag">开发工具</span><span class="tag">插件和扩展</span><span class="tag">操作系统</span><span class="tag">数据存储</span><span class="tag">数据库</span><span class="tag">新能源新材料</span><span class="tag">泛化工具</span><span class="tag">游戏/娱乐</span><span class="tag">物联网</span><span class="tag">生物医药健康</span><span class="tag">程序开发</span></div>
|
||||
</div>
|
||||
<div class="card"><h2>协作网络图(连线=共同方向数)</h2><svg viewBox="0 0 420 420" width="420" height="420" xmlns="http://www.w3.org/2000/svg"><line x1="210" y1="64" x2="336" y2="283" stroke="#9ab4e8" stroke-width="1.3" opacity="0.75"><title>yystopf ↔ jiangtx: 1 个共同方向</title></line><circle cx="210" cy="64" r="22" fill="#2f6feb"/><text x="210" y="68" text-anchor="middle" font-size="11" fill="#fff">何慧</text><circle cx="336" cy="283" r="22" fill="#1f883d"/><text x="336" y="287" text-anchor="middle" font-size="11" fill="#fff">jian</text><circle cx="84" cy="283" r="22" fill="#bf8700"/><text x="84" y="287" text-anchor="middle" font-size="11" fill="#fff">wang</text></svg></div>
|
||||
<p class="muted" style="margin-top:24px">由 research-team-insight 工作流生成 · gitlink-cli profile(只读)</p></div></body></html>
|
||||
|
|
@ -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 时成员数据根目录(默认 <out>/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
|
||||
}
|
||||
|
|
@ -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{"<svg", "<polygon", "<line", "能力矩阵", "协作网络图", "课题组科研画像"} {
|
||||
if !strings.Contains(htmlOut, want) {
|
||||
t.Fatalf("html missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntoUnwrapped(t *testing.T) {
|
||||
// tolerate payload without the {"data":..} envelope
|
||||
dir := t.TempDir()
|
||||
writeFixture(t, dir, "major.json", `{"categories":["X"]}`)
|
||||
var mj majorData
|
||||
if err := parseInto(filepath.Join(dir, "major.json"), &mj); err != nil {
|
||||
t.Fatalf("parseInto: %v", err)
|
||||
}
|
||||
if len(mj.Categories) != 1 || mj.Categories[0] != "X" {
|
||||
t.Fatalf("categories=%v", mj.Categories)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var palette = []string{"#2f6feb", "#1f883d", "#bf8700", "#cf222e", "#6f42c1", "#0969da"}
|
||||
|
||||
// ---------- Markdown insight ----------
|
||||
|
||||
func buildInsightMarkdown(team string, subs []subject) string {
|
||||
var b strings.Builder
|
||||
n := len(subs)
|
||||
avg := teamAverages(subs)
|
||||
|
||||
fmt.Fprintf(&b, "# 🔬 科研团队洞察报告:%s\n\n", team)
|
||||
fmt.Fprintf(&b, "> 成员 %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("<tr><th>成员</th>")
|
||||
for _, d := range abilityDims {
|
||||
fmt.Fprintf(&matrix, "<th>%s</th>", d.ZH)
|
||||
}
|
||||
matrix.WriteString("</tr>")
|
||||
for i, s := range subs {
|
||||
color := palette[i%len(palette)]
|
||||
fmt.Fprintf(&matrix, `<tr><td><span class="dot" style="background:%s"></span>%s <span class="muted">@%s</span></td>`,
|
||||
color, html.EscapeString(s.displayName()), html.EscapeString(s.Login))
|
||||
for _, d := range abilityDims {
|
||||
fmt.Fprintf(&matrix, "<td>%d</td>", s.Dev.User.get(d.Key))
|
||||
}
|
||||
matrix.WriteString("</tr>")
|
||||
}
|
||||
matrix.WriteString("<tr><td><b>团队均值</b></td>")
|
||||
for _, d := range abilityDims {
|
||||
fmt.Fprintf(&matrix, "<td><b>%d</b></td>", avg[d.Key])
|
||||
}
|
||||
matrix.WriteString("</tr>")
|
||||
|
||||
sharedTags := tagsHTML(sharedTagList(shared))
|
||||
uniqueTags := tagsHTML(unique)
|
||||
|
||||
body := fmt.Sprintf(`
|
||||
<h1>🔬 课题组科研画像:%s</h1>
|
||||
<div class="sub">成员 %d 人 · 数据来源 gitlink-cli(只读)</div>
|
||||
<div class="grid">
|
||||
<div class="card"><h2>开发能力雷达</h2>%s%s</div>
|
||||
<div class="card"><h2>能力矩阵(0–100)</h2><table>%s</table></div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="card"><h2>共有研究方向</h2>%s</div>
|
||||
<div class="card"><h2>独有研究方向</h2>%s</div>
|
||||
</div>
|
||||
<div class="card"><h2>协作网络图(连线=共同方向数)</h2>%s</div>
|
||||
`,
|
||||
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(`<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">`+
|
||||
`<meta name="viewport" content="width=device-width,initial-scale=1"><title>%s</title><style>%s</style></head>`+
|
||||
`<body><div class="wrap">%s<p class="muted" style="margin-top:24px">由 research-team-insight 工作流生成 · gitlink-cli profile(只读)</p></div></body></html>`,
|
||||
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, `<svg viewBox="0 0 %.0f %.0f" width="%.0f" height="%.0f" xmlns="http://www.w3.org/2000/svg">`, size, size, size, size)
|
||||
for k := 1; k <= 4; k++ {
|
||||
fmt.Fprintf(&sb, `<circle cx="%.0f" cy="%.0f" r="%.1f" fill="none" stroke="#e6e8eb"/>`, 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, `<line x1="%.0f" y1="%.0f" x2="%.1f" y2="%.1f" stroke="#e6e8eb"/>`, 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, `<text x="%.1f" y="%.1f" text-anchor="%s" font-size="12" fill="#444">%s</text>`, 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, `<polygon points="%s" fill="%s22" stroke="%s" stroke-width="2"/>`, pts.String(), color, color)
|
||||
}
|
||||
sb.WriteString("</svg>")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func radarLegend(subs []subject) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<div class="legend">`)
|
||||
for i, s := range subs {
|
||||
color := palette[i%len(palette)]
|
||||
fmt.Fprintf(&sb, `<span class="dot" style="background:%s"></span>%s `, color, html.EscapeString(s.displayName()))
|
||||
}
|
||||
sb.WriteString("</div>")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func networkSVG(subs []subject) string {
|
||||
n := len(subs)
|
||||
const size = 420.0
|
||||
if n < 2 {
|
||||
return `<p class="muted">成员不足 2 人,无协作网络。</p>`
|
||||
}
|
||||
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, `<svg viewBox="0 0 %.0f %.0f" width="%.0f" height="%.0f" xmlns="http://www.w3.org/2000/svg">`, 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, `<line x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#9ab4e8" stroke-width="%.1f" opacity="0.75"><title>%s ↔ %s: %d 个共同方向</title></line>`,
|
||||
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, `<circle cx="%.0f" cy="%.0f" r="22" fill="%s"/><text x="%.0f" y="%.0f" text-anchor="middle" font-size="11" fill="#fff">%s</text>`,
|
||||
pos[i].x, pos[i].y, color, pos[i].x, pos[i].y+4, html.EscapeString(string(label)))
|
||||
}
|
||||
sb.WriteString("</svg>")
|
||||
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 `<span class="muted">无</span>`
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, it := range items {
|
||||
fmt.Fprintf(&sb, `<span class="tag">%s</span>`, 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)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<#
|
||||
一键复现脚本:运行科研团队洞察工作流并生成示例输出。
|
||||
用法: .\scripts\run_demo.ps1 [-Out <dir>] [-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
|
||||
Loading…
Reference in New Issue