Compare commits

...

92 Commits

Author SHA1 Message Date
wyxfzgg 0c1eebaf54 docs: 同步 README.zh-CN.md 与新版 README.md(竞赛项目说明,中文版一致)
CI / Build, Lint, Test (push) Failing after 2m24s Details
2026-07-10 20:08:12 +08:00
wyxfzgg dcf7a868b0 docs: 重写 README 为竞赛项目说明(jiangtx 增强版)
CI / Build, Lint, Test (push) Failing after 1m20s Details
基于原 README/README.zh-CN 格式,重写为描述本组竞赛项目:
小组分工、功能矩阵(21命令组+37 Skill+工作流+科研服务)、四子任务成果、
科研服务 gitlink-web、上游 PR 成果落地、DevOps、项目结构、文档导航、FAQ。
2026-07-10 19:49:20 +08:00
wyxfzgg d46be7adfc docs(skills): 5 个编排 Skill 补充 EXAMPLES(子任务三执行流程演示)
CI / Build, Lint, Test (push) Failing after 2m27s Details
为 community-ops/pr-gate/project-bootstrap/multi-repo-ops/contributor-growth 各补一份
EXAMPLES.md:针对 jiangtx/gitlink-cli 的 Agent 执行流程演示(触发→编排步骤→调用的
gitlink-cli 命令→预期产物→检查清单),标注写操作需确认,非伪造实跑数值。
2026-07-10 18:42:20 +08:00
wyxfzgg 9359b1b364 chore: 删除 gitlink-web 空占位文件 fork自/镜像(原创性隐患)
CI / Build, Lint, Test (push) Failing after 1m20s Details
2026-07-10 18:38:56 +08:00
wyxfzgg 7bff8719f6 docs: 新增上游 PR 清单(12 个细粒度 PR 的成果落地汇总)
CI / Build, Lint, Test (push) Failing after 2m24s Details
汇总向 gitlink/gitlink-cli 提交的 12 个 PR(5 CLI + 7 Skill),含分支名、对比链接、场景。
作为竞赛成果落地维度的可见证据。
2026-07-09 22:33:44 +08:00
wyxfzgg 601db5bba8 docs+test: 在队友 master 基础上叠加提交前完善(纯新增,不碰队友工作)
CI / Build, Lint, Test (push) Failing after 2m19s Details
- 测试: 4 模块 16 测试(issue series-update/ci lifecycle/org teams/search code-issues)
  (notification_test.go 用队友的,不覆盖)
- 文档: REPRODUCE.md 复现指南、UPSTREAM_PR_GUIDE 上游PR手册、
  命令 help doc(workflow/capability/compare/notification)、change doc(export/pm/wiki)、
  三份课程报告入 docs/reports/、skills/README(37 Skill)、gitlink-web/README(14 Skill)
- 工作流: gitlink_workflow.py 接通 --cli-bin/GITLINK_CLI_BIN、新增跨平台 run_demo.sh、run_demo.ps1 支持 -CliBin
- DevOps: 新增 gitlink-web.yml 科研服务部署(仅同步代码+重启,不碰 API_KEY)
- 不含: README/.devops(保留队友版本)、不删队友的 gitlink-cli.yml/wyx_*
2026-07-09 20:26:45 +08:00
蒋天翔 094bf57ac6 chore: 加 .gitattributes 统一文本文件 LF 行尾
CI / Build, Lint, Test (push) Failing after 18m2s Details
Windows core.autocrlf 会把 LF 转 CRLF,导致 gofmt -s -l / CI make fmt
误报整个文件改动(78 个 .go 文件受影响)。固定 LF 后所有平台一致,
verify.sh 段1 的 gofmt 检查本地也能稳定通过。

* text=auto eol=lf;二进制(png/jpg/pdf/exe/zip)显式 binary。
2026-07-07 09:37:17 +08:00
jiangtx 2d141639c6 Merge pull request '修复跨平台操作' (#19) from jtx_branch into master
CI / Build, Lint, Test (push) Failing after 1m18s Details
2026-07-07 09:14:02 +08:00
蒋天翔 3dbcef1b34 Merge origin/master into jtx_branch
CI / Build, Lint, Test (pull_request) Failing after 2m23s Details
冲突解决策略:9 个冲突文件均以 master 为准(master 的 shortcut 集是
jtx_branch 的超集——notification/pm/export/wiki/capability 等已独立实现
且更完整,含 P0 show/demo/--web/--demo + i18n + 覆盖率门禁)。

jtx_branch 的独有贡献(master 没有的)通过 merge 自动合入:
- shortcuts/notification/notification_test.go

冲突文件(全选 master 版本):
.devops/ci.yml(jtx_branch 删 / master 加覆盖率门禁 → 保留 master)
.devops/gitlink-cli.yml, cmd/{alias,auth,root}.go, internal/client/client.go,
shortcuts/{register,register_test,repo/repo}.go
2026-07-07 09:10:45 +08:00
蒋天翔 2019da5eb5 chore: 停止跟踪 gitlink-cli 二进制构建产物
CI / Build, Lint, Test (push) Failing after 2m24s Details
gitlink-cli / gitlink-cli.exe 是 go build 的本地产出,不应入库。
- 加入 .gitignore(/ 前缀精确匹配根目录)
- git rm --cached 停止跟踪(保留本地文件)
- 后续分发走 Release 附件

历史中的旧版本 blob 仍保留(不影响已发布的 commit),仅停止后续跟踪。
2026-07-07 09:00:14 +08:00
蒋天翔 87767f5482 feat: P0 新增 show/demo/--web/--demo 命令与离线演示能力
CI / Build, Lint, Test (push) Failing after 2m27s Details
- internal/web: URL Builder (16 模式) + 跨平台浏览器(单一 URL 来源)
- cmd/show: 12 子命令打印 GitLink 网页 URL(投影友好 + --format json)
- cmd/demo: +run showcase 离线演示脚本(自动 GITLINK_DEMO=1)
- internal/demo: Mock Transport + 20 fixtures + --demo flag(无网无 Token)
- shortcuts/common/web_post.go: --web 全局后处理(命令执行后打开网页)
- shortcuts/capability: +check 支持 --format 结构化输出
- scripts/verify.sh: 7 段分段验证脚本(编译/覆盖率/注册/E2E/联动)
- doc/: 验证报告 + 演示脚本 + 设计文档
- .gitignore: 排除验证临时产物与课程材料
2026-07-07 08:51:14 +08:00
蒋天翔 57c2811f48 新建 internal/web/ (url_builder.go + browser.go + 测试) 2026-07-07 08:51:12 +08:00
wyxttn 6f88fd8496 refactor: .devops/wyx_gitlink_cli_release.yml
CI / Build, Lint, Test (push) Failing after 2m17s Details
2026-07-04 10:30:44 +08:00
wyxttn 8aacc6d29b feat: .devops/wyx_gitlink_cli_release.yml
CI / Build, Lint, Test (push) Failing after 17m27s Details
2026-07-04 10:06:04 +08:00
wyxfzgg 51026af669 feat(skills): 新增gitlink-pr-gate编排型Skill(场景②代码质量看门人)
CI / Build, Lint, Test (push) Failing after 2m17s Details
PR提交 → AI代码审查 → CI检查 → 结构化Review评论 → 质量达标自动合并
编排 code-review/commit-quality/ci-health/pr 四个子Skill,串联8+命令。
统一采用编排型Skill方式实现,与前4个场景一致。
2026-07-03 10:59:27 +08:00
wyxfzgg 9a493914de feat(skills): 新增4个编排型Skill(子任务三端到端自动化工作流)
CI / Build, Lint, Test (push) Failing after 2m16s Details
- gitlink-community-ops: 社区运营自动化(Issue分类→周报→Release)
- gitlink-project-bootstrap: 项目一键初始化
- gitlink-multi-repo-ops: 多仓库协同
- gitlink-contributor-growth: 贡献者成长体系(排行+徽章)

每个Skill编排多个已有子Skill,串联≥6个CLI命令,含Mermaid流程图与Agent触发示例。
2026-07-02 16:15:18 +08:00
林迪文 52ac386721 feat: 增强onboarding(自动评论) + issue-triage(标签/责任人) + 新增3个Skill
CI / Build, Lint, Test (push) Failing after 2m23s Details
2026-07-01 21:24:02 +08:00
lindiwen23 201ec996db Merge pull request 'feat: add gitlink-web service' (#18) from dw into master
CI / Build, Lint, Test (push) Failing after 1h4m21s Details
2026-07-01 15:28:35 +08:00
林迪文 5a51c882b2 feat: add gitlink-web service
CI / Build, Lint, Test (pull_request) Failing after 1h4m55s Details
2026-07-01 15:28:57 +08:00
蒋天翔 0eb37dc2de style: gofmt 格式化修正(import 排序 + 对齐)
CI / Build, Lint, Test (push) Failing after 2m11s Details
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:28:22 +08:00
蒋天翔 b32ff9e682 ci: 构建流水线增加批量操作命令验证(batch-close/series-update/batch-add)
CI / Build, Lint, Test (push) Failing after 2m11s Details
2026-06-05 09:28:31 +08:00
蒋天翔 4c8d330ae0 chore: 添加 Linux 版 gitlink-cli 二进制
CI / Build, Lint, Test (push) Failing after 2m9s Details
2026-06-04 15:52:03 +08:00
蒋天翔 40e162e6cd chore: 添加 gitlink-cli.exe 编译产物
CI / Build, Lint, Test (push) Failing after 2m8s Details
2026-06-04 15:50:14 +08:00
蒋天翔 b094cac5df merge: 合并远程 master,解决 register_test.go 冲突(保留 capability 模块)
CI / Build, Lint, Test (push) Failing after 2m3s Details
2026-06-04 15:47:35 +08:00
蒋天翔 c4769544ef fix: 修复 wiki 命令参数传递 + auth token store 跨平台适配 2026-06-04 15:46:55 +08:00
wyxttn 90536f620d refactor: .devops/wyx_gitlink_cli_ci.yml
CI / Build, Lint, Test (push) Failing after 1m3s Details
2026-06-04 15:31:17 +08:00
wyxfzgg 0c47cf261f fix: CI 格式化改为自动修复模式(本地无Go环境无法手动gofmt)
CI / Build, Lint, Test (push) Has been cancelled Details
2026-06-04 15:28:32 +08:00
wyxfzgg fad02b9f66 fix: 修复 gofmt 格式化问题
CI / Build, Lint, Test (push) Failing after 2m6s Details
- 所有文件从 CRLF 转为 LF 换行
- issue.go: 修正 journals/series-update 条目缩进
- register_test.go: 修正 expectedGroups 缩进
- ci.yml: 恢复格式化检查模式
2026-06-04 15:23:15 +08:00
蒋天翔 72055f3aad 更改部分 2026-06-04 15:13:01 +08:00
wyxfzgg 11e6087b1b fix: CI 格式化改为自动修复模式,避免别人的未格式化代码导致流水线失败
CI / Build, Lint, Test (push) Failing after 2m8s Details
2026-06-04 14:35:00 +08:00
蒋天翔 a8e67fc32e feat: add repo +delete-file shortcut and convert Skills from raw API to shortcut commands
CI / Build, Lint, Test (push) Failing after 2m6s Details
- Add repo +delete-file shortcut command in shortcuts/repo/repo.go
- Add comprehensive unit tests for create-file, update-file, delete-file
  (15 new test cases covering happy paths, optional params, missing args,
  and HTTP error handling)
- Update gitlink-notification-digest Skill to v3.0.0 using notification
  +list/+read/+read-all/+watch shortcuts
- Update gitlink-pm Skill to v1.1.0 using pm +dashboards/+sprints/+weekly
  shortcuts
- Fix gitlink-shared/SKILL.md Raw API example (user +me → api GET /users/me)
- Update api-reference.md examples to use notification shortcuts
- Update gitlink-contributor-insight examples to reflect current
  command availability
- Update CI config branch from wyx_branch to master

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:34:08 +08:00
wyxttn f334f3484f refactor: .devops/wyx_gitlink_cli_build.yml
CI / Build, Lint, Test (push) Failing after 2m6s Details
2026-06-04 11:18:49 +08:00
wyxttn 23894e5a6f refactor: .devops/wyx_gitlink_cli_ci.yml
CI / Build, Lint, Test (push) Failing after 2m6s Details
2026-06-04 11:15:37 +08:00
wyxfzgg 594e8f487b ci: 改回使用 gitlink_cli 密钥组
CI / Build, Lint, Test (push) Failing after 2m2s Details
2026-06-04 11:08:21 +08:00
wyxfzgg ff14af90fa fix: 修复 alias 单元测试,改为直接测试底层函数避免 cobra 集成问题
CI / Build, Lint, Test (push) Failing after 2m7s Details
2026-06-04 11:05:54 +08:00
wyxttn 5fd6b390ec refactor: delete .devops/jtx_gitlink_cli.yml
CI / Build, Lint, Test (push) Failing after 2m6s Details
2026-06-04 11:02:14 +08:00
wyxfzgg 0cd20b7616 fix: 复用已验证的 gitlink_cli_ci 密钥组,修复 SSH 认证问题
CI / Build, Lint, Test (push) Has been cancelled Details
2026-06-04 11:01:06 +08:00
wyxttn 2da3607083 refactor: .devops/wyx_gitlink_cli_ci.yml
CI / Build, Lint, Test (push) Has been cancelled Details
2026-06-04 11:00:12 +08:00
wyxttn f7a9492ceb refactor: .devops/wyx_gitlink_cli_build.yml
CI / Build, Lint, Test (push) Failing after 1m3s Details
2026-06-04 10:54:14 +08:00
wyxttn 7c29e22606 refactor: .devops/wyx_gitlink_cli_ci.yml
CI / Build, Lint, Test (push) Failing after 2m3s Details
2026-06-04 10:51:45 +08:00
wyxfzgg f1555afc18 fix: 流水线 SSH 认证改为密码方式
CI / Build, Lint, Test (push) Has been cancelled Details
2026-06-04 10:50:59 +08:00
wyxttn 02f5022852 feat: .devops/wyx_gitlink_cli_build.yml
CI / Build, Lint, Test (push) Failing after 2m6s Details
2026-06-04 10:49:05 +08:00
wyxttn 31b6385e15 feat: .devops/wyx_gitlink_cli_ci.yml
CI / Build, Lint, Test (push) Has been cancelled Details
2026-06-04 10:48:35 +08:00
wyxfzgg 49c0fc9194 ci: 配置 master 分支的 CI 检查和构建验证两条流水线
CI / Build, Lint, Test (push) Failing after 2m6s Details
- 更新 .devops/ci.yml: master 合并后自动执行编译/静态分析/测试/格式化检查
- 新增 .devops/build.yml: master 合并后编译二进制并验证所有新命令注册成功
2026-06-04 10:40:44 +08:00
jiangtx 6a883ad27d refactor: .devops/jtx_gitlink_cli.yml
CI / Build, Lint, Test (push) Failing after 2m3s Details
2026-06-04 08:50:41 +08:00
蒋天翔 da95900b83 chore: trigger pipeline with updated CI config
CI / Build, Lint, Test (push) Failing after 2m7s Details
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 08:47:03 +08:00
蒋天翔 298c29795e feat: merge jtx_branch unique commands into master
CI / Build, Lint, Test (push) Failing after 13m10s Details
- ci: add activate, deactivate, authorize commands; fix indentation
- org: add teams, create-team, remove-user commands; fix indentation
- search: add code, issues commands; fix indentation and cleanup
- repo: add raw, activity, compare, create-file, update-file commands
- notification: add watch command
- label: add clone command
- register_test: fix expected group count

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 08:33:04 +08:00
蒋天翔 044551dbc8 fix: remove -race flag and fix shell chain logic in CI pipeline
CI / Build, Lint, Test (push) Failing after 1h2m37s Details
-race requires CGO/gcc not available in Alpine. Parenthesized || true
to prevent chain logic interference.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 21:06:08 +08:00
蒋天翔 7e7e2ccb47 feat: 添加通知管理、标签克隆、仓库文件操作等 8 项功能
CI / Build, Lint, Test (pull_request) Failing after 2m22s Details
新增功能:
- notification: list/read/read-all/watch 快捷命令
- repo: raw/activity/compare/create-file/update-file 快捷命令
- label: clone 快捷命令(从其他仓库克隆标签)
- alias: +list/+set/+delete/+expand 子命令

修复:
- auth.go: 修复 Windows 下 syscall.Stdin 类型兼容问题
- register_test: 更新分组计数以包含 notify

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 20:45:15 +08:00
jiangtx 796e7c0bb5 feat: .devops/jtx_gitlink_cli.yml
CI / Build, Lint, Test (push) Failing after 2m6s Details
2026-06-03 20:38:16 +08:00
蒋天翔 028a2f1cab feat: 添加 Dockerfile 和建木 DevOps 流水线配置
CI / Build, Lint, Test (push) Waiting to run Details
- 多阶段 Docker 构建(golang:1.26-alpine + alpine:3.20)
- 流水线:push 到 master 自动测试、构建、部署到 ECS

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 20:34:57 +08:00
蒋天翔 0fd9d0f6d7 feat: 添加 Dockerfile 和建木 DevOps 流水线配置
- 多阶段 Docker 构建(golang:1.26-alpine + alpine:3.20)
- 流水线:push 到 master 自动测试、构建、部署到 ECS

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 20:34:30 +08:00
wyxttn 3a3b2a9f31 Merge pull request '新增 alias/browse/status 三个开发者体验命令的单元测试' (#17) from wyx_branch into master
CI / Build, Lint, Test (push) Failing after 1m2s Details
2026-06-03 12:45:26 +08:00
wyxfzgg 9474c1f432 test: 补充 alias/browse/status 单元测试
CI / Build, Lint, Test (pull_request) Failing after 2m3s Details
- 新增 cmd/alias/alias_test.go: 8 个测试(加载/保存/覆盖/删除/无效YAML/子命令结构)
- 新增 cmd/browse/browse_test.go: 5 个测试(命令结构/参数校验/示例内容)
- 新增 cmd/status/status_test.go: 5 个测试(命令结构/token来源判断)

关联 Issue: #14
2026-06-03 12:39:48 +08:00
wyxttn 23f44bf211 Merge pull request 'feat: 安装脚本 + CI 配置 + 命令帮助文档 + 跨平台验证报告' (#16) from wyx_branch into master
CI / Build, Lint, Test (push) Failing after 2m7s Details
2026-06-03 12:23:47 +08:00
wyxfzgg b887e5053d feat: 新增安装脚本 + CI 配置 + 命令帮助文档 + 跨平台验证报告
CI / Build, Lint, Test (pull_request) Failing after 2m6s Details
- 新增 scripts/install.sh: Linux/macOS 一键安装脚本
- 新增 scripts/install.ps1: Windows PowerShell 安装脚本
- 修改 .devops/ci.yml: 更新 CI 支持 wyx_branch 分支
- 新增 doc/commands/pm.md: pm 模块命令参考手册
- 新增 doc/commands/wiki.md: wiki 模块命令参考手册
- 新增 doc/commands/alias-browse-status.md: alias/browse/status 命令参考手册
- 新增 doc/commands/export.md: export 模块命令参考手册
- 新增 doc/commands/cross-platform.md: 跨平台兼容性验证报告

关联 Issue: #16
2026-06-03 12:07:13 +08:00
wyxttn 99e90deda1 Merge pull request 'feat: 新建 export 模块,添加 3 条数据导出命令' (#15) from wyx_branch into master
CI / Build, Lint, Test (push) Failing after 2m7s Details
2026-06-03 12:02:09 +08:00
wyxfzgg 2cc0e5c08b feat: 新建 export 模块,添加 3 条数据导出命令 (issues/prs/contributors)
CI / Build, Lint, Test (pull_request) Failing after 2m6s Details
- 新增 shortcuts/export/export.go: 3 条 Shortcut 命令,支持 CSV/JSON 格式导出
- 新增 shortcuts/export/export_test.go: 7 个测试函数
- 修改 shortcuts/register.go: 注册 export 模块,修复重复 webhook import

关联 Issue: #15
2026-06-03 12:00:04 +08:00
wyxttn 90d1bb53f3 Merge pull request 'feat: 新增 alias/browse/status 三个开发者体验命令' (#14) from wyx_branch into master
CI / Build, Lint, Test (push) Failing after 2m6s Details
2026-06-03 11:56:16 +08:00
wyxfzgg a28a8eae8a feat: 新增 alias/browse/status 三个开发者体验命令
CI / Build, Lint, Test (pull_request) Failing after 2m2s Details
- 新增 cmd/alias/alias.go: 管理命令别名 (+list/+set/+delete)
- 新增 cmd/browse/browse.go: 在浏览器中打开 GitLink 页面(跨平台)
- 新增 cmd/status/status.go: 显示登录状态和上下文信息
- 修改 cmd/root.go: 注册三个新命令

关联 Issue: #14
2026-06-03 11:50:33 +08:00
wyxttn 3c278c5922 Merge pull request 'feat: 新建 wiki 模块,添加 5 条 Wiki 管理命令' (#13) from wyx_branch into master
CI / Build, Lint, Test (push) Failing after 1m2s Details
2026-06-03 11:44:09 +08:00
wyxfzgg 6bcd8b65d2 feat: 新建 wiki 模块,添加 5 条 Wiki 管理命令 (pages/get/create/update/delete)
CI / Build, Lint, Test (pull_request) Failing after 1m2s Details
- 新增 shortcuts/wiki/wiki.go: 5 条 Shortcut 命令
- 新增 shortcuts/wiki/wiki_test.go: 9 个测试函数,覆盖正常/异常/缺少参数场景
- 修改 shortcuts/register.go: 注册 wiki 模块

关联 Issue: #13
2026-06-03 11:39:55 +08:00
wyxttn 54632b9ac6 Merge pull request 'feat: 新建 pm 模块,添加 6 条项目管理命令' (#12) from wyx_branch into master
CI / Build, Lint, Test (push) Failing after 2m7s Details
2026-06-03 11:35:45 +08:00
wyxfzgg b4cfdb753e feat: 新建 pm 模块,添加 6 条项目管理命令 (dashboards/sprints/weekly/tags/pipelines/runs)
CI / Build, Lint, Test (pull_request) Failing after 2m9s Details
- 新增 shortcuts/pm/pm.go: 6 条 Shortcut 命令
- 新增 shortcuts/pm/pm_test.go: 单元测试覆盖
- 修改 shortcuts/register.go: 注册 pm 模块
- 修复 register.go 中 milestone 描述缺少逗号的语法问题

关联 Issue: #12
2026-06-03 11:27:44 +08:00
lindiwen23 eae0269171 Merge pull request 'fix(notification-digest): v2.0.0 修正虚构 notification 命令' (#11) from dw into master
CI / Build, Lint, Test (push) Failing after 2m8s Details
2026-06-03 10:32:30 +08:00
林迪文 94d5115789 fix(notification-digest): v2.0.0 — 修正虚构的 notification 命令为实际 messages API
CI / Build, Lint, Test (pull_request) Failing after 2m5s Details
- notification +list/+read/+read-all 均不存在,替换为 gitlink-cli api 调用
- API 端点: GET /api/users/{owner}/messages.json
- 标记已读: POST /api/users/{owner}/messages/{id}/read
- 添加 CLI 路径 Bug 说明(前导 / 导致解析错误)
- 补充完整 source 枚举值表(30+ 个枚举)
- 新增 EXAMPLES.md(手动执行 + Agent 测试两个样例)
- gitlink-shared/api-reference.md 补充消息 API 章节

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:31:42 +08:00
lindiwen23 dd82cf4d3f Merge pull request 'feat(contributor-insight): 更新至 v1.1.0,新增执行样例与命令可用性适配' (#10) from dw into master
CI / Build, Lint, Test (push) Failing after 2m6s Details
2026-06-03 10:30:23 +08:00
林迪文 7c293f9b5a feat(contributor-insight): 更新至 v1.1.0,新增 EXAMPLES.md 和实际执行样例
CI / Build, Lint, Test (pull_request) Failing after 2m8s Details
- 新增命令可用性声明(标注 repo +contributors/user +heatmap 等不可用命令及替代方案)
- 重写工作流步骤:贡献者列表从 pr +list 提取,活跃度从 PR 时间戳推算
- 新增年轻项目(<30天)分级放宽规则
- 扩充异常场景处理表(新增 3 种场景)
- 新增 EXAMPLES.md:手动执行 + Agent 调用两种完整样例
- 新增 examples/jiangtx-gitlink-cli.md:原始命令输出数据附录
- 同步 ci-health EXAMPLES.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:29:52 +08:00
林迪文 dd805951a5 feat: 新增 3 个 Skill + 增强 2 个 Skill(v1.1.0)
新增:
- gitlink-contributor-insight: 贡献者活跃度分析(user +heatmap/stats/trends)
- gitlink-ci-health: CI 健康巡检(ci +authorize/builds/logs)
- gitlink-notification-digest: 通知摘要(notification +list/read/read-all)

增强(v1.1.0):
- gitlink-issue-triage: 新增 issue +journals 活动日志分析 + series-update 批量操作
- gitlink-research-tracker: 新增 search +code/+issues 多维度搜索

基于子任务一的新命令开发。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:15:08 +08:00
lindiwen23 431545c633 Merge pull request 'fix: Skills 文件 api 命令替换为 Shortcut 命令 (~107 处)' (#9) from dw into master
CI / Build, Lint, Test (push) Failing after 2m7s Details
2026-06-03 09:39:37 +08:00
林迪文 d50b3c7364 fix: Skills 文件 api 命令替换为 Shortcut 命令 (~107 处)
CI / Build, Lint, Test (pull_request) Failing after 2m9s Details
将 skills/ 目录下 21 个文件中的 gitlink-cli api <METHOD> <PATH> 命令
替换为对应的 Shortcut 命令,修复因 Token 注入不完整导致的 HTML 响应问题。

主要替换:
- api GET /:owner/:repo/languages → repo +languages
- api GET /:owner/:repo/sub_entries → repo +files
- api GET /:owner/:repo/raw/... → repo +raw
- api POST /:owner/:repo/create_file → repo +create-file
- api POST /:owner/:repo/pulls/:id/reviews → pr +review
- api GET /:owner/:repo/pulls/:id → pr +view
- 等共 107 处替换

注: 6 处 pm 域命令暂留(pm 模块尚未构建)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:39:48 +08:00
林迪文 b748a95511 feat: 新建 notification 模块并注册
CI / Build, Lint, Test (pull_request) Failing after 2m2s Details
- notification +list: 列出通知(支持已读/参与过滤)
- notification +read: 标记单条通知为已读
- notification +read-all: 标记所有通知为已读
- register.go: 添加 notification 模块的 import、groups、descriptions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:19:18 +08:00
lindiwen23 e9c4b5b0aa Merge pull request 'feat: 新建 notification 模块并注册' (#8) from dw into master
CI / Build, Lint, Test (push) Failing after 1m2s Details
2026-06-03 09:19:02 +08:00
林迪文 0017147cbd feat: org +teams/+create-team/+remove-user, search +code/+issues
CI / Build, Lint, Test (pull_request) Failing after 1m3s Details
- org +teams: 列出组织下的所有团队
- org +create-team: 在组织下创建新团队
- org +remove-user: 从组织中移除成员
- search +code: 在仓库中搜索代码
- search +issues: 搜索 Issue(支持状态/标签/作者过滤)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:14:57 +08:00
lindiwen23 b8952a8838 Merge pull request 'feat: org +teams/+create-team/+remove-user, search +code/+issues' (#7) from dw into master
CI / Build, Lint, Test (push) Failing after 2m11s Details
2026-06-03 09:14:46 +08:00
lindiwen23 20bcdad4ba Merge pull request 'fix: detectHTMLResponse 跳过 XML 声明,添加 HTML 响应检测' (#6) from dw into master
CI / Build, Lint, Test (push) Failing after 1m2s Details
2026-06-03 09:02:02 +08:00
林迪文 691961072a feat: issue +journals/+series-update, ci +activate/+deactivate/+authorize
CI / Build, Lint, Test (pull_request) Failing after 1m2s Details
- issue +journals: 查看 Issue 活动日志
- issue +series-update: 批量更新 Issue 状态
- ci +activate: 激活 CI/CD
- ci +deactivate: 停用 CI/CD
- ci +authorize: 检查 CI/CD 授权状态

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:02:06 +08:00
林迪文 9b08029643 merge: resolve client.go conflict, keep XML declaration fix
CI / Build, Lint, Test (pull_request) Failing after 2m2s Details
2026-06-03 08:58:40 +08:00
林迪文 044e736a1b fix: detectHTMLResponse 跳过 XML 声明,添加 HTML 响应检测
CI / Build, Lint, Test (pull_request) Failing after 2m2s Details
- 在 json.Unmarshal 之前检测 HTML 响应
- detectHTMLResponse 先 strip <?xml...?> 声明再检查 HTML 前缀
- 返回中文诊断提示替代原始 JSON parse error
- 修复 Code Review: TestDetectHTMLResponse/XML_声明后跟_HTML

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:33:52 +08:00
林迪文 64ccd0466a Merge remote-tracking branch 'upstream/master' 2026-06-03 08:27:51 +08:00
蒋天翔 ad906e212c fix: handle XML declaration in detectHTMLResponse and remove .devops/ci.yml
- Strip <?xml...?> declaration before checking HTML prefixes
- Remove .devops/ci.yml containing personal DevCloud config

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 08:13:33 +08:00
jiangtx 46311f1b4d Merge pull request 'label 模块新建' (#5) from jtx_branch into master
CI / Build, Lint, Test (push) Failing after 2m2s Details
2026-06-03 00:26:59 +08:00
蒋天翔 70476a3a2a feat: add heatmap, stats, trends shortcuts to user module
CI / Build, Lint, Test (pull_request) Failing after 2m2s Details
- heatmap: show user contribution heatmap with optional --year flag
- stats: show user development statistics with optional --start-time/--end-time
- trends: show user project trends
- includes 11 new unit tests covering normal paths, parameter validation, and HTTP error handling

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 00:21:02 +08:00
jiangtx e10d56810d Merge pull request 'pr 域补全' (#4) from jtx_branch into master
CI / Build, Lint, Test (push) Failing after 1m1s Details
2026-06-03 00:11:15 +08:00
蒋天翔 fd2517f2f8 feat: add pr commits/branches/check-merge shortcuts with tests
CI / Build, Lint, Test (pull_request) Failing after 17m16s Details
Add three new shortcuts to the pr module:
- commits: list commits in a pull request (v1 API)
- branches: list branches for PR creation
- check-merge: check if two branches can be merged

Each shortcut includes unit tests covering both success and HTTP error paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 23:47:59 +08:00
jiangtx 9e7eb94658 Merge pull request 'repo 域补全' (#3) from jtx_branch into master
CI / Build, Lint, Test (push) Failing after 2m7s Details
2026-06-02 23:29:15 +08:00
蒋天翔 9106be35d4 feat: add repo languages/contributors/files/tags/commits shortcuts with tests
CI / Build, Lint, Test (pull_request) Failing after 2m10s Details
Add 5 new repo shortcuts:
- languages: show language breakdown
- contributors: list contributors with pagination
- files: list directory contents with ref/path filters
- tags: list tags with pagination
- commits: list commits with sha/path filters

Include 13 unit tests covering normal paths and HTTP error paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:18:23 +08:00
jiangtx 2620a4bf5d Merge pull request '基础设施修复' (#2) from jtx_branch into master
CI / Build, Lint, Test (push) Failing after 49m32s Details
2026-06-02 16:56:23 +08:00
蒋天翔 9ea0dc71b9 ci: 更新 CI 配置支持 Go 1.26.1 和建木流水线
CI / Build, Lint, Test (pull_request) Failing after 35m40s Details
- Gitea Actions: 更新 Go 版本为 1.26.1 以匹配 go.mod
- 建木流水线: 新增 .devops/ci.yml,push 到 jtx_branch 自动触发

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 16:03:40 +08:00
蒋天翔 aa0a623a73 feat: 添加 Raw API HTML 响应自动检测与诊断提示
当 GitLink API 返回 HTML 页面(如登录页)而非 JSON 数据时,
自动识别并返回结构化错误信息,包含可能原因和修复建议。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 16:02:59 +08:00
lindiwen23 6e7ee9cd12 Merge pull request 'feat: 新增 3 个 Skill(onboarding / issue-triage / research-tracker)' (#1) from dw into master
CI / Build, Lint, Test (push) Failing after 1m5s Details
2026-06-01 11:34:38 +08:00
林迪文 548141e2f1 feat: 新增 3 个 Skill(onboarding / issue-triage / research-tracker),收窄 gitlink-search 触发范围
CI / Build, Lint, Test (push) Failing after 1m1s Details
CI / Build, Lint, Test (pull_request) Failing after 1m6s Details
- gitlink-onboarding: 新人入门引导,帮助新贡献者发现适合入门的 Issue
- gitlink-issue-triage: Issue 智能分拣,按类型/紧急度/复杂度分类并生成分拣报告
- gitlink-research-tracker: 技术调研报告生成,多关键词搜索+深度评估+成熟度评分
- gitlink-search: 收窄描述避免与 research-tracker 触发冲突

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:25:30 +08:00
195 changed files with 20254 additions and 2041 deletions

41
.devops/build.yml Normal file
View File

@ -0,0 +1,41 @@
version: 2
name: wyx_gitlink_cli_build
description: "master 分支收到合并后编译构建并验证新命令是否注册成功"
trigger:
webhook: gitlink@1.0.0
event:
- ref: push
ruleset-operator: AND
global:
concurrent: 1
workflow:
- ref: start
name: 开始
task: start
- ref: git_clone_0
name: git clone
task: git_clone@1.2.9
input:
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: ssh_cmd_0
name: 编译并验证命令
task: ssh_cmd@1.1.1
input:
ssh_ip: '"121.41.212.97"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
ssh_cmd: >-
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && echo '>>> 编译二进制' && go build -o gitlink-cli . && echo '>>> 验证根命令' && ./gitlink-cli --help | head -20 && echo '>>> 验证 pm 模块6条命令' && ./gitlink-cli pm --help && echo '>>> 验证 wiki 模块5条命令' && ./gitlink-cli wiki --help && echo '>>> 验证 export 模块3条命令' && ./gitlink-cli export --help && echo '>>> 验证 alias 命令' && ./gitlink-cli alias --help && echo '>>> 验证 browse 命令' && ./gitlink-cli browse --help && echo '>>> 验证 status 命令' && ./gitlink-cli status && echo '✅ 所有命令验证通过'"
needs:
- git_clone_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0

41
.devops/ci.yml Normal file
View File

@ -0,0 +1,41 @@
version: 2
name: wyx_gitlink_cli_ci
description: "master 分支收到合并后自动执行 CI 检查(构建、静态分析、测试、格式化)"
trigger:
webhook: gitlink@1.0.0
event:
- ref: push
ruleset-operator: AND
global:
concurrent: 1
workflow:
- ref: start
name: 开始
task: start
- ref: git_clone_0
name: git clone
task: git_clone@1.2.9
input:
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: ssh_cmd_0
name: CI 检查
task: ssh_cmd@1.1.1
input:
ssh_ip: '"121.41.212.97"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
ssh_cmd: >-
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && go version && echo '>>> 1. 编译检查' && go build ./... && echo '>>> 2. 静态分析' && go vet ./... && echo '>>> 3. 单元测试 + 覆盖率门禁' && go test -race -coverprofile=coverage.out ./... && go tool cover -func=coverage.out | grep '^total:' && COV=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print int($3)}') && echo '覆盖率: '$COV'% (门禁阈值 78%)' && [ $COV -ge 78 ] && echo '✅ 覆盖率达标' && echo '>>> 4. 格式化修复' && gofmt -s -w . && echo '✅ 所有 CI 检查通过'"
needs:
- git_clone_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0

41
.devops/gitlink-cli.yml Normal file
View File

@ -0,0 +1,41 @@
version: 2
name: jtx_gitlink_cli
description: "gitlink-cli 项目:代码提交时自动测试、构建并部署到服务器"
trigger:
webhook: gitlink@1.0.0
event:
- ref: push
ruleset-operator: AND
global:
concurrent: 1
workflow:
- ref: start
name: 开始
task: start
- ref: git_clone_0
name: git clone
task: git_clone@1.2.9
input:
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: ssh_cmd_0
name: 测试并部署到服务器
task: ssh_cmd@1.1.1
input:
ssh_pass: ((jtx_gitlink_cli.jtx_gitlink_cli_ssh))
ssh_ip: '"121.41.212.97"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_cmd: >-
"cd /root && rm -rf gitlink-cli && git clone --depth=1 https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && docker run --rm -v $(pwd):/build -w /build docker.1ms.run/library/golang:1.26-alpine go test ./... && docker build --no-cache -t gitlink-cli . && (docker stop gitlink-cli || true) && (docker rm gitlink-cli || true) && docker run -d --restart=always --name gitlink-cli gitlink-cli version"
needs:
- git_clone_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0

41
.devops/gitlink-web.yml Normal file
View File

@ -0,0 +1,41 @@
version: 2
name: gitlink_web_deploy
description: "gitlink-web 科研服务master 合并后仅同步代码并重启服务(不触碰 systemd 单元与 API_KEY密钥由 deploy.py 一次性配在服务器上)"
trigger:
webhook: gitlink@1.0.0
event:
- ref: push
ruleset-operator: AND
global:
concurrent: 1
workflow:
- ref: start
name: 开始
task: start
- ref: git_clone_0
name: git clone
task: git_clone@1.2.9
input:
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: ssh_cmd_0
name: 同步代码并重启 gitlink-web
task: ssh_cmd@1.1.1
input:
ssh_ip: '"121.41.212.97"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
ssh_cmd: >-
"set -e && cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master https://gitlink.org.cn/jiangtx/gitlink-cli.git && echo '>>> 1. 同步代码gitlink-web → /opt/gitlink-web不动 systemd 与 API_KEY' && mkdir -p /opt/gitlink-web/{templates,skills,reports} && cp -rf gitlink-cli/gitlink-web/* /opt/gitlink-web/ && echo '>>> 2. 更新 Python 依赖' && cd /opt/gitlink-web && pip3 install -r requirements.txt -q 2>&1 | tail -3 && echo '>>> 3. 重启服务(沿用既有 systemd 单元API_KEY 已在服务器上)' && systemctl restart gitlink-web && sleep 3 && echo '>>> 4. 健康检查' && (curl -fsS http://127.0.0.1/skills/ -o /dev/null && echo '✅ 服务已更新上线http://121.41.212.97/skills/' || (echo '⚠️ 健康检查失败,若首次部署需先跑 gitlink-web/deploy.py 配好 API_KEY 与 systemd 单元' && systemctl status gitlink-web --no-pager -l | head -20)) && echo '>>> 5. 最近日志' && journalctl -u gitlink-web -n 5 --no-pager"
needs:
- git_clone_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0

View File

@ -0,0 +1,41 @@
version: 2
name: wyx_gitlink_cli_build
description: "master 分支收到合并后编译构建并验证新命令是否注册成功"
trigger:
webhook: gitlink@1.0.0
event:
- ref: push
ruleset-operator: AND
global:
concurrent: 1
workflow:
- ref: start
name: 开始
task: start
- ref: git_clone_0
name: git clone
task: git_clone@1.2.9
input:
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: ssh_cmd_0
name: 编译并验证命令
task: ssh_cmd@1.1.1
input:
ssh_ip: '"121.41.212.97"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
ssh_cmd: >-
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && echo '>>> 编译二进制' && go build -o gitlink-cli . && echo '>>> 验证根命令' && ./gitlink-cli --help | head -20 && echo '>>> 验证 pm 模块6条命令' && ./gitlink-cli pm --help && echo '>>> 验证 wiki 模块5条命令' && ./gitlink-cli wiki --help && echo '>>> 验证 export 模块3条命令' && ./gitlink-cli export --help && echo '>>> 验证 alias 命令' && ./gitlink-cli alias --help && echo '>>> 验证 browse 命令' && ./gitlink-cli browse --help && echo '>>> 验证 status 命令' && ./gitlink-cli status && echo '>>> 验证批量操作命令' && ./gitlink-cli issue +batch-close --help && ./gitlink-cli issue +series-update --help && ./gitlink-cli member +batch-add --help && echo '✅ 所有命令验证通过含3条批量操作命令'"
needs:
- git_clone_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0

View File

@ -0,0 +1,41 @@
version: 2
name: wyx_gitlink_cli_ci
description: "master 分支收到合并后自动执行 CI 检查(构建、静态分析、测试、格式化)"
trigger:
webhook: gitlink@1.0.0
event:
- ref: push
ruleset-operator: AND
global:
concurrent: 1
workflow:
- ref: start
name: 开始
task: start
- ref: git_clone_0
name: git clone
task: git_clone@1.2.9
input:
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: ssh_cmd_0
name: CI 检查
task: ssh_cmd@1.1.1
input:
ssh_ip: '"121.41.212.97"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
ssh_cmd: >-
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && go version && echo '>>> 1. 编译检查' && go build ./... && echo '>>> 2. 静态分析' && go vet ./... && echo '>>> 3. 单元测试' && go test -race ./... && echo '>>> 4. 格式化修复' && gofmt -s -w . && echo '✅ 所有 CI 检查通过'"
needs:
- git_clone_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0

View File

@ -0,0 +1,41 @@
version: 2
name: wyx_gitlink_cli_release
description: "部署流水线:打 tag 时交叉编译 Linux/Windows/macOS 三平台二进制并发布 GitLink Release"
trigger:
webhook: gitlink@1.0.0
event:
- ref: push
ruleset-operator: AND
global:
concurrent: 1
workflow:
- ref: start
name: 开始
task: start
- ref: git_clone_0
name: git clone含 tag 历史)
task: git_clone@1.2.9
input:
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: ssh_cmd_0
name: 交叉编译并发布 Release
task: ssh_cmd@1.1.1
input:
ssh_ip: '"121.41.212.97"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
ssh_cmd: >-
"cd /root && rm -rf release && mkdir release && cd release && git clone https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && git fetch --tags && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo dev) && echo '>>> 发布版本: '$VERSION && echo '>>> 1. 编译 Linux amd64' && GOOS=linux GOARCH=amd64 go build -o gitlink-cli-linux-amd64 . && echo '>>> 2. 编译 Windows amd64' && GOOS=windows GOARCH=amd64 go build -o gitlink-cli-windows-amd64.exe . && echo '>>> 3. 编译 macOS arm64' && GOOS=darwin GOARCH=arm64 go build -o gitlink-cli-darwin-arm64 . && echo '>>> 4. 发布 GitLink Release用服务器已登录的 gitlink-cli' && gitlink-cli release +create --owner jiangtx --repo gitlink-cli --tag $VERSION --name $VERSION --target master --body '本次发布版本 $VERSION含 Linux/Windows/macOS 三平台二进制。新增 pm/wiki/export/alias/browse/status 等模块,详见变更说明。' && echo '✅ Release '$VERSION' 发布完成'"
needs:
- git_clone_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0

15
.gitattributes vendored Normal file
View File

@ -0,0 +1,15 @@
# 统一文本文件为 LF 行尾。
# Windows 上 core.autocrlf 会把 LF 转成 CRLF导致 gofmt -s -l / CI make fmt
# 误报整个文件被改动。固定 LF 后所有平台一致。
* text=auto eol=lf
# 二进制文件(显式标记,避免被当作文本转码)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.exe binary
*.zip binary
*.gz binary

View File

@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version: '1.26.1'
- name: Build
run: go build ./...
@ -23,8 +23,8 @@ jobs:
- name: Lint
run: make lint
- name: Test
run: make test
- name: Test with coverage gate
run: make cover
- name: Check formatting
run: make fmt

View File

@ -26,5 +26,14 @@ jobs:
- name: Scan i18n key references
run: go run ./internal/i18n/cmd/check --scan-code
- name: Run Go tests
run: go test ./...
- name: Run Go tests with coverage gate
run: make cover
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-${{ github.sha }}
path: |
coverage.out
coverage.html
if-no-files-found: ignore

13
.gitignore vendored
View File

@ -1,3 +1,14 @@
gitlink-cli.exe
coverage
coverage.out
# 验证临时产物
coverage.verify.out
coverage.html
# 课程任务材料(非代码)
课程实践任务及要求*.pdf
# 构建产物二进制go build / make build 本地产出,不入库;分发走 Release 附件)
/gitlink-cli
/gitlink-cli.exe

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
FROM docker.1ms.run/library/golang:1.26-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o gitlink-cli .
FROM docker.1ms.run/library/alpine:3.20
RUN apk add --no-cache ca-certificates git
COPY --from=builder /build/gitlink-cli /usr/local/bin/gitlink-cli
RUN chmod +x /usr/local/bin/gitlink-cli
ENTRYPOINT ["gitlink-cli"]

View File

@ -28,9 +28,17 @@ fmt:
exit 1; \
fi
COVER_THRESHOLD ?= 78
cover:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
@go tool cover -func=coverage.out | grep '^total:'
@COV=$$(go tool cover -func=coverage.out | grep '^total:' | awk '{print int($$3)}'); \
echo "总覆盖率: $$COV% (门禁阈值: $(COVER_THRESHOLD)%)"; \
if [ $$COV -lt $(COVER_THRESHOLD) ]; then \
echo "❌ 覆盖率 $$COV% 低于门禁阈值 $(COVER_THRESHOLD)%"; exit 1; \
fi; \
echo "✓ 覆盖率达标"
lint:
golangci-lint run ./...

829
README.md
View File

@ -1,719 +1,266 @@
# gitlink-cli
# gitlink-clijiangtx 小组·竞赛增强版)
[![GitLink](https://img.shields.io/badge/GitLink-Gitlink%2Fgitlink--cli-green)](https://www.gitlink.org.cn/Gitlink/gitlink-cli)
[![License](https://img.shields.io/badge/License-MulanPSL--2.0-blue.svg)](https://license.coscl.org.cn/MulanPSL2)
[![GitLink](https://img.shields.io/badge/GitLink-jiangtx%2Fgitlink--cli-green)](https://www.gitlink.org.cn/jiangtx/gitlink-cli)
[![上游](https://img.shields.io/badge/upstream-gitlink%2Fgitlink--cli-blue)](https://www.gitlink.org.cn/gitlink/gitlink-cli)
[![License](https://img.shields.io/badge/License-MulanPSL--2.0-blue.svg)](https://license.coscl.org.cn/mulanpsl2)
[![Go Version](https://img.shields.io/badge/Go-1.26%2B-blue.svg)](https://golang.org)
[![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli)
[![CI](https://img.shields.io/badge/CI-%E5%BB%BA%E6%9C%A8-brightgreen)](./.devops/ci.yml)
The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, webhooks, member collaboration, CI/CD, and AI-powered workflows, with 40+ commands and AI Agent [Skills](./skills/).
> 本仓库是 [gitlink/gitlink-cli](https://www.gitlink.org.cn/gitlink/gitlink-cli) 的竞赛 Fork由 **jiangtx 小组**为《软件演化与运维》进阶任务 + GitLink 大赛开发。
> 在官方 **40+ 命令 / 11 Skill** 基础上,新增 **12 个命令模块 / 26 个 Skill / 端到端自动化工作流 / 科研智能服务**,覆盖开发者协作与科研辅助两大场景,兼容 Claude Code / OpenClaw / Cursor 等 AI Agent。
**[中文文档](./README.zh-CN.md)**
🌐 **科研服务在线演示**<http://121.41.212.97/skills/>Flask + DeepSeek + gitlink-cli14 个 Skill
[Install](#installation--quick-start) · [AI Agent Skills](#ai-agent-skills) · [Auth](#configure--use) · [Commands](#usage-examples) · [Contributing](#related-projects)
[安装](#安装与快速开始) · [功能矩阵](#功能矩阵) · [子任务成果](#四个子任务成果) · [Skills](./skills/README.md) · [复现指南](./REPRODUCE.md) · [课程报告](./docs/reports/README.md) · [上游 PR](./docs/UPSTREAM_PRS.md)
## Contributors
---
<div style="display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-start;">
<div align="center">
<a href="https://www.gitlink.org.cn/wangyue111" title="wangyue111"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/W/43_254_70/120.png" width="40" height="40" alt="wangyue111" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/wangyue111">wangyue111</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/wbtiger" title="tigerwang"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/T/14_168_39/120.png" width="40" height="40" alt="wbtiger" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/wbtiger">wbtiger</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/Mengz" title="Mengz"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/M/166_152_185/120.png" width="40" height="40" alt="Mengz" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/Mengz">Mengz</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/yangsai" title="杨赛"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/Y/94_150_149/120.png" width="40" height="40" alt="yangsai" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/yangsai">yangsai</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/mengcheng" title="camelliamc"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/M/206_114_54/120.png" width="40" height="40" alt="mengcheng" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/mengcheng">mengcheng</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/muel" title="赵奕程"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/Z/144_206_212/120.png" width="40" height="40" alt="muel" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/muel">muel</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/Leo77" title="Leo77"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/L/173_120_149/120.png" width="40" height="40" alt="Leo77" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/Leo77">Leo77</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/yingjie" title="yingjie"><img src="https://www.gitlink.org.cn/images/avatars/User/145288?t=1765791899" width="40" height="40" alt="yingjie" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/yingjie">yingjie</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/topshare" title="Kevin Zhang"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/K/65_152_142/120.png" width="40" height="40" alt="topshare" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/topshare">topshare</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/dtwdtw" title="dtwdtw"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/D/53_166_51/120.png" width="40" height="40" alt="dtwdtw" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/dtwdtw">dtwdtw</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/recorder" title="recorder"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/R/141_201_87/120.png" width="40" height="40" alt="recorder" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/recorder">recorder</a></sub>
</div>
</div>
## 小组成员
## Why gitlink-cli?
| 成员 | 主责领域 |
|---|---|
| wyx / wyxfzgg | pm / wiki / export / alias·browse·status 命令、安装脚本、DevOps 流水线、编排 Skill |
| 林迪文lindiwen23 | notification / search / ci / org 增强、gitlink-web 科研服务、research-tracker 等 Skill |
| whzy | workflow 命令、i18n 本地化、编排 Skill、show/demo 离线演示 |
| wangyue789 | pipeline 命令 |
| Mengz | webhook 命令 |
| 蒋天翔jiangtx | repo/pr/user 增强、capability、Skill 收录协调 |
| 何开元 | label 命令 |
- **Agent-Native Design** — Structured [Skills](./skills/) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup
- **Wide Coverage** — Repository, Issue, PR, Webhook, Member, Branch, Release, CI, Pipeline, Org, Search, and User workflows are covered by high-level commands
- **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output
- **Cross-Platform** — Runs on macOS, Linux, and Windows (x64/arm64), install via `npm install -g @gitlink-ai/cli` in one command, binary auto-downloaded
- **Open Source, Zero Barriers** — MulanPSL-2.0 license, ready to use, just `npm install`
- **Up and Running in 3 Minutes** — Interactive login or `GITLINK_TOKEN` env var, from install to first API call in just 3 steps
- **Secure & Controllable** — OS-native keychain credential storage, `GITLINK_TOKEN` env var for CI/CD & non-interactive environments, auto git remote context resolution
- **Three-Layer Architecture** — Shortcuts (human & AI friendly) → Raw API (full coverage) → Config (configuration management)
> 个人工作量以 GitLink 提交记录为准(`git shortlog -sn`)。
## Features
---
| Category | Capabilities |
|----------|-------------|
| 📦 Repo | List, create, fork, delete repositories, view repo info |
| 🐛 Issue | Create, update, close, batch close, comment on issues |
| 🔖 Label | Create, list, update, delete issue labels |
| 🔀 PR | Create, merge, review pull requests, view changed files |
| 👥 Member | List, add, remove repository members, change roles, create and accept invite links |
| 🌿 Branch | Create, delete, list, protect, unprotect branches |
| 🏷️ Release | Create, view, delete releases |
| 🏢 Org | Manage organizations, members, teams |
| 🔧 CI | View builds, logs, CI/CD operations |
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
| 🔔 Webhook | Manage repo webhooks and test deliveries |
| 🔍 Search | Search repositories, users |
| 👤 User | View user profiles and info |
| 📋 PM | Sprint management, kanban boards, weekly reports |
| 🤖 Workflow | AI-powered issue triage, PR review, release notes |
## 为什么用 gitlink-cli竞赛增强版
## Installation & Quick Start
- **Agent 原生** — 内置结构化 [Skills](./skills/)Claude Code / OpenClaw / Cursor 等 Agent 零配置即可操作 GitLink。
- **覆盖广** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户、**项目管理(PM)、Wiki、数据导出、通知、能力探测、对比、工作流分析** 全覆盖。
- **批量操作** — 批量关闭 Issue、批量更新 Issue 状态、批量添加成员(支持 CSV + `--dry-run` 预览)。
- **科研赋能** — 科研智能服务覆盖热点追踪 / 项目洞悉 / 合规检查 / 协作匹配 / 进度预警全链路。
- **端到端工作流** — 社区运营、代码质量看门人、项目一键初始化、多仓库协同、贡献者成长等编排 Skill。
- **跨平台** — macOS / Linux / Windowsx64/arm64`npm install -g @gitlink-ai/cli` 一键安装。
- **三层架构** — Shortcuts& AI 友好)→ Raw API全覆盖→ Config配置管理
### Requirements
---
- Node.js 14+ (`npm`/`npx`) — for npm installation
- Supported platforms: macOS, Linux, Windows (x64/arm64)
- Go 1.26+ — only required for building from source
## 功能矩阵
### Quick Start (Human Users)
**21 个命令组(~129 子命令)**
> **Note for AI assistants:** If you are an AI Agent helping the user with installation, jump directly to [Quick Start (AI Agent)](#quick-start-ai-agent), which contains all the steps you need to complete.
| 命令组 | 说明 | 本组新增 |
|---|---|:-:|
| repo / issue / pr / release / branch / user / org / search / ci | 官方核心命令含本组增强repo 文件操作、issue 批量、pr commits/versions 等) | 部分 |
| **pm** | 项目管理:仪表盘/Sprint/周报/标签/流水线/运行 | ✨ |
| **wiki** | Wiki 页面pages/get/create/update/delete | ✨ |
| **export** | 数据导出issues/prs/contributors → CSV/JSON | ✨ |
| **notification** | 通知list/read/read-all/watch | ✨ |
| **capability** | 后端 API 能力探测(跨实例兼容) | ✨ |
| **compare** | 分支/标签/commit 对比 | ✨ |
| **workflow** | 本地规则工作流triage/health/repo-report/pr-summary | ✨ |
| label / member / milestone / pipeline / webhook | 标签 / 成员(含批量) / 里程碑 / 流水线 / Webhook | ✨ |
| alias / browse / status | 开发者体验:别名 / 浏览器跳转 / 状态 | ✨ |
| api / auth / config / show / demo / completion / version | 基础设施 + 离线演示 | 部分 |
#### Install
**37 个 AI Agent Skill**(官方 11 + 新增 26 · 详见 [skills/README.md](./skills/README.md)
**端到端自动化工作流** · 详见 [examples/workflows/](./examples/workflows/)
**科研智能服务** · 详见 [gitlink-web/](./gitlink-web/README.md)
**From npm (recommended):**
---
## 安装与快速开始
### 要求
- Node.js 14+`npm`/`npx`)用于 npm 安装Go 1.26+ 用于源码构建;支持 macOS/Linux/Windows。
### 安装
```bash
# One command: installs CLI binary + AI Agent Skills
# 方式 Anpm推荐一条命令装好 CLI + Skills
npm install -g @gitlink-ai/cli
# 方式 B源码构建需 Go 1.26+
git clone https://www.gitlink.org.cn/jiangtx/gitlink-cli.git
cd gitlink-cli && go build -o gitlink-cli . # Windows: go build -o gitlink-cli.exe .
```
The binary is auto-downloaded for your platform during `postinstall`. No extra steps needed.
**From source:**
Requires Go 1.26+.
### 配置与登录
```bash
git clone https://www.gitlink.org.cn/Gitlink/gitlink-cli.git
cd gitlink-cli
make install
gitlink-cli config init # 一次性引导配置
gitlink-cli auth login # 交互登录(账号密码,推荐)
gitlink-cli auth login --token # 或粘贴私有 token
export GITLINK_TOKEN="your-token" # 或设环境变量CI/CD、非交互
gitlink-cli auth status # 确认登录
```
> **Windows users:** Run `npm install -g @gitlink-ai/cli` in PowerShell or CMD. For building from source, use `go install .` instead of `make install`.
#### Configure & Use
### 3 分钟体验
```bash
# 1. Configure (one-time, interactive guided setup)
gitlink-cli config init
# 2. Log in (choose one)
gitlink-cli auth login # Username/password (recommended)
gitlink-cli auth login --token # Or paste a private token
export GITLINK_TOKEN="your-token" # Or set env var (for CI/CD, non-interactive environments)
# 3. Start using
gitlink-cli repo +list
gitlink-cli repo +info --owner Gitlink --repo gitlink-cli # 仓库信息
gitlink-cli issue +list --owner Gitlink --repo gitlink-cli # Issue 列表
gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli # AI 分拣
gitlink-cli export +contributors --owner Gitlink --repo gitlink-cli --format json # 导出
gitlink-cli capability +check # 后端能力探测
```
### Quick Start (AI Agent)
---
> The following steps are for AI Agents. Some steps require the user to complete actions in a browser.
## 使用示例
**Step 1 — Install**
### 新增命令(本组贡献)
```bash
# One command: CLI binary + all Skills auto-installed
npm install -g @gitlink-ai/cli
# 项目管理
gitlink-cli pm +dashboards --owner jiangtx --repo gitlink-cli
gitlink-cli pm +weekly --owner jiangtx --repo gitlink-cli
# Wiki 管理
gitlink-cli wiki +pages --owner jiangtx --repo gitlink-cli
gitlink-cli wiki +create --owner jiangtx --repo gitlink-cli --title "周报" --content "..."
# 数据导出CSV/JSON科研/报表数据源)
gitlink-cli export +issues --owner jiangtx --repo gitlink-cli --format csv --output issues.csv
gitlink-cli export +contributors --owner jiangtx --repo gitlink-cli --format json
# 通知管理
gitlink-cli notification +list
gitlink-cli notification +watch --owner jiangtx --repo gitlink-cli
# 能力探测 + 对比 + 工作流分析
gitlink-cli capability +check
gitlink-cli compare +view --head feature --base master
gitlink-cli workflow +health --repository Gitlink/gitlink-cli --open-issues 3 --open-prs 1 --has-readme --has-license
# 批量操作
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli --numbers 1,2,3 --dry-run
gitlink-cli member +batch-add --owner jiangtx --repo gitlink-cli --user-ids 101,102
# 开发者体验
gitlink-cli alias +set rl "repo +list"
gitlink-cli browse issues/42
gitlink-cli status
```
**Step 2 — Configure**
### 核心命令(沿用上游)
```bash
gitlink-cli config init
gitlink-cli repo +list / +info / +create / +fork
gitlink-cli issue +create / +list / +view / +close / +comment
gitlink-cli pr +list / +create / +view / +merge / +review / +versions
gitlink-cli release +list / +create / +view
gitlink-cli branch +list / +create / +protect
gitlink-cli webhook +list / +create / +test
gitlink-cli ci +builds / +logs / +restart
gitlink-cli pipeline +runs / +run / +logs
gitlink-cli search +repos / +users / +code / +issues
gitlink-cli api GET /users/me # Raw API未封装接口
```
**Step 3 — Login**
### 全局参数
For interactive environments:
```bash
gitlink-cli auth login
```
| 参数 | 说明 |
|---|---|
| `--owner` / `--repo` | 仓库所有者 / 名称git 仓库内自动从 remote 解析) |
| `--format` | 输出格式 json / table / yamlworkflow 另支持 markdown |
| `--debug` | 调试输出 |
For non-interactive environments (CI/CD, Trae sandbox, MCP, etc.):
```bash
export GITLINK_TOKEN="your-private-token"
```
---
> To get a private token, go to GitLink web → Settings → Private Tokens.
## 四个子任务成果
**Step 4 — Verify**
### 子任务一·扩展 CLI50%
新增 12 个命令模块、~61 个子命令、3 条批量操作。每个新模块含源码 + 单元测试 + 命令帮助文档 + 变更说明。详见 [doc/commands/](./doc/commands/)、[doc/changes/](./doc/changes/)。
```bash
gitlink-cli user +me
```
### 子任务二·开发 Skill20%
新增 26 个 Skill覆盖智能代码审查 / Issue 分拣 / Release Notes / 项目健康度 / 许可证合规 / 新人引导 6 大场景。6 个已通过 Agent 平台验证。详见 [skills/README.md](./skills/README.md)。
## Usage Examples
### 子任务三·端到端自动化工作流20%
[community-ops-automation](./examples/workflows/community-ops-automation/):串联 `repo+info → issue+list → pr+list → release+list → issue+comment` 共 45 条命令,在 `Gitlink/gitlink-cli``jiangtx/gitlink-cli` 真实项目跑通,含 Python/PowerShell/bash 脚本、JSON 配置、单元测试、SVG 架构图、验证记录。另有 5 个编排 Skill社区运营/代码质量看门人/项目初始化/多仓库协同/贡献者成长)。
### Repository Operations
### 子任务四·应用 GitLink 辅助科研(加分)
[科研智能服务 gitlink-web](./gitlink-web/README.md) + [gitlink-research-tracker](./skills/gitlink-research-tracker/) Skill技术调研多关键词搜索→成熟度评分→趋势→选型覆盖热点追踪 / 项目洞悉 / 合规检查 / 协作匹配 / 进度跟踪 5 个科研场景。
```bash
# List repositories
gitlink-cli repo +list
### DevOps 流水线10%
[.devops/](./.devops/) 建木流水线:`ci.yml`test/vet/fmt· `build.yml`(编译+命令注册验证)· `gitlink-web.yml`科研服务部署。master 合并自动触发。
# View repository info
gitlink-cli repo +info --owner Gitlink --repo forgeplus
# Read repository README
gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
# Create a repository
gitlink-cli repo +create -n my-project -d "Project description"
# Fork a repository
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
```
### Webhook Management
```bash
# List webhooks
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
# Create a webhook
gitlink-cli webhook +create --owner Gitlink --repo forgeplus \
--url https://example.com/hook --events push,create
# Test a webhook
gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
# View webhook delivery tasks
gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68
```
### Member Management
```bash
# List repository members
gitlink-cli member +list --owner Gitlink --repo forgeplus
# Add a member
gitlink-cli member +add --owner Gitlink --repo forgeplus --user-id 101
# Preview batch add without changing data
gitlink-cli member +batch-add --owner Gitlink --repo forgeplus --user-ids 101,102 --dry-run
# Batch add members from a CSV file
gitlink-cli member +batch-add --owner Gitlink --repo forgeplus --from members.csv
# Change a member role
gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role Developer
# Create an invite link
gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true
```
### Issue Management
```bash
# List issues
gitlink-cli issue +list --owner Gitlink --repo forgeplus
# Create an issue
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: Login failed" -b "Steps to reproduce..."
# Create an issue with metadata
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: Login failed" --priority-id 3 --tag-ids 4,5 --assigner-ids 7
# View an issue
gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123
# Update issue metadata
gitlink-cli issue +update --owner Gitlink --repo forgeplus --number 123 --priority-id 4 --branch bugfix/login --due-date 2026-06-15
# Close an issue
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
# Preview batch close without changing data
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
# Batch close issues from a CSV file
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
# Add a comment
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed"
# List issue assigners
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
# List issue authors
gitlink-cli issue +authors --owner Gitlink --repo forgeplus
# List issue priorities
gitlink-cli issue +priorities --owner Gitlink --repo forgeplus
# List issue tags
gitlink-cli issue +tags --owner Gitlink --repo forgeplus --only-name
# List issue statuses
gitlink-cli issue +statuses --owner Gitlink --repo forgeplus
```
`issue +view`, `issue +update`, `issue +close`, and `issue +comment` prefer
`--number` / `-n` for the issue number shown in the web URL. `--id` / `-i`
is accepted as a compatibility alias for the same web issue number, not the
global database ID.
### Label Management
```bash
# List issue labels
gitlink-cli label +list --owner Gitlink --repo forgeplus
# Filter labels by keyword
gitlink-cli label +list --owner Gitlink --repo forgeplus -k bug
# Create a label (color defaults to #1E90FF)
gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "Something is broken" -c "#FF0000"
# Update a label (unspecified fields are preserved)
gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00"
# Delete a label
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
```
### Pull Requests
```bash
# List PRs
gitlink-cli pr +list --owner Gitlink --repo forgeplus
# Create a PR (same-repo branch)
gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: Search feature" --head feature/search --base master
# Create a PR (from a fork)
gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: New feature" --head your_username/forgeplus:feature/my-feature --base master
# View a PR
gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42
# Merge a PR
gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42
# Reopen a closed PR
gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42
# View changed files
gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42
# List PR patchset versions
gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42
# View a patchset version diff
gitlink-cli pr +version-diff --owner Gitlink --repo forgeplus -i 42 --version-id 16040
# List PR reviews
gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42
# Create a PR review (with dry-run preview)
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM" --dry-run
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM"
```
### Branch Management
```bash
# List branches
gitlink-cli branch +list --owner Gitlink --repo forgeplus
# Create a branch
gitlink-cli branch +create --name feature/new-feature
# Delete a branch
gitlink-cli branch +delete --name feature/old-feature
# Protect a branch
gitlink-cli branch +protect --name main
# Remove branch protection
gitlink-cli branch +unprotect --name main
```
### Release Management
```bash
# List releases
gitlink-cli release +list --owner Gitlink --repo forgeplus
# Create a release
gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.0 Stable" -b "Changelog..."
# View a release
gitlink-cli release +view --owner Gitlink --repo forgeplus -i <version_id>
```
### CI/CD Operations
```bash
# List builds
gitlink-cli ci +list --owner Gitlink --repo forgeplus
# View build log
gitlink-cli ci +log --owner Gitlink --repo forgeplus -i <build_id>
# Restart a build
gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i <build_id>
```
### Pipeline Operations
```bash
# List platform pipelines
gitlink-cli pipeline +list --owner-id 123 --page 1 --limit 20
# List repository pipeline runs
gitlink-cli pipeline +runs --owner Gitlink --repo forgeplus --ref master --workflow build.yml
# Start a pipeline workflow, previewing the request first
gitlink-cli pipeline +run --owner Gitlink --repo forgeplus --ref master --workflow build.yml --dry-run
# Inspect pipeline details and logs
gitlink-cli pipeline +view --owner Gitlink --repo forgeplus --id 7
gitlink-cli pipeline +logs --owner Gitlink --repo forgeplus --run-id 99 --id 7 --index 43
gitlink-cli pipeline +results --owner Gitlink --repo forgeplus --run-id 99
# Toggle or delete pipeline workflows, previewing destructive writes first
gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow build.yml --dry-run
gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run
```
### Search
```bash
# Search repositories
gitlink-cli search +repos -k "machine learning"
# Search users
gitlink-cli search +users -k "zhangsan"
```
### Workflow Agent Commands
`workflow` provides rule-based repository analysis for maintainers and AI Agents. It currently supports:
- `workflow +triage`
- `workflow +health`
- `workflow +pr-summary`
- `workflow +repo-report`
`workflow +pr-summary` defaults to `table` when `--format` is omitted.
`workflow +repo-report` defaults to `markdown` when `--format` is omitted.
Examples:
```bash
# Triage with local parameters
gitlink-cli workflow +triage --title "Install failed on Windows" --body "go install failed with error" --format table
# Triage with JSON output
gitlink-cli workflow +triage --title "Token leaked in logs" --body "The access token appears in command output" --format json
# Triage with Chinese markdown output
gitlink-cli workflow +triage \
--title "安装失败,无法登录" \
--body "运行命令时报错" \
--lang zh-CN \
--format markdown
# Triage from a local JSON file
gitlink-cli workflow +triage --from shortcuts/workflow/testdata/issue_bug.json --format json
# Triage by read-only GitLink fetch
gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli --state open --limit 5 --format table
# Health for a healthy repository
gitlink-cli workflow +health \
--repository Gitlink/gitlink-cli \
--open-issues 3 \
--open-prs 1 \
--has-readme \
--has-license \
--has-contributing \
--agent-readiness-known \
--agent-readiness-score 9 \
--format table
# Health for a risky repository
gitlink-cli workflow +health \
--repository demo/repo \
--open-issues 60 \
--stale-issues 25 \
--open-prs 12 \
--stale-prs 6 \
--recent-activity-known \
--recent-activity-days 120 \
--release-known=false \
--format json
# Health with Chinese markdown output
gitlink-cli workflow +health \
--repository Gitlink/gitlink-cli \
--open-issues 3 \
--open-prs 1 \
--has-readme \
--has-license \
--has-contributing \
--lang zh-CN \
--format markdown
# Health by read-only GitLink fetch
gitlink-cli workflow +health --owner Gitlink --repo gitlink-cli --stale-days 30 --format table
# PR review summary by read-only GitLink fetch
gitlink-cli workflow +pr-summary --owner Gitlink --repo gitlink-cli --number 1 --format markdown
# PR review summary from a local JSON file
gitlink-cli workflow +pr-summary --from shortcuts/workflow/testdata/pr_summary.json --format json
# Repository workflow report by read-only GitLink fetch
gitlink-cli workflow +repo-report --owner Gitlink --repo gitlink-cli --format markdown
# Repository workflow report from a local JSON file
gitlink-cli workflow +repo-report --from shortcuts/workflow/testdata/repo_report.json --format json
```
Output formats:
- `json` for scripts and AI Agents
- `table` for terminal review
- `markdown` for Issue comments, PR comments, release notes, and competition write-ups
Safety:
- Current workflow commands use local analysis by default and can also read GitLink data in read-only fetch mode.
- They do not modify remote GitLink data.
- They do not depend on LLM APIs.
- `workflow +pr-summary` does not comment, approve, reject, or merge pull requests.
- `workflow +repo-report` aggregates health, issue triage, and PR review summary signals without remote writes.
### Raw API
For endpoints not covered by shortcuts, use the Raw API directly:
```bash
# GET request
gitlink-cli api GET /users/me
# POST request
gitlink-cli api POST /Gitlink/forgeplus/issues --body '{"subject":"test","description":"..."}'
# POST request with body from a file
gitlink-cli api POST /Gitlink/forgeplus/issues --body-file issue.json
# POST request with body from stdin
Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-stdin
# With query parameters
gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
```
## Global Parameters
| Parameter | Description | Example |
|-----------|-------------|---------|
| `--owner` | Repository owner | `--owner Gitlink` |
| `--repo` | Repository name | `--repo forgeplus` |
| `--format` | Output format (json/table/yaml; workflow also supports markdown) | `--format json` |
| `--debug` | Enable debug output | `--debug` |
**Automatic context resolution:** When running inside a git repository, `--owner` and `--repo` are automatically resolved from `git remote origin`.
## Branch Conventions
gitlink-cli supports bidirectional code sync between GitHub and GitLink:
| Platform | Default Branch |
|----------|---------------|
| GitHub | `main` |
| GitLink | `master` |
**Push to GitLink from local:**
```bash
# Method 1: Use git command directly
git push gitlink main:master
# Method 2: Configure git remote
git config remote.gitlink.push refs/heads/main:refs/heads/master
git push gitlink
```
---
## AI Agent Skills
The `skills/` directory contains Agent Skill files for AI-automated GitLink operations.
`skills/` 目录含 **37 个 Skill**(官方 11 + 新增 26分类核心基础 / 命令封装 / 智能化场景代码审查·分拣·Release·健康度·合规·新人引导/ 端到端编排 / 科研辅助。兼容 Claude Code / OpenClaw / Cursor。完整说明见 [skills/README.md](./skills/README.md)。
See [skills/README.md](skills/README.md) for details.
---
| Skill | Description |
|-------|-------------|
| `gitlink-shared` | Authentication, global parameters, safety rules, API notes |
| `gitlink-repo` | Repository operations (create, view, delete, fork, etc.) |
| `gitlink-issue` | Issue operations (create, update, close, comment, etc.) |
| `gitlink-pr` | Pull request operations (create, merge, review, etc.) |
| `gitlink-member` | Repository member and invite link management |
| `gitlink-branch` | Branch management (create, delete, list, protect, unprotect) |
| `gitlink-release` | Release management (create, view, delete, etc.) |
| `gitlink-ci` | CI/CD operations (builds, logs, etc.) |
| `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) |
| `gitlink-search` | Search (repositories, users, etc.) |
| `gitlink-org` | Organization management (members, teams, etc.) |
| `gitlink-user` | User management (profile info, etc.) |
| `gitlink-pm` | Project management (sprints, kanban, weekly reports, etc.) |
| `gitlink-workflow` | AI-powered workflows (issue triage, PR review, release notes, etc.) |
## 科研智能服务 gitlink-web
## Project Structure
基于 Flask + DeepSeek API + gitlink-cli 的 Web 服务14 个 Skill核心 9 + 科研实验室 5输入研究主题或仓库 → 自动调 gitlink-cli 拉数据 → DeepSeek 生成结构化报告。
🌐 在线:<http://121.41.212.97/skills/> 代码:[gitlink-web/](./gitlink-web/README.md) 验证报告:[作品提交/子任务四](../../作品提交/子任务四/科研场景验证报告.md)
---
## 成果落地(竞赛核心)
向真正上游 `gitlink/gitlink-cli` 提交细粒度 PR每功能一个特性分支 = 功能代码+测试+文档+变更说明),共 13 个。详见 [docs/UPSTREAM_PRS.md](./docs/UPSTREAM_PRS.md)
- **子任务一 CLI5**export / pm / notification / alias·browse·status / capability
- **子任务二 Skill2**release-notes / compliance-check
- **子任务三 编排 Skill5**community-ops / pr-gate / project-bootstrap / multi-repo-ops / contributor-growth
- **子任务四 科研服务1**gitlink-web
---
## 项目结构
```
gitlink-cli/
├── cmd/ # Cobra command definitions
│ ├── root.go # Root command + global flags
│ ├── auth/ # Authentication commands
│ ├── api/ # Raw API commands
│ ├── config/ # Configuration commands
│ └── cmdutil/ # Global utilities
├── internal/ # Internal packages
│ ├── auth/ # Login, token storage, transport
│ ├── client/ # HTTP client + pagination
│ ├── config/ # Config file management
│ ├── context/ # Git remote resolution
│ └── output/ # Envelope + formatter
├── shortcuts/ # Shortcut implementations
│ ├── common/ # Framework (types, runner)
│ ├── repo/ # Repository shortcuts
│ ├── issue/ # Issue shortcuts
│ ├── pr/ # PR shortcuts
│ ├── member/ # Repository member shortcuts
│ ├── branch/ # Branch shortcuts
│ ├── release/ # Release shortcuts
│ ├── org/ # Organization shortcuts
│ ├── ci/ # CI shortcuts
│ ├── pipeline/ # Pipeline shortcuts
│ ├── search/ # Search shortcuts
│ ├── user/ # User shortcuts
│ └── register.go # Registration entry point
├── skills/ # AI Agent Skills
│ ├── README.md # Skills guide
│ ├── gitlink-shared/ # Shared rules
│ ├── gitlink-repo/ # Repository skill
│ ├── gitlink-issue/ # Issue skill
│ ├── gitlink-pr/ # PR skill
│ ├── gitlink-pm/ # Project management skill
│ └── ...
├── doc/ # Design documents
│ ├── Design.md
│ ├── CODE_SYNC_STRATEGY_FINAL.md
│ └── ...
├── main.go
├── Makefile
├── go.mod
└── README.md
├── cmd/ # Cobra 命令root/auth/api/config/alias/browse/status/show/demo/...
├── internal/ # 内部包auth/client/config/context/output/capability/i18n/demo
├── shortcuts/ # 21 个命令组repo/issue/pr/...pm/wiki/export/notification/capability/compare/workflow/...
├── skills/ # 37 个 AI Agent Skill11 官方 + 26 新增)
├── gitlink-web/ # 子任务四 科研智能服务Flask + DeepSeek
├── examples/workflows/ # 子任务三 端到端工作流community-ops-automation
├── .devops/ # 建木 DevOps 流水线ci/build/gitlink-web
├── doc/ # 设计文档 + commands/ 命令文档 + changes/ 变更说明
├── docs/ # 报告 + 复现指南 + 上游 PR 清单
├── REPRODUCE.md # 评委复现手册
└── main.go / Makefile / go.mod
```
## Documentation
---
- [Skills Guide](skills/README.md) — AI Agent Skills detailed documentation
- [Design Document](doc/design.md) — Architecture design and development plan
## 文档导航
- 📖 [Skills 指南](./skills/README.md) · 🔧 [设计文档](./doc/design.md) · 📚 [命令帮助](./doc/commands/) · 📝 [变更说明](./doc/changes/)
- 🔄 [端到端工作流](./examples/workflows/community-ops-automation/) · 🔬 [科研服务](./gitlink-web/README.md)
- 🎓 [课程报告](./docs/reports/README.md)(软件分析建模 / 新需求构思 / 变更影响分析)
- ✅ [复现指南](./REPRODUCE.md) · 🏆 [上游 PR 清单](./docs/UPSTREAM_PRS.md)
---
## FAQ
### Q: How do I use gitlink-cli in scripts?
Use the `GITLINK_TOKEN` environment variable + `--format json` for structured output:
**Q脚本/CI 中怎么用?** 设 `GITLINK_TOKEN` 环境变量 + `--format json`
```bash
export GITLINK_TOKEN="your-private-token"
export GITLINK_TOKEN="your-token"
gitlink-cli repo +list --format json | jq '.data.projects[] | .name'
```
### Q: How does automatic owner/repo resolution work?
**Qowner/repo 自动解析?** 在 git 仓库内运行,自动从 `git remote origin` 解析。
When running inside a git repository, the CLI automatically resolves `--owner` and `--repo` from `git remote origin`:
**Q某命令在 `--help` 标 ⚠?** 该模块后端暂不支持,`gitlink-cli capability +check` 查看详情。
```bash
cd ~/my-gitlink-project
gitlink-cli issue +list # Automatically uses the current repository
```
**Q完整 API 参考?** 见 [skills/gitlink-shared/REFERENCE.md](./skills/gitlink-shared/REFERENCE.md) 与 [GitLink OpenAPI](https://apifox.com/apidoc/shared-da30afb0-9d2e-429b-a4bc-a83209e06021)。
### Q: What if my token expires?
---
Re-authenticate:
## 相关项目
```bash
# Username/password login
gitlink-cli auth login
# Or use a private token (generate at GitLink web → Settings → Private Tokens)
gitlink-cli auth login --token
```
### Q: How do I use gitlink-cli in CI/CD or non-interactive environments (e.g. Trae sandbox)?
Set the `GITLINK_TOKEN` environment variable — no `auth login` needed:
```bash
export GITLINK_TOKEN="your-private-token"
gitlink-cli repo +list # Ready to use
gitlink-cli auth status # Shows "✓ Logged in via GITLINK_TOKEN environment variable"
```
Priority: `GITLINK_TOKEN` env var > keyring/file stored token. When the env var is not set, the original interactive login flow works as before.
### Q: What if npm installs successfully but `gitlink-cli` reports a missing binary?
Reinstall first:
```bash
npm install -g @gitlink-ai/cli
```
If the error persists, check whether the release page contains the asset for your platform,
for example `gitlink-cli_<version>_windows_amd64.zip` on Windows x64.
You can also download the binary manually from the release page or build from source with `go install .`.
### Q: Where are credentials stored on Windows?
gitlink-cli uses Windows Credential Manager for secure token storage. If Credential Manager is unavailable, it automatically falls back to file storage (`~/.config/gitlink-cli/credentials`).
### Q: Where can I find the full API reference?
See [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md).
- 上游主仓库:[gitlink/gitlink-cli](https://www.gitlink.org.cn/gitlink/gitlink-cli)
- 大赛主页:[www.gitlink.org.cn/competitions/2026](https://www.gitlink.org.cn/competitions/2026)
## License
[MulanPSL-2.0](https://license.coscl.org.cn/MulanPSL2)
[MulanPSL-2.0](https://license.coscl.org.cn/mulanpsl2)

View File

@ -1,595 +1,266 @@
# gitlink-cli
# gitlink-clijiangtx 小组·竞赛增强版)
[![GitLink](https://img.shields.io/badge/GitLink-Gitlink%2Fgitlink--cli-green)](https://www.gitlink.org.cn/Gitlink/gitlink-cli)
[![License](https://img.shields.io/badge/License-MulanPSL--2.0-blue.svg)](https://license.coscl.org.cn/MulanPSL2)
[![GitLink](https://img.shields.io/badge/GitLink-jiangtx%2Fgitlink--cli-green)](https://www.gitlink.org.cn/jiangtx/gitlink-cli)
[![上游](https://img.shields.io/badge/upstream-gitlink%2Fgitlink--cli-blue)](https://www.gitlink.org.cn/gitlink/gitlink-cli)
[![License](https://img.shields.io/badge/License-MulanPSL--2.0-blue.svg)](https://license.coscl.org.cn/mulanpsl2)
[![Go Version](https://img.shields.io/badge/Go-1.26%2B-blue.svg)](https://golang.org)
[![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli)
[![CI](https://img.shields.io/badge/CI-%E5%BB%BA%E6%9C%A8-brightgreen)](./.devops/ci.yml)
[GitLink确实开源](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**覆盖仓库管理、Issue 追踪、Pull Request、Webhook、成员协作、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 AI Agent [Skills](./skills/)。
> 本仓库是 [gitlink/gitlink-cli](https://www.gitlink.org.cn/gitlink/gitlink-cli) 的竞赛 Fork由 **jiangtx 小组**为《软件演化与运维》进阶任务 + GitLink 大赛开发。
> 在官方 **40+ 命令 / 11 Skill** 基础上,新增 **12 个命令模块 / 26 个 Skill / 端到端自动化工作流 / 科研智能服务**,覆盖开发者协作与科研辅助两大场景,兼容 Claude Code / OpenClaw / Cursor 等 AI Agent。
**[English](./README.md)**
🌐 **科研服务在线演示**<http://121.41.212.97/skills/>Flask + DeepSeek + gitlink-cli14 个 Skill
[安装](#安装与快速上手) · [AI Agent Skills](#ai-agent-skills) · [认证](#配置与使用) · [命令](#使用示例) · [贡献](#相关项目)
[安装](#安装与快速开始) · [功能矩阵](#功能矩阵) · [子任务成果](#四个子任务成果) · [Skills](./skills/README.md) · [复现指南](./REPRODUCE.md) · [课程报告](./docs/reports/README.md) · [上游 PR](./docs/UPSTREAM_PRS.md)
## 贡献者
---
<div style="display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-start;">
<div align="center">
<a href="https://www.gitlink.org.cn/wangyue111" title="wangyue111"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/W/43_254_70/120.png" width="40" height="40" alt="wangyue111" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/wangyue111">wangyue111</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/wbtiger" title="tigerwang"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/T/14_168_39/120.png" width="40" height="40" alt="wbtiger" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/wbtiger">wbtiger</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/Mengz" title="Mengz"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/M/166_152_185/120.png" width="40" height="40" alt="Mengz" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/Mengz">Mengz</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/yangsai" title="杨赛"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/Y/94_150_149/120.png" width="40" height="40" alt="yangsai" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/yangsai">yangsai</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/mengcheng" title="camelliamc"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/M/206_114_54/120.png" width="40" height="40" alt="mengcheng" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/mengcheng">mengcheng</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/muel" title="赵奕程"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/Z/144_206_212/120.png" width="40" height="40" alt="muel" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/muel">muel</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/Leo77" title="Leo77"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/L/173_120_149/120.png" width="40" height="40" alt="Leo77" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/Leo77">Leo77</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/yingjie" title="yingjie"><img src="https://www.gitlink.org.cn/images/avatars/User/145288?t=1765791899" width="40" height="40" alt="yingjie" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/yingjie">yingjie</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/topshare" title="Kevin Zhang"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/K/65_152_142/120.png" width="40" height="40" alt="topshare" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/topshare">topshare</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/dtwdtw" title="dtwdtw"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/D/53_166_51/120.png" width="40" height="40" alt="dtwdtw" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/dtwdtw">dtwdtw</a></sub>
</div>
<div align="center">
<a href="https://www.gitlink.org.cn/recorder" title="recorder"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/R/141_201_87/120.png" width="40" height="40" alt="recorder" style="border-radius: 50%;"></a>
<br><sub><a href="https://www.gitlink.org.cn/recorder">recorder</a></sub>
</div>
</div>
## 小组成员
## 为什么选择 gitlink-cli
| 成员 | 主责领域 |
|---|---|
| wyx / wyxfzgg | pm / wiki / export / alias·browse·status 命令、安装脚本、DevOps 流水线、编排 Skill |
| 林迪文lindiwen23 | notification / search / ci / org 增强、gitlink-web 科研服务、research-tracker 等 Skill |
| whzy | workflow 命令、i18n 本地化、编排 Skill、show/demo 离线演示 |
| wangyue789 | pipeline 命令 |
| Mengz | webhook 命令 |
| 蒋天翔jiangtx | repo/pr/user 增强、capability、Skill 收录协调 |
| 何开元 | label 命令 |
- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/),兼容 Claude Code — Agent 零配置即可操作 GitLink
- **广泛覆盖** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户等常用工作流均提供高层命令
- **AI 友好 & 优化** — 每条命令都经过真实 Agent 测试,简洁参数、智能默认值、结构化输出
- **跨平台** — macOS、Linux、Windows (x64/arm64) 全支持,`npm` 一条命令安装
- **开源零门槛** — 木兰宽松许可证第2版MulanPSL-2.0`npm install` 即用
- **3 分钟上手** — 交互式登录或 `GITLINK_TOKEN` 环境变量,从安装到首次 API 调用仅需 3 步
- **安全可控** — OS 原生 keychain 凭证存储,`GITLINK_TOKEN` 环境变量支持 CI/CD 和非交互环境,自动 git remote 上下文解析
- **三层架构** — Shortcuts人+AI友好→ Raw API全覆盖→ Config配置管理
> 个人工作量以 GitLink 提交记录为准(`git shortlog -sn`)。
## 功能一览
---
| 分类 | 能力 |
|------|------|
| 📦 仓库 | 列出、创建、Fork、删除仓库查看仓库信息 |
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
| 🔀 PR | 创建、合并、Review Pull Request查看变更文件 |
| 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 |
| 🌿 分支 | 创建、删除、保护分支 |
| 🏷️ 发布 | 创建、查看、删除 Release |
| 🏢 组织 | 管理组织、成员、团队 |
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
| 🔍 搜索 | 搜索仓库、用户 |
| 👤 用户 | 查看用户资料和信息 |
| 📋 项目管理 | Sprint 管理、看板、周报 |
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
## 为什么用 gitlink-cli竞赛增强版
## 安装与快速上手
- **Agent 原生** — 内置结构化 [Skills](./skills/)Claude Code / OpenClaw / Cursor 等 Agent 零配置即可操作 GitLink。
- **覆盖广** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户、**项目管理(PM)、Wiki、数据导出、通知、能力探测、对比、工作流分析** 全覆盖。
- **批量操作** — 批量关闭 Issue、批量更新 Issue 状态、批量添加成员(支持 CSV + `--dry-run` 预览)。
- **科研赋能** — 科研智能服务覆盖热点追踪 / 项目洞悉 / 合规检查 / 协作匹配 / 进度预警全链路。
- **端到端工作流** — 社区运营、代码质量看门人、项目一键初始化、多仓库协同、贡献者成长等编排 Skill。
- **跨平台** — macOS / Linux / Windowsx64/arm64`npm install -g @gitlink-ai/cli` 一键安装。
- **三层架构** — Shortcuts& AI 友好)→ Raw API全覆盖→ Config配置管理
### 前置条件
---
- Node.js 14+`npm`/`npx`)— 用于 npm 安装
- 支持平台macOS、Linux、Windowsx64/arm64
- Go 1.26+ — 仅从源码构建时需要
## 功能矩阵
### 快速上手(人类用户)
**21 个命令组(~129 子命令)**
> **AI 助手请注意:** 如果你是帮助用户安装的 AI Agent请直接跳到 [快速上手AI Agent](#快速上手ai-agent),其中包含你需要完成的所有步骤。
| 命令组 | 说明 | 本组新增 |
|---|---|:-:|
| repo / issue / pr / release / branch / user / org / search / ci | 官方核心命令含本组增强repo 文件操作、issue 批量、pr commits/versions 等) | 部分 |
| **pm** | 项目管理:仪表盘/Sprint/周报/标签/流水线/运行 | ✨ |
| **wiki** | Wiki 页面pages/get/create/update/delete | ✨ |
| **export** | 数据导出issues/prs/contributors → CSV/JSON | ✨ |
| **notification** | 通知list/read/read-all/watch | ✨ |
| **capability** | 后端 API 能力探测(跨实例兼容) | ✨ |
| **compare** | 分支/标签/commit 对比 | ✨ |
| **workflow** | 本地规则工作流triage/health/repo-report/pr-summary | ✨ |
| label / member / milestone / pipeline / webhook | 标签 / 成员(含批量) / 里程碑 / 流水线 / Webhook | ✨ |
| alias / browse / status | 开发者体验:别名 / 浏览器跳转 / 状态 | ✨ |
| api / auth / config / show / demo / completion / version | 基础设施 + 离线演示 | 部分 |
#### 安装
**37 个 AI Agent Skill**(官方 11 + 新增 26 · 详见 [skills/README.md](./skills/README.md)
**端到端自动化工作流** · 详见 [examples/workflows/](./examples/workflows/)
**科研智能服务** · 详见 [gitlink-web/](./gitlink-web/README.md)
选择以下**任一**方式:
---
**方式 1 — 从 npm 安装(推荐):**
## 安装与快速开始
### 要求
- Node.js 14+`npm`/`npx`)用于 npm 安装Go 1.26+ 用于源码构建;支持 macOS/Linux/Windows。
### 安装
```bash
# 安装 CLI
# 方式 Anpm推荐一条命令装好 CLI + Skills
npm install -g @gitlink-ai/cli
# 安装 CLI Skill必须全平台通用
gitlink-cli-install-skills
# 也可使用 npx 安装 Skill
npx skills add ccfos/gitlink-cli/skills -y -g
# 方式 B源码构建需 Go 1.26+
git clone https://www.gitlink.org.cn/jiangtx/gitlink-cli.git
cd gitlink-cli && go build -o gitlink-cli . # Windows: go build -o gitlink-cli.exe .
```
**方式 2 — 从源码构建:**
需要 Go 1.26+。
### 配置与登录
```bash
git clone https://www.gitlink.org.cn/Gitlink/gitlink-cli.git
cd gitlink-cli
make install
# 安装 CLI Skill必须
npx skills add ./skills -y -g
gitlink-cli config init # 一次性引导配置
gitlink-cli auth login # 交互登录(账号密码,推荐)
gitlink-cli auth login --token # 或粘贴私有 token
export GITLINK_TOKEN="your-token" # 或设环境变量CI/CD、非交互
gitlink-cli auth status # 确认登录
```
> **Windows 用户注意:** 请在 PowerShell 或 CMD 中运行 `npm install -g @gitlink-ai/cli`。从源码构建请使用 `go install .` 代替 `make install`
#### 配置与使用
### 3 分钟体验
```bash
# 1. 配置(首次使用,交互式引导)
gitlink-cli config init
# 2. 登录(任选其一)
gitlink-cli auth login # 用户名密码(推荐)
gitlink-cli auth login --token # 或粘贴私人令牌
export GITLINK_TOKEN="your-token" # 或设置环境变量(适用于 CI/CD、非交互环境
# 3. 开始使用
gitlink-cli repo +list
gitlink-cli repo +info --owner Gitlink --repo gitlink-cli # 仓库信息
gitlink-cli issue +list --owner Gitlink --repo gitlink-cli # Issue 列表
gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli # AI 分拣
gitlink-cli export +contributors --owner Gitlink --repo gitlink-cli --format json # 导出
gitlink-cli capability +check # 后端能力探测
```
### 快速上手AI Agent
> 以下步骤面向 AI Agent。部分步骤需要用户在浏览器中完成操作。
**第 1 步 — 安装**
```bash
# 安装 CLI
npm install -g @gitlink-ai/cli
# 安装 CLI Skill必须全平台通用
gitlink-cli-install-skills
```
**第 2 步 — 配置**
```bash
gitlink-cli config init
```
**第 3 步 — 登录**
交互环境:
```bash
gitlink-cli auth login
```
非交互环境CI/CD、Trae 沙箱、MCP 等):
```bash
export GITLINK_TOKEN="your-private-token"
```
> 获取私人令牌GitLink 网页端 → 个人设置 → 私人令牌。
**第 4 步 — 验证**
```bash
gitlink-cli user +me
```
---
## 使用示例
### 仓库操作
### 新增命令(本组贡献)
```bash
# 列出仓库
gitlink-cli repo +list
# 项目管理
gitlink-cli pm +dashboards --owner jiangtx --repo gitlink-cli
gitlink-cli pm +weekly --owner jiangtx --repo gitlink-cli
# 查看仓库信息
gitlink-cli repo +info --owner Gitlink --repo forgeplus
# Wiki 管理
gitlink-cli wiki +pages --owner jiangtx --repo gitlink-cli
gitlink-cli wiki +create --owner jiangtx --repo gitlink-cli --title "周报" --content "..."
# 读取仓库 README
gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
# 数据导出CSV/JSON科研/报表数据源)
gitlink-cli export +issues --owner jiangtx --repo gitlink-cli --format csv --output issues.csv
gitlink-cli export +contributors --owner jiangtx --repo gitlink-cli --format json
# 创建仓库
gitlink-cli repo +create -n my-project -d "项目描述"
# 通知管理
gitlink-cli notification +list
gitlink-cli notification +watch --owner jiangtx --repo gitlink-cli
# Fork 仓库
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
# 能力探测 + 对比 + 工作流分析
gitlink-cli capability +check
gitlink-cli compare +view --head feature --base master
gitlink-cli workflow +health --repository Gitlink/gitlink-cli --open-issues 3 --open-prs 1 --has-readme --has-license
# 批量操作
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli --numbers 1,2,3 --dry-run
gitlink-cli member +batch-add --owner jiangtx --repo gitlink-cli --user-ids 101,102
# 开发者体验
gitlink-cli alias +set rl "repo +list"
gitlink-cli browse issues/42
gitlink-cli status
```
### Webhook 管理
### 核心命令(沿用上游)
```bash
# 列出 webhook
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
# 创建 webhook
gitlink-cli webhook +create --owner Gitlink --repo forgeplus \
--url https://example.com/hook --events push,create
# 测试 webhook
gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
# 查看 webhook 投递任务
gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68
gitlink-cli repo +list / +info / +create / +fork
gitlink-cli issue +create / +list / +view / +close / +comment
gitlink-cli pr +list / +create / +view / +merge / +review / +versions
gitlink-cli release +list / +create / +view
gitlink-cli branch +list / +create / +protect
gitlink-cli webhook +list / +create / +test
gitlink-cli ci +builds / +logs / +restart
gitlink-cli pipeline +runs / +run / +logs
gitlink-cli search +repos / +users / +code / +issues
gitlink-cli api GET /users/me # Raw API未封装接口
```
### 成员管理
### 全局参数
```bash
# 列出仓库成员
gitlink-cli member +list --owner Gitlink --repo forgeplus
| 参数 | 说明 |
|---|---|
| `--owner` / `--repo` | 仓库所有者 / 名称git 仓库内自动从 remote 解析) |
| `--format` | 输出格式 json / table / yamlworkflow 另支持 markdown |
| `--debug` | 调试输出 |
# 添加成员
gitlink-cli member +add --owner Gitlink --repo forgeplus --user-id 101
---
# 预览批量添加成员,不修改数据
gitlink-cli member +batch-add --owner Gitlink --repo forgeplus --user-ids 101,102 --dry-run
## 四个子任务成果
# 从 CSV 文件批量添加成员
gitlink-cli member +batch-add --owner Gitlink --repo forgeplus --from members.csv
### 子任务一·扩展 CLI50%
新增 12 个命令模块、~61 个子命令、3 条批量操作。每个新模块含源码 + 单元测试 + 命令帮助文档 + 变更说明。详见 [doc/commands/](./doc/commands/)、[doc/changes/](./doc/changes/)。
# 调整成员权限
gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role Developer
### 子任务二·开发 Skill20%
新增 26 个 Skill覆盖智能代码审查 / Issue 分拣 / Release Notes / 项目健康度 / 许可证合规 / 新人引导 6 大场景。6 个已通过 Agent 平台验证。详见 [skills/README.md](./skills/README.md)。
# 生成邀请链接
gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true
```
### 子任务三·端到端自动化工作流20%
[community-ops-automation](./examples/workflows/community-ops-automation/):串联 `repo+info → issue+list → pr+list → release+list → issue+comment` 共 45 条命令,在 `Gitlink/gitlink-cli``jiangtx/gitlink-cli` 真实项目跑通,含 Python/PowerShell/bash 脚本、JSON 配置、单元测试、SVG 架构图、验证记录。另有 5 个编排 Skill社区运营/代码质量看门人/项目初始化/多仓库协同/贡献者成长)。
### Issue 管理
### 子任务四·应用 GitLink 辅助科研(加分)
[科研智能服务 gitlink-web](./gitlink-web/README.md) + [gitlink-research-tracker](./skills/gitlink-research-tracker/) Skill技术调研多关键词搜索→成熟度评分→趋势→选型覆盖热点追踪 / 项目洞悉 / 合规检查 / 协作匹配 / 进度跟踪 5 个科研场景。
```bash
# 列出 Issue
gitlink-cli issue +list --owner Gitlink --repo forgeplus
### DevOps 流水线10%
[.devops/](./.devops/) 建木流水线:`ci.yml`test/vet/fmt· `build.yml`(编译+命令注册验证)· `gitlink-web.yml`科研服务部署。master 合并自动触发。
# 创建 Issue
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" -b "复现步骤..."
# 创建带元数据的 Issue
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" --priority-id 3 --tag-ids 4,5 --assigner-ids 7
# 查看 Issue
gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123
# 更新 Issue 元数据
gitlink-cli issue +update --owner Gitlink --repo forgeplus --number 123 --priority-id 4 --branch bugfix/login --due-date 2026-06-15
# 关闭 Issue
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
# 预览批量关闭,不修改数据
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
# 从 CSV 文件批量关闭 Issue
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
# 添加评论
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复"
# 列出 Issue 负责人
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
# 列出 Issue 发布人
gitlink-cli issue +authors --owner Gitlink --repo forgeplus
# 列出 Issue 优先级
gitlink-cli issue +priorities --owner Gitlink --repo forgeplus
# 列出 Issue 标签
gitlink-cli issue +tags --owner Gitlink --repo forgeplus --only-name
# 列出 Issue 状态
gitlink-cli issue +statuses --owner Gitlink --repo forgeplus
```
`issue +view`、`issue +update`、`issue +close` 和 `issue +comment` 推荐使用
`--number` / `-n` 传网页 URL 中的 Issue 编号。`--id` / `-i` 是同一网页 Issue
编号的兼容别名,不是数据库内部 ID。
### 标签管理
```bash
# 列出 Issue 标签
gitlink-cli label +list --owner Gitlink --repo forgeplus
# 按关键词筛选标签
gitlink-cli label +list --owner Gitlink --repo forgeplus -k bug
# 创建标签(颜色默认 #1E90FF
gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "功能缺陷" -c "#FF0000"
# 更新标签(未指定的字段会被保留)
gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00"
# 删除标签
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
```
### Pull Request
```bash
# 列出 PR
gitlink-cli pr +list --owner Gitlink --repo forgeplus
# 创建 PR同仓库分支
gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 搜索功能" --head feature/search --base master
# 创建 PR从 Fork 仓库)
gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 新功能" --head your_username/forgeplus:feature/my-feature --base master
# 查看 PR
gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42
# 合并 PR
gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42
# 重开已关闭的 PR
gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42
# 查看 PR 变更文件
gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42
# 查看 PR patchset/version 列表
gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42
# 查看指定 patchset/version diff
gitlink-cli pr +version-diff --owner Gitlink --repo forgeplus -i 42 --version-id 16040
# 查看 PR 审查记录
gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42
# 创建 PR 审查(支持 dry-run 预览)
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM" --dry-run
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM"
```
### 发布管理
```bash
# 列出 Release
gitlink-cli release +list --owner Gitlink --repo forgeplus
# 创建 Release
gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.0 正式版" -b "更新内容..."
# 查看 Release
gitlink-cli release +view --owner Gitlink --repo forgeplus -i <version_id>
```
### 流水线管理
```bash
# 列出平台流水线
gitlink-cli pipeline +list --owner-id 123 --page 1 --limit 20
# 列出仓库流水线运行记录
gitlink-cli pipeline +runs --owner Gitlink --repo forgeplus --ref master --workflow build.yml
# 运行流水线工作流,先用 dry-run 预览请求
gitlink-cli pipeline +run --owner Gitlink --repo forgeplus --ref master --workflow build.yml --dry-run
# 查看流水线详情、日志和运行结果
gitlink-cli pipeline +view --owner Gitlink --repo forgeplus --id 7
gitlink-cli pipeline +logs --owner Gitlink --repo forgeplus --run-id 99 --id 7 --index 43
gitlink-cli pipeline +results --owner Gitlink --repo forgeplus --run-id 99
# 启停或删除流水线工作流,写入/删除前先预览
gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow build.yml --dry-run
gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run
```
### 搜索
```bash
# 搜索仓库
gitlink-cli search +repos -k "machine learning"
# 搜索用户
gitlink-cli search +users -k "zhangsan"
```
### Raw API
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:
```bash
# GET 请求
gitlink-cli api GET /users/me
# POST 请求
gitlink-cli api POST /Gitlink/forgeplus/issues --body '{"subject":"test","description":"..."}'
# 从文件读取 JSON body
gitlink-cli api POST /Gitlink/forgeplus/issues --body-file issue.json
# 从 stdin 读取 JSON body
Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-stdin
# 带查询参数
gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
```
## 全局参数
| 参数 | 说明 | 示例 |
|------|------|------|
| `--owner` | 仓库所有者 | `--owner Gitlink` |
| `--repo` | 仓库名称 | `--repo forgeplus` |
| `--format` | 输出格式json/table/yaml | `--format json` |
| `--debug` | 启用调试输出 | `--debug` |
**自动上下文解析**:在 git 仓库目录下,`--owner` 和 `--repo` 会自动从 `git remote origin` 解析。
## 分支约定
gitlink-cli 支持 GitHub 和 GitLink 的代码双向同步:
| 平台 | 主分支 |
|------|--------|
| GitHub | `main` |
| GitLink | `master` |
**本地 push 到 GitLink**
```bash
# 方式 1使用 git 命令
git push gitlink main:master
# 方式 2配置 git remote
git config remote.gitlink.push refs/heads/main:refs/heads/master
git push gitlink
```
---
## AI Agent Skills
`skills/` 目录包含 Claude Code Agent Skill 文件,支持 AI 自动化操作 GitLink 平台
`skills/` 目录含 **37 个 Skill**(官方 11 + 新增 26分类核心基础 / 命令封装 / 智能化场景代码审查·分拣·Release·健康度·合规·新人引导/ 端到端编排 / 科研辅助。兼容 Claude Code / OpenClaw / Cursor。完整说明见 [skills/README.md](./skills/README.md)。
详见 [skills/README.md](skills/README.md)
---
| Skill | 说明 |
|-------|------|
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
| `gitlink-repo` | 仓库操作创建、查看、删除、Fork 等) |
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
| `gitlink-pr` | Pull Request 操作创建、合并、Review 等) |
| `gitlink-member` | 仓库成员与邀请链接管理 |
| `gitlink-release` | 发布管理(创建、查看、删除等) |
| `gitlink-org` | 组织管理(成员、团队等) |
| `gitlink-ci` | CI/CD 操作(构建、日志等) |
| `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) |
| `gitlink-search` | 搜索功能(仓库、用户等) |
| `gitlink-user` | 用户管理(个人信息等) |
| `gitlink-pm` | 项目管理Sprint、看板、周报等 |
| `gitlink-workflow` | AI 自动化工作流Issue 分类、PR Review、Release Notes 等) |
## 科研智能服务 gitlink-web
基于 Flask + DeepSeek API + gitlink-cli 的 Web 服务14 个 Skill核心 9 + 科研实验室 5输入研究主题或仓库 → 自动调 gitlink-cli 拉数据 → DeepSeek 生成结构化报告。
🌐 在线:<http://121.41.212.97/skills/> 代码:[gitlink-web/](./gitlink-web/README.md) 验证报告:[作品提交/子任务四](../../作品提交/子任务四/科研场景验证报告.md)
---
## 成果落地(竞赛核心)
向真正上游 `gitlink/gitlink-cli` 提交细粒度 PR每功能一个特性分支 = 功能代码+测试+文档+变更说明),共 13 个。详见 [docs/UPSTREAM_PRS.md](./docs/UPSTREAM_PRS.md)
- **子任务一 CLI5**export / pm / notification / alias·browse·status / capability
- **子任务二 Skill2**release-notes / compliance-check
- **子任务三 编排 Skill5**community-ops / pr-gate / project-bootstrap / multi-repo-ops / contributor-growth
- **子任务四 科研服务1**gitlink-web
---
## 项目结构
```
gitlink-cli/
├── cmd/ # Cobra 命令定义
│ ├── root.go # 根命令 + 全局 flags
│ ├── auth/ # 认证命令
│ ├── api/ # Raw API 命令
│ ├── config/ # 配置命令
│ └── cmdutil/ # 全局工具
├── internal/ # 内部包
│ ├── auth/ # 登录、Token 存储、Transport
│ ├── client/ # HTTP 客户端 + 分页
│ ├── config/ # 配置文件管理
│ ├── context/ # git remote 解析
│ └── output/ # Envelope + Formatter
├── shortcuts/ # Shortcut 实现
│ ├── common/ # 框架types, runner
│ ├── repo/ # 仓库 shortcuts
│ ├── issue/ # Issue shortcuts
│ ├── pr/ # PR shortcuts
│ ├── member/ # 仓库成员 shortcuts
│ ├── branch/ # 分支 shortcuts
│ ├── release/ # Release shortcuts
│ ├── org/ # 组织 shortcuts
│ ├── ci/ # CI shortcuts
│ ├── pipeline/ # Pipeline shortcuts
│ ├── search/ # 搜索 shortcuts
│ ├── user/ # 用户 shortcuts
│ └── register.go # 注册入口
├── skills/ # AI Agent Skills
│ ├── README.md # Skills 使用指南
│ ├── gitlink-shared/ # 共享规则
│ ├── gitlink-repo/ # 仓库 Skill
│ ├── gitlink-issue/ # Issue Skill
│ ├── gitlink-pr/ # PR Skill
│ ├── gitlink-pm/ # 项目管理 Skill
│ └── ...
├── doc/ # 设计文档
│ ├── Design.md
│ ├── CODE_SYNC_STRATEGY_FINAL.md
│ └── ...
├── main.go
├── Makefile
├── go.mod
└── README.md
├── cmd/ # Cobra 命令root/auth/api/config/alias/browse/status/show/demo/...
├── internal/ # 内部包auth/client/config/context/output/capability/i18n/demo
├── shortcuts/ # 21 个命令组repo/issue/pr/...pm/wiki/export/notification/capability/compare/workflow/...
├── skills/ # 37 个 AI Agent Skill11 官方 + 26 新增)
├── gitlink-web/ # 子任务四 科研智能服务Flask + DeepSeek
├── examples/workflows/ # 子任务三 端到端工作流community-ops-automation
├── .devops/ # 建木 DevOps 流水线ci/build/gitlink-web
├── doc/ # 设计文档 + commands/ 命令文档 + changes/ 变更说明
├── docs/ # 报告 + 复现指南 + 上游 PR 清单
├── REPRODUCE.md # 评委复现手册
└── main.go / Makefile / go.mod
```
## 文档
---
- [Skills 使用指南](skills/README.md) — AI Agent Skills 详细说明
- [设计文档](doc/design.md) — 架构设计和开发计划
## 文档导航
## 常见问题
- 📖 [Skills 指南](./skills/README.md) · 🔧 [设计文档](./doc/design.md) · 📚 [命令帮助](./doc/commands/) · 📝 [变更说明](./doc/changes/)
- 🔄 [端到端工作流](./examples/workflows/community-ops-automation/) · 🔬 [科研服务](./gitlink-web/README.md)
- 🎓 [课程报告](./docs/reports/README.md)(软件分析建模 / 新需求构思 / 变更影响分析)
- ✅ [复现指南](./REPRODUCE.md) · 🏆 [上游 PR 清单](./docs/UPSTREAM_PRS.md)
### Q: 如何在脚本中使用 gitlink-cli
---
使用 `GITLINK_TOKEN` 环境变量 + `--format json` 获取结构化输出:
## FAQ
**Q脚本/CI 中怎么用?** 设 `GITLINK_TOKEN` 环境变量 + `--format json`
```bash
export GITLINK_TOKEN="your-private-token"
export GITLINK_TOKEN="your-token"
gitlink-cli repo +list --format json | jq '.data.projects[] | .name'
```
### Q: 如何自动解析 owner/repo
**Qowner/repo 自动解析?** 在 git 仓库内运行,自动从 `git remote origin` 解析。
在 git 仓库目录下运行命令CLI 会自动从 `git remote origin` 解析:
**Q某命令在 `--help` 标 ⚠?** 该模块后端暂不支持,`gitlink-cli capability +check` 查看详情。
```bash
cd ~/my-gitlink-project
gitlink-cli issue +list # 自动使用当前仓库
```
**Q完整 API 参考?** 见 [skills/gitlink-shared/REFERENCE.md](./skills/gitlink-shared/REFERENCE.md) 与 [GitLink OpenAPI](https://apifox.com/apidoc/shared-da30afb0-9d2e-429b-a4bc-a83209e06021)。
### Q: Token 过期了怎么办?
---
重新登录:
## 相关项目
```bash
# 用户名密码登录
gitlink-cli auth login
- 上游主仓库:[gitlink/gitlink-cli](https://www.gitlink.org.cn/gitlink/gitlink-cli)
- 大赛主页:[www.gitlink.org.cn/competitions/2026](https://www.gitlink.org.cn/competitions/2026)
# 或使用私人令牌(在 GitLink 网页端 个人设置 → 私人令牌 中生成)
gitlink-cli auth login --token
```
## License
### Q: 如何在 CI/CD 或非交互环境Trae 沙箱等)中使用?
设置 `GITLINK_TOKEN` 环境变量即可,无需 `auth login`
```bash
export GITLINK_TOKEN="your-private-token"
gitlink-cli repo +list # 直接可用
gitlink-cli auth status # 显示 "✓ Logged in via GITLINK_TOKEN environment variable"
```
Token 优先级:`GITLINK_TOKEN` 环境变量 > keyring/文件存储的 token。不设置环境变量时完全兼容原有交互式登录。
### Q: npm 安装成功但 `gitlink-cli` 提示缺少二进制怎么办?
先尝试重新安装:
```bash
npm install -g @gitlink-ai/cli
```
如果仍然失败,请检查 Release 页面是否包含当前平台的资产,例如 Windows x64 对应 `gitlink-cli_<version>_windows_amd64.zip`。也可以从 Release 页面手动下载二进制,或使用 `go install .` 从源码构建。
### Q: Windows 上凭证存储在哪里?
gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Credential Manager 不可用,会自动降级到文件存储(`~/.config/gitlink-cli/credentials`)。
### Q: 如何查看完整的 API 参考?
查看 [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md)
## 许可证
[MulanPSL-2.0](https://license.coscl.org.cn/MulanPSL2)
[MulanPSL-2.0](https://license.coscl.org.cn/mulanpsl2)

139
REPRODUCE.md Normal file
View File

@ -0,0 +1,139 @@
# 复现指南REPRODUCE
> 面向评委的"照此即可复现全部成果"手册。四个部分相互独立,可按需选做。
> 全程约 1530 分钟(不含等待 Review
## 0. 前置准备
- GitLink 账号(<https://www.gitlink.org.cn> 注册)
- 任一终端macOS / Linux / Windows
- 二选一Node.js 14+npm 装 CLI或 Go 1.26+(源码构建)
- GitLink Token登录后在「个人设置 → 访问令牌」生成,或用 `auth login` 交互登录
```bash
git clone https://www.gitlink.org.cn/jiangtx/gitlink-cli.git
cd gitlink-cli
```
---
## 1. 复现 CLI 命令(子任务一)
### 1.1 安装
```bash
# 方式 Anpm
npm install -g @gitlink-ai/cli
# 方式 B源码需 Go 1.26+
go build -o gitlink-cli .
```
### 1.2 认证
```bash
gitlink-cli auth login # 交互登录CI 环境用 export GITLINK_TOKEN=xxx
gitlink-cli auth status # 确认登录
```
### 1.3 验证新命令(任选)
```bash
gitlink-cli pm --help # 项目管理6 命令)
gitlink-cli wiki --help # Wiki5 命令)
gitlink-cli export --help # 数据导出3 命令)
gitlink-cli workflow --help # 工作流分析4 命令)
gitlink-cli capability +check # 后端能力探测
# 跑一条真实命令
gitlink-cli export +contributors --owner Gitlink --repo gitlink-cli --format json
gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli --limit 10
```
### 1.4 跑单元测试
```bash
go test ./... -race # 需 Go 环境CI 已自动执行
```
---
## 2. 复现 Skill子任务二
### 2.1 加载 Skills 到 Agent 平台
`skills/` 目录拷贝到 Agent 的 skills 路径,或直接在仓库内调用:
**Claude Code**`skills/` 放到 `~/.claude/skills/` 或项目 `.claude/skills/`,重启会话。
### 2.2 验证已验证 Skill6 个有运行记录)
```bash
# 例:技术调研 Skill
# 在 Agent 对话:「调研 GitLink 上'大模型'相关的活跃项目」
# Agent 会按 skills/gitlink-research-tracker/SKILL.md 执行
# 或看现成验证记录
cat skills/gitlink-research-tracker/EXAMPLES.md # token/耗时/输出
cat skills/gitlink-contributor-insight/EXAMPLES.md
cat skills/gitlink-ci-health/EXAMPLES.md
cat skills/gitlink-issue-triage/EXAMPLES.md
cat skills/gitlink-onboarding/EXAMPLES.md
cat skills/gitlink-notification-digest/EXAMPLES.md
```
> 待验证 Skill20 个)含完整 SKILL.md按同样方式加载即可运行。
---
## 3. 复现端到端工作流(子任务三)
```bash
cd examples/workflows/community-ops-automation
# Windows
powershell -File scripts/run_demo.ps1 -Owner Gitlink -Repo gitlink-cli
# Linux/macOS需 npm 可用,脚本通过 npm exec 调 @gitlink-ai/cli
node scripts/gitlink_workflow.js --owner Gitlink --repo gitlink-cli
```
输出:`outputs/{owner}_{repo}_{timestamp}_report.md`(社区运营报告)+ `_release_notes.md`
验证证据见 `docs/verification.md`(已在 `Gitlink/gitlink-cli`、`puygob236/gitlink-cli` 两真项目跑过,含计数与时间戳)。
---
## 4. 复现科研服务(子任务四)
### 4.1 在线体验(无需自部署)
直接访问:<http://121.41.212.97/skills/>
- 技术调研:输入「大模型」→ 生成调研报告
- 科研实验室:热点追踪 / 项目洞悉 / 合规检查 / 协作匹配 / 进度跟踪
### 4.2 自部署
```bash
cd gitlink-web
pip install -r requirements.txt
export API_KEY="你的 DeepSeek Key"
export API_BASE="https://api.deepseek.com/v1"
export API_MODEL="deepseek-chat"
python app.py --port=5000
# 访问 http://localhost:5000/
```
或一键部署到服务器:`python deploy.py`paramiko SSH详见 [gitlink-web/README.md](gitlink-web/README.md))。
---
## 5. 常见问题
| 问题 | 解决 |
|---|---|
| `command not found: go` | 用 npm 方式装 CLI或装 Go 1.26+ |
| 认证失败 / 401 | `gitlink-cli auth login` 重新登录,或检查 `GITLINK_TOKEN` |
| 命令标 ⚠ | 该模块后端暂不支持,`gitlink-cli capability +check` 查看详情 |
| 工作流只支持 Windows | 用第 3 节的 node 命令在任意平台跑 |
| 科研服务报 API_KEY 未设置 | 配 `API_KEY` 环境变量DeepSeek 或兼容 OpenAI 的接口) |
## 6. 预期产物清单(复现成功后应得到)
- [ ] CLI 可运行,`--help` 列出 21 个命令组
- [ ] `go test ./...` 全绿(或 CI 绿)
- [ ] 至少 1 个 Skill 在 Agent 平台产出结果
- [ ] 工作流生成 1 份 `_report.md`
- [ ] 科研服务页面可访问并生成 1 份调研报告

158
cmd/alias/alias.go Normal file
View File

@ -0,0 +1,158 @@
package alias
import (
"fmt"
"os"
"sort"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
"github.com/gitlink-org/gitlink-cli/internal/config"
"github.com/gitlink-org/gitlink-cli/internal/output"
)
// AliasConfig represents the aliases section of the CLI config.
type AliasConfig struct {
Aliases map[string]string `yaml:"aliases,omitempty"`
}
// NewAliasCmd creates the alias command with subcommands.
func NewAliasCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "alias",
Short: "管理命令别名(把长命令变短)",
Long: `管理 gitlink-cli 的命令别名
别名允许你为常用命令创建简短的名称例如
gitlink-cli alias +set rl "repo +list"
之后可以使用: gitlink-cli rl
别名存储在 ~/.config/gitlink-cli/aliases.yaml `,
Example: ` gitlink-cli alias +list
gitlink-cli alias +set rl "repo +list"
gitlink-cli alias +set ri "repo +info --owner Gitlink --repo gitlink-cli"
gitlink-cli alias +delete rl`,
}
cmd.AddCommand(
&cobra.Command{
Use: "+list",
Short: "列出所有已定义的别名",
Long: "列出所有已定义的命令别名。如果没有任何别名,会给出创建提示。",
RunE: func(cmd *cobra.Command, args []string) error {
aliases, _ := loadAliases()
// Structured formats (json/yaml/table) route through output.Print
// so alias +list integrates with scripts and AI Agents.
if cmdutil.Format == "json" || cmdutil.Format == "yaml" || cmdutil.Format == "table" {
rows := make([]map[string]string, 0, len(aliases))
names := make([]string, 0, len(aliases))
for k := range aliases {
names = append(names, k)
}
sort.Strings(names)
for _, k := range names {
rows = append(rows, map[string]string{"name": k, "command": aliases[k]})
}
return output.Print(output.SuccessEnvelope(rows, nil), cmdutil.Format)
}
if len(aliases) == 0 {
fmt.Println("(未定义任何别名)")
fmt.Println("使用 alias +set <名称> <命令> 来创建别名")
return nil
}
names := make([]string, 0, len(aliases))
for k := range aliases {
names = append(names, k)
}
sort.Strings(names)
for _, k := range names {
fmt.Printf(" %-15s → %s\n", k, aliases[k])
}
return nil
},
},
&cobra.Command{
Use: "+set <name> <command>",
Short: "设置别名",
Long: "为一条命令设置别名。如果别名已存在,会被覆盖。",
Args: cobra.ExactArgs(2),
Example: ` gitlink-cli alias +set rl "repo +list"
gitlink-cli alias +set ri "repo +info"`,
RunE: func(cmd *cobra.Command, args []string) error {
aliases, _ := loadAliases()
aliases[args[0]] = args[1]
if err := saveAliases(aliases); err != nil {
return err
}
fmt.Printf("别名已设置: %s → %s\n", args[0], args[1])
return nil
},
},
&cobra.Command{
Use: "+delete <name>",
Short: "删除别名",
Long: "删除一个已定义的命令别名。",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
aliases, _ := loadAliases()
if _, ok := aliases[args[0]]; !ok {
return fmt.Errorf("别名 %s 不存在", args[0])
}
delete(aliases, args[0])
if err := saveAliases(aliases); err != nil {
return err
}
fmt.Printf("别名已删除: %s\n", args[0])
return nil
},
},
&cobra.Command{
Use: "+expand <name>",
Short: "展开别名查看原命令",
Long: "查看一个别名对应的原始命令。如果别名不存在则报错。",
Args: cobra.ExactArgs(1),
Example: ` gitlink-cli alias +expand rl
输出: rl repo +list`,
RunE: func(cmd *cobra.Command, args []string) error {
aliases, _ := loadAliases()
expanded, ok := aliases[args[0]]
if !ok {
return fmt.Errorf("别名 %s 不存在", args[0])
}
fmt.Printf("%s → %s\n", args[0], expanded)
return nil
},
},
)
return cmd
}
func aliasesPath() string {
return config.ConfigDir() + "/aliases.yaml"
}
func loadAliases() (map[string]string, error) {
data, err := os.ReadFile(aliasesPath())
if err != nil {
return make(map[string]string), nil
}
var ac AliasConfig
if err := yaml.Unmarshal(data, &ac); err != nil {
return make(map[string]string), nil
}
if ac.Aliases == nil {
ac.Aliases = make(map[string]string)
}
return ac.Aliases, nil
}
func saveAliases(a map[string]string) error {
data, err := yaml.Marshal(AliasConfig{Aliases: a})
if err != nil {
return err
}
os.MkdirAll(config.ConfigDir(), 0700)
return os.WriteFile(aliasesPath(), data, 0600)
}

224
cmd/alias/alias_test.go Normal file
View File

@ -0,0 +1,224 @@
package alias
import (
"io"
"os"
"strings"
"testing"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
)
func TestLoadAliasesEmpty(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
aliases, err := loadAliases()
if err != nil {
t.Fatalf("loadAliases failed: %v", err)
}
if len(aliases) != 0 {
t.Fatalf("expected empty aliases, got %d", len(aliases))
}
}
func TestSaveAndLoadAliases(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
original := map[string]string{
"rl": "repo +list",
"ri": "repo +info",
}
if err := saveAliases(original); err != nil {
t.Fatalf("saveAliases failed: %v", err)
}
loaded, err := loadAliases()
if err != nil {
t.Fatalf("loadAliases failed: %v", err)
}
if len(loaded) != 2 {
t.Fatalf("expected 2 aliases, got %d", len(loaded))
}
if loaded["rl"] != "repo +list" {
t.Errorf("expected rl -> repo +list, got %s", loaded["rl"])
}
if loaded["ri"] != "repo +info" {
t.Errorf("expected ri -> repo +info, got %s", loaded["ri"])
}
}
func TestSaveAliasesOverwrite(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
saveAliases(map[string]string{"rl": "repo +list"})
saveAliases(map[string]string{"rl": "repo +list --owner Gitlink"})
loaded, _ := loadAliases()
if loaded["rl"] != "repo +list --owner Gitlink" {
t.Errorf("alias should be overwritten, got %s", loaded["rl"])
}
}
func TestLoadAliasesInvalidYAML(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
os.WriteFile(tmpDir+"/aliases.yaml", []byte("{{invalid yaml}}"), 0600)
aliases, err := loadAliases()
if err != nil {
t.Fatalf("should not error on invalid YAML, got: %v", err)
}
if len(aliases) != 0 {
t.Fatalf("should return empty map on invalid YAML, got %d", len(aliases))
}
}
func TestNewAliasCmdStructure(t *testing.T) {
cmd := NewAliasCmd()
if cmd.Use != "alias" {
t.Errorf("expected Use 'alias', got %s", cmd.Use)
}
if !cmd.HasSubCommands() {
t.Error("alias command should have subcommands")
}
subcmds := cmd.Commands()
if len(subcmds) != 4 {
t.Fatalf("expected 4 subcommands, got %d", len(subcmds))
}
expectedUses := map[string]bool{"+list": false, "+set <name> <command>": false, "+delete <name>": false, "+expand <name>": false}
for _, sub := range subcmds {
if _, ok := expectedUses[sub.Use]; ok {
expectedUses[sub.Use] = true
}
}
for use, found := range expectedUses {
if !found {
t.Errorf("subcommand %q not found", use)
}
}
}
func TestAliasSetAndDeleteFlow(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
// 模拟 +set 操作:直接调用 saveAliases
aliases := make(map[string]string)
aliases["rl"] = "repo +list"
aliases["ri"] = "repo +info"
if err := saveAliases(aliases); err != nil {
t.Fatalf("saveAliases failed: %v", err)
}
// 验证保存成功
loaded, _ := loadAliases()
if loaded["rl"] != "repo +list" {
t.Fatalf("alias not saved correctly: %v", loaded)
}
if loaded["ri"] != "repo +info" {
t.Fatalf("alias not saved correctly: %v", loaded)
}
// 模拟 +delete 操作:删除别名后保存
delete(loaded, "rl")
if err := saveAliases(loaded); err != nil {
t.Fatalf("saveAliases after delete failed: %v", err)
}
// 验证删除成功
final, _ := loadAliases()
if _, ok := final["rl"]; ok {
t.Fatal("alias 'rl' should have been deleted")
}
if final["ri"] != "repo +info" {
t.Fatal("alias 'ri' should still exist")
}
}
func TestAliasDeleteNonExistent(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
// 空别名列表,删除不存在的别名
aliases, _ := loadAliases()
if _, ok := aliases["nonexistent"]; ok {
t.Fatal("nonexistent alias should not exist")
}
// 验证逻辑:别名不存在时不应执行删除
// 这对应 alias.go 中 if _, ok := aliases[args[0]]; !ok 的检查
}
func TestAliasesFilePath(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
expected := tmpDir + "/aliases.yaml"
got := aliasesPath()
if got != expected {
t.Errorf("expected path %s, got %s", expected, got)
}
}
func TestAliasExpandExisting(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
saveAliases(map[string]string{
"rl": "repo +list",
"ri": "repo +info",
})
aliases, _ := loadAliases()
if expanded, ok := aliases["rl"]; !ok || expanded != "repo +list" {
t.Fatalf("expected rl → repo +list, got %s", expanded)
}
if expanded, ok := aliases["ri"]; !ok || expanded != "repo +info" {
t.Fatalf("expected ri → repo +info, got %s", expanded)
}
}
func TestAliasExpandNonExistent(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
aliases, _ := loadAliases()
if _, ok := aliases["nonexistent"]; ok {
t.Fatal("nonexistent alias should not be found")
}
}
func TestAliasListJSONFormat(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
if err := saveAliases(map[string]string{"rl": "repo +list", "ri": "repo +info"}); err != nil {
t.Fatalf("save: %v", err)
}
cmdutil.Format = "json"
defer func() { cmdutil.Format = "" }()
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
root := NewAliasCmd()
root.SetArgs([]string{"+list"})
execErr := root.Execute()
w.Close()
os.Stdout = old
if execErr != nil {
t.Fatalf("execute: %v", execErr)
}
var buf strings.Builder
io.Copy(&buf, r)
out := buf.String()
for _, want := range []string{`"ok": true`, `"name"`, `"rl"`, `"repo +list"`} {
if !strings.Contains(out, want) {
t.Errorf("JSON output missing %q: %s", want, out)
}
}
}

View File

@ -3,6 +3,7 @@ package auth
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/spf13/cobra"
@ -11,6 +12,16 @@ import (
internalAuth "github.com/gitlink-org/gitlink-cli/internal/auth"
)
// setupAuthTest sets up a temporary config directory for auth tests.
// Returns the path to the config dir (.config/gitlink-cli).
func setupAuthTest(t *testing.T) string {
t.Helper()
dir := filepath.Join(t.TempDir(), ".config", "gitlink-cli")
t.Setenv("GITLINK_CONFIG_DIR", dir)
t.Setenv("HOME", filepath.Dir(filepath.Dir(dir))) // for backward compat
return dir
}
func TestEnvTokenVar(t *testing.T) {
if envTokenVar != "GITLINK_TOKEN" {
t.Fatalf("envTokenVar = %q, want GITLINK_TOKEN", envTokenVar)
@ -61,7 +72,7 @@ func TestLoginTokenFlag(t *testing.T) {
func TestStatusCmdNotLoggedIn(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
t.Setenv("HOME", t.TempDir())
setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "")
_ = internalAuth.DeleteToken()
@ -76,7 +87,7 @@ func TestStatusCmdNotLoggedIn(t *testing.T) {
func TestStatusCmdEnvToken(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
t.Setenv("HOME", t.TempDir())
setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "env-token-123")
_ = internalAuth.DeleteToken()
@ -86,12 +97,11 @@ func TestStatusCmdEnvToken(t *testing.T) {
func TestStatusCmdStoredToken(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
dir := t.TempDir()
t.Setenv("HOME", dir)
dir := setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "")
os.MkdirAll(dir+"/.config/gitlink-cli", 0700)
os.WriteFile(dir+"/.config/gitlink-cli/credentials", []byte("cookie:test=abc"), 0600)
os.MkdirAll(dir, 0700)
os.WriteFile(filepath.Join(dir, "credentials"), []byte("cookie:test=abc"), 0600)
cmd := findSub(NewAuthCmd(), "status")
cmd.RunE(cmd, nil)
@ -99,12 +109,11 @@ func TestStatusCmdStoredToken(t *testing.T) {
func TestStatusCmdEnvAndStoredToken(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
dir := t.TempDir()
t.Setenv("HOME", dir)
dir := setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "env-token")
os.MkdirAll(dir+"/.config/gitlink-cli", 0700)
os.WriteFile(dir+"/.config/gitlink-cli/credentials", []byte("stored-token"), 0600)
os.MkdirAll(dir, 0700)
os.WriteFile(filepath.Join(dir, "credentials"), []byte("stored-token"), 0600)
cmd := findSub(NewAuthCmd(), "status")
if err := cmd.RunE(cmd, nil); err != nil {
@ -114,7 +123,7 @@ func TestStatusCmdEnvAndStoredToken(t *testing.T) {
func TestStatusCmdStoredTokenButLoadFails(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
t.Setenv("HOME", t.TempDir())
setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "")
// Don't create credentials file — LoadToken returns empty
@ -126,7 +135,7 @@ func TestStatusCmdStoredTokenButLoadFails(t *testing.T) {
func TestLogoutCmdError(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
t.Setenv("HOME", t.TempDir())
setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "")
// Don't create credentials dir — DeleteToken will fail
@ -139,14 +148,12 @@ func TestLogoutCmdError(t *testing.T) {
func TestLogoutCmd(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
home := t.TempDir()
t.Setenv("HOME", home)
dir := setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "")
// Store a token first so DeleteToken has something to delete
credDir := home + "/.config/gitlink-cli"
os.MkdirAll(credDir, 0700)
os.WriteFile(credDir+"/credentials", []byte("some-token"), 0600)
os.MkdirAll(dir, 0700)
os.WriteFile(filepath.Join(dir, "credentials"), []byte("some-token"), 0600)
cmd := findSub(NewAuthCmd(), "logout")
if cmd == nil {
@ -159,8 +166,7 @@ func TestLogoutCmd(t *testing.T) {
func TestLoginWithToken(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
home := t.TempDir()
t.Setenv("HOME", home)
dir := setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "")
// Mock stdin
@ -185,7 +191,7 @@ func TestLoginWithToken(t *testing.T) {
}
// Verify token was saved to file
data, err := os.ReadFile(home + "/.config/gitlink-cli/credentials")
data, err := os.ReadFile(filepath.Join(dir, "credentials"))
if err != nil {
t.Fatalf("read credentials: %v", err)
}
@ -196,7 +202,7 @@ func TestLoginWithToken(t *testing.T) {
func TestLoginWithTokenEmpty(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
t.Setenv("HOME", t.TempDir())
setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "")
oldStdin := os.Stdin
@ -219,7 +225,7 @@ func TestLoginWithTokenEmpty(t *testing.T) {
func TestLoginWithPasswordNoTerminal(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
t.Setenv("HOME", t.TempDir())
setupAuthTest(t)
t.Setenv("GITLINK_TOKEN", "")
// term.ReadPassword will fail because test has no terminal

165
cmd/browse/browse.go Normal file
View File

@ -0,0 +1,165 @@
package browse
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
"github.com/gitlink-org/gitlink-cli/internal/context"
"github.com/gitlink-org/gitlink-cli/internal/output"
"github.com/gitlink-org/gitlink-cli/internal/web"
)
// stdout is the browse command's output target (so tests can redirect).
var stdout io.Writer = os.Stdout
// browsableKinds maps the first path segment of `browse <kind>[/id>` to a URL
// builder. The "default" entry is used as a fallback that appends the raw arg
// to the repo URL, preserving the original passthrough behaviour.
var browsableKinds = []struct {
kind string
desc string
}{
{"issues", "Issue 列表 / 详情 (issues/42)"},
{"pulls", "PR 列表 / 详情 (pulls/128)"},
{"wiki", "Wiki 首页 / 页面 (wiki 或 wiki/API指南)"},
{"actions", "CI/Actions 页面"},
{"commits", "提交列表 / 详情 (commits/abc123)"},
{"branches", "分支列表"},
{"releases", "Release 列表 / 详情 (releases/v2.0)"},
{"milestones", "里程碑页面"},
{"labels", "标签管理页"},
{"settings/hooks", "Webhook 设置页"},
{"settings/collaboration", "成员管理页"},
{"projects", "项目看板页"},
}
// NewBrowseCmd creates the browse command for opening GitLink pages in a browser.
func NewBrowseCmd() *cobra.Command {
var listFlag, noOpen bool
cmd := &cobra.Command{
Use: "browse [resource]",
Short: "在浏览器中打开 GitLink 页面",
Long: `打开当前仓库或指定资源 GitLink 页面
资源格式: issues/42, pulls/42, wiki, wiki/页面名, commits/abc123, ...
不带参数则打开仓库主页owner/repo 自动从 git remote 推断或用 --owner/--repo 指定
示例:
gitlink-cli browse
gitlink-cli browse issues/42
gitlink-cli browse pulls/128
gitlink-cli browse wiki
gitlink-cli browse --list # 列出所有可浏览页面
gitlink-cli browse --no-open # 只打印 URL不打开浏览器`,
Example: ` gitlink-cli browse
gitlink-cli browse issues/42
gitlink-cli browse pulls/128
gitlink-cli browse wiki
gitlink-cli browse --list
gitlink-cli browse --no-open`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
owner, repo, err := context.ResolveOwnerRepo(cmdutil.Owner, cmdutil.Repo)
if err != nil {
return fmt.Errorf("无法推断仓库信息: %w", err)
}
if listFlag {
listBrowsables(owner, repo)
return nil
}
rurl := resolveBrowseURL(web.NewBuilder(), owner, repo, args)
emitBrowse(rurl)
if !noOpen {
if err := web.OpenBrowser(rurl.URL); err != nil {
// 打开失败仅告警URL 已经打印供手动复制
fmt.Fprintf(stdout, "(浏览器未自动打开: %v请手动复制上方 URL\n", err)
}
}
return nil
},
}
cmd.Flags().BoolVar(&listFlag, "list", false, "列出当前仓库所有可浏览的页面")
cmd.Flags().BoolVar(&noOpen, "no-open", false, "只打印 URL不打开浏览器")
return cmd
}
// emitBrowse prints the URL — friendly single line by default, structured
// envelope when --format is set.
func emitBrowse(r *web.ResourceURL) {
if cmdutil.Format == "" {
fmt.Fprintf(stdout, "🔗 %s\n", r.URL)
return
}
_ = output.PrintTo(stdout, output.SuccessEnvelope(r, nil), cmdutil.Format)
}
// listBrowsables prints the catalog of pages `browse` understands.
func listBrowsables(owner, repo string) {
fmt.Fprintf(stdout, "可浏览的 GitLink 页面 (%s/%s):\n", owner, repo)
for _, k := range browsableKinds {
fmt.Fprintf(stdout, " %-28s %s\n", k.kind, k.desc)
}
fmt.Fprintf(stdout, "\n用法: gitlink-cli browse <资源>\n")
}
// resolveBrowseURL maps `browse <arg>` to a web URL. With no arg → repo home.
func resolveBrowseURL(b *web.Builder, owner, repo string, args []string) *web.ResourceURL {
if len(args) == 0 || args[0] == "" {
return b.RepoURL(owner, repo)
}
arg := strings.TrimPrefix(args[0], "/")
// Split into kind and (optional) rest after the first "/".
kind, rest, _ := strings.Cut(arg, "/")
rest = strings.Trim(rest, "/")
switch {
case kind == "issues" || kind == "issue":
return b.IssueURL(owner, repo, atoiOrZero(rest))
case kind == "pulls" || kind == "pr" || kind == "pull":
return b.PRURL(owner, repo, atoiOrZero(rest))
case kind == "wiki":
return b.WikiURL(owner, repo, rest)
case kind == "actions" || kind == "ci":
return b.CIURL(owner, repo)
case kind == "commits":
return b.CommitURL(owner, repo, rest)
case kind == "branches":
return b.BranchURL(owner, repo, rest)
case kind == "releases":
return b.ReleaseURL(owner, repo, rest)
case kind == "milestones":
return b.MilestoneURL(owner, repo)
case kind == "labels":
return b.LabelURL(owner, repo)
case arg == "settings/hooks":
return b.WebhookURL(owner, repo)
case arg == "settings/collaboration":
return b.MemberURL(owner, repo)
case kind == "settings":
return b.RepoURL(owner, repo) // settings landing falls back to repo home
default:
// Unknown resource: append the raw arg as a path segment so behaviour
// stays predictable for callers that already know their URL shape.
return &web.ResourceURL{
URL: b.RepoURL(owner, repo).URL + "/" + arg,
Resource: "custom",
Identifier: arg,
}
}
}
func atoiOrZero(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
return 0
}
return n
}

176
cmd/browse/browse_test.go Normal file
View File

@ -0,0 +1,176 @@
package browse
import (
"bytes"
"strings"
"testing"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
"github.com/gitlink-org/gitlink-cli/internal/web"
)
func TestNewBrowseCmd(t *testing.T) {
cmd := NewBrowseCmd()
if cmd.Use != "browse [resource]" {
t.Errorf("expected Use 'browse [resource]', got %s", cmd.Use)
}
if cmd.Short == "" {
t.Error("Short description should not be empty")
}
if cmd.Long == "" {
t.Error("Long description should not be empty")
}
}
func TestBrowseCmdHasCorrectArgs(t *testing.T) {
cmd := NewBrowseCmd()
if err := cmd.Args(cmd, []string{}); err != nil {
t.Errorf("should accept 0 args: %v", err)
}
if err := cmd.Args(cmd, []string{"issues/42"}); err != nil {
t.Errorf("should accept 1 arg: %v", err)
}
if err := cmd.Args(cmd, []string{"a", "b"}); err == nil {
t.Error("should reject more than 1 arg")
}
}
func TestBrowseCmdNoSubcommands(t *testing.T) {
cmd := NewBrowseCmd()
if cmd.HasSubCommands() {
t.Error("browse should not have subcommands")
}
}
func TestBrowseCmdExample(t *testing.T) {
cmd := NewBrowseCmd()
if cmd.Example == "" {
t.Error("Example should not be empty")
}
if !strings.Contains(cmd.Example, "browse") {
t.Error("Example should contain 'browse'")
}
}
func TestBrowseCmdHasListAndNoOpenFlags(t *testing.T) {
cmd := NewBrowseCmd()
if cmd.Flags().Lookup("list") == nil {
t.Error("missing --list flag")
}
if cmd.Flags().Lookup("no-open") == nil {
t.Error("missing --no-open flag")
}
}
func TestResolveBrowseURL(t *testing.T) {
b := web.NewBuilder()
cases := []struct {
name string
args []string
wantSub string
}{
{"no args → repo", nil, "/o/r"},
{"issue detail", []string{"issues/42"}, "/issues/42"},
{"issue alias", []string{"issue/7"}, "/issues/7"},
{"pr detail", []string{"pulls/128"}, "/pulls/128"},
{"pr alias", []string{"pr/9"}, "/pulls/9"},
{"wiki index", []string{"wiki"}, "/wiki"},
{"wiki page", []string{"wiki/Guide"}, "/wiki/Guide"},
{"ci", []string{"actions"}, "/actions"},
{"ci alias", []string{"ci"}, "/actions"},
{"commit", []string{"commits/abc123"}, "/commits/abc123"},
{"release", []string{"releases/v2.0"}, "/releases/v2.0"},
{"milestones", []string{"milestones"}, "/milestones"},
{"labels", []string{"labels"}, "/issues/labels"},
{"webhook settings", []string{"settings/hooks"}, "/settings/hooks"},
{"collaboration", []string{"settings/collaboration"}, "/settings/collaboration"},
{"unknown passthrough", []string{"custom/seg"}, "/custom/seg"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
r := resolveBrowseURL(b, "o", "r", c.args)
if !strings.Contains(r.URL, c.wantSub) {
t.Errorf("URL %q missing %q", r.URL, c.wantSub)
}
})
}
}
func TestBrowseListOutputsCatalog(t *testing.T) {
out := runBrowse(t, "--owner", "o", "--repo", "r", "--list")
for _, want := range []string{"issues", "pulls", "wiki", "actions"} {
if !strings.Contains(out, want) {
t.Errorf("list missing %q: %q", want, out)
}
}
}
func TestBrowseJSONFormat(t *testing.T) {
// --format json must route emitBrowse through the output envelope.
out := runBrowseFmt(t, "json", "issues/42")
if !strings.Contains(out, `"html_url"`) {
t.Errorf("JSON browse missing html_url: %q", out)
}
}
func TestResolveBrowseURLIssueNonNumeric(t *testing.T) {
// atoiOrZero("abc") should fall back to 0 (issue list page).
r := resolveBrowseURL(web.NewBuilder(), "o", "r", []string{"issues/abc"})
if !strings.HasSuffix(r.URL, "/issues") {
t.Errorf("expected /issues fallback, got %q", r.URL)
}
}
func TestBrowseNoOpenDoesNotLaunchBrowser(t *testing.T) {
// --no-open must print the URL but never invoke a browser. We can't easily
// stub web.OpenBrowser across packages, so we assert the URL is printed
// and that the "browser did not open" warning (printed only when
// OpenBrowser returns an error) is absent.
out := runBrowseNoOpen(t, "issues/42", true)
if !strings.Contains(out, "/issues/42") {
t.Errorf("expected /issues/42 in output: %q", out)
}
if strings.Contains(out, "浏览器未自动打开") {
t.Errorf("--no-open should not print open-failure warning: %q", out)
}
}
// runBrowse runs `browse <args>` with captured stdout.
func runBrowse(t *testing.T, args ...string) string {
t.Helper()
old := stdout
oldOwner, oldRepo, oldFmt := cmdutil.Owner, cmdutil.Repo, cmdutil.Format
buf := &bytes.Buffer{}
stdout = buf
defer func() {
stdout = old
cmdutil.Owner, cmdutil.Repo, cmdutil.Format = oldOwner, oldRepo, oldFmt
}()
cmd := NewBrowseCmd()
cmd.PersistentFlags().StringVar(&cmdutil.Owner, "owner", "", "")
cmd.PersistentFlags().StringVar(&cmdutil.Repo, "repo", "", "")
cmd.PersistentFlags().StringVar(&cmdutil.Format, "format", "", "")
cmd.SetArgs(args)
if err := cmd.Execute(); err != nil {
t.Fatalf("browse %v: %v", args, err)
}
return buf.String()
}
// runBrowseFmt runs `browse <resource>` with a specific --format value.
func runBrowseFmt(t *testing.T, format, resource string) string {
t.Helper()
return runBrowse(t, "--owner", "o", "--repo", "r", "--format", format, "--no-open", resource)
}
func runBrowseNoOpen(t *testing.T, resource string, noOpen bool) string {
t.Helper()
args := []string{"--owner", "o", "--repo", "r"}
if resource != "" {
args = append(args, resource)
}
if noOpen {
args = append(args, "--no-open")
}
return runBrowse(t, args...)
}

View File

@ -7,4 +7,10 @@ var (
Format string
Debug bool
Lang string
// Web, when true, asks shortcut commands to print and open the GitLink web
// URL corresponding to the resource they just operated on.
Web bool
// Demo, when true (or GITLINK_DEMO=1), routes every API call through the
// in-process mock transport so the CLI runs with no network and no token.
Demo bool
)

121
cmd/demo/demo.go Normal file
View File

@ -0,0 +1,121 @@
// Package demo implements `gitlink-cli demo +run <showcase>`: a pre-baked,
// projector-friendly runner that walks through the subtask-1 demo script with
// GITLINK_DEMO=1 forced on, so the whole showcase runs offline.
package demo
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
)
// stdout is the runner's output target (so tests can redirect).
var stdout io.Writer = os.Stdout
// NewDemoCmd builds the `demo` command tree.
func NewDemoCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "demo",
Short: "演示模式 — 无需 Token 即可展示全部功能",
Long: `运行预编排的演示脚本自动开启 GITLINK_DEMO=1所有命令走内置 Mock 数据
无需网络和 Token适合课堂投影演示
可用 showcase:
showcase 全功能展示覆盖五大类别~20 min
quick-tour 5 分钟快速导览`,
}
cmd.AddCommand(newRunCmd())
return cmd
}
func newRunCmd() *cobra.Command {
var listOnly bool
cmd := &cobra.Command{
Use: "+run [showcase]",
Short: "运行(或列出)预编排演示脚本",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if listOnly {
for _, s := range showcases {
fmt.Fprintf(stdout, "%-12s %s\n", s.Name, s.Description)
}
return nil
}
name := "showcase"
if len(args) > 0 {
name = args[0]
}
sc, ok := findShowcase(name)
if !ok {
return fmt.Errorf("未找到 showcase %q运行 `demo +run --list` 查看可用项", name)
}
return runShowcase(sc)
},
}
cmd.Flags().BoolVar(&listOnly, "list", false, "列出所有可用 showcase")
return cmd
}
// runStep is the per-step executor. It is a package-level variable so tests
// can swap it for a no-op instead of recursing into the test binary (which
// os.Args[0] points at during go test).
var runStep = func(bin string, argv []string, env []string, out io.Writer) error {
c := exec.Command(bin, argv...)
c.Env = env
c.Stdout = out
c.Stderr = out
return c.Run()
}
// runShowcase prints + executes each step. Each step invokes the gitlink-cli
// binary (os.Args[0]) with GITLINK_DEMO=1 so commands return mock data even on
// a machine without network/token. Step failures never abort the showcase.
func runShowcase(sc Showcase) error {
fmt.Fprintf(stdout, "╔════ %s ════╗\n", sc.Name)
fmt.Fprintf(stdout, " %s\n 步数: %d\n\n", sc.Description, len(sc.Steps))
bin := os.Args[0]
env := append(os.Environ(), "GITLINK_DEMO=1")
for i, step := range sc.Steps {
fmt.Fprintf(stdout, "\n━━━ [%d/%d] %s ━━━\n", i+1, len(sc.Steps), step.Desc)
fmt.Fprintf(stdout, "▶ gitlink-cli %s\n", step.Cmd)
argv := splitArgs(step.Cmd)
if len(argv) == 0 {
continue
}
if err := runStep(bin, argv, env, stdout); err != nil {
// Demo never aborts on a step failure — just narrate it.
fmt.Fprintf(stdout, " (本步返回错误: %v\n", err)
}
}
fmt.Fprintf(stdout, "\n╚════ 演示结束(共 %d 步) ════╝\n", len(sc.Steps))
return nil
}
// splitArgs tokenises a command line. It honours double-quoted segments so
// flags like --title "a b c" survive intact.
func splitArgs(line string) []string {
var out []string
var cur strings.Builder
inQ := false
for _, r := range line {
switch {
case r == '"':
inQ = !inQ
case (r == ' ' || r == '\t') && !inQ:
if cur.Len() > 0 {
out = append(out, cur.String())
cur.Reset()
}
default:
cur.WriteRune(r)
}
}
if cur.Len() > 0 {
out = append(out, cur.String())
}
return out
}

156
cmd/demo/demo_test.go Normal file
View File

@ -0,0 +1,156 @@
package demo
import (
"bytes"
"fmt"
"io"
"strings"
"testing"
)
func TestNewDemoCmd(t *testing.T) {
cmd := NewDemoCmd()
if cmd.Use != "demo" {
t.Errorf("Use = %q, want demo", cmd.Use)
}
if !cmd.HasSubCommands() {
t.Error("demo should have +run subcommand")
}
}
func TestRunList(t *testing.T) {
out := runRun(t, "--list")
if !strings.Contains(out, "showcase") {
t.Errorf("list missing showcase: %q", out)
}
if !strings.Contains(out, "quick-tour") {
t.Errorf("list missing quick-tour: %q", out)
}
}
func TestRunUnknownShowcaseErrors(t *testing.T) {
root := NewDemoCmd()
root.SetArgs([]string{"+run", "does-not-exist"})
err := root.Execute()
if err == nil {
t.Fatal("expected error for unknown showcase")
}
if !strings.Contains(err.Error(), "does-not-exist") {
t.Errorf("error should name the missing showcase: %v", err)
}
}
func TestFindShowcase(t *testing.T) {
if _, ok := findShowcase("showcase"); !ok {
t.Error("showcase should exist")
}
if _, ok := findShowcase("quick-tour"); !ok {
t.Error("quick-tour should exist")
}
if _, ok := findShowcase("missing"); ok {
t.Error("missing should not exist")
}
}
func TestSplitArgs(t *testing.T) {
cases := []struct {
in string
want []string
}{
{"", nil},
{"version", []string{"version"}},
{"issue +list --owner o --repo r", []string{"issue", "+list", "--owner", "o", "--repo", "r"}},
{`--title "a b c" --x`, []string{"--title", "a b c", "--x"}},
{" multiple spaces ", []string{"multiple", "spaces"}},
}
for _, c := range cases {
got := splitArgs(c.in)
if len(got) != len(c.want) {
t.Errorf("splitArgs(%q) = %v, want %v", c.in, got, c.want)
continue
}
for i := range got {
if got[i] != c.want[i] {
t.Errorf("splitArgs(%q)[%d] = %q, want %q", c.in, i, got[i], c.want[i])
}
}
}
}
func TestRunShowcasePrintsHeaderAndFooter(t *testing.T) {
// Stub the per-step executor so we don't recurse into the test binary.
oldStep := runStep
runStep = func(string, []string, []string, io.Writer) error { return nil }
defer func() { runStep = oldStep }()
old := stdout
buf := &bytes.Buffer{}
stdout = buf
defer func() { stdout = old }()
sc := Showcase{Name: "t", Description: "test", Steps: []Step{{Desc: "s1", Cmd: "version"}}}
if err := runShowcase(sc); err != nil {
t.Fatalf("runShowcase: %v", err)
}
out := buf.String()
if !strings.Contains(out, "test") {
t.Errorf("missing description: %q", out)
}
if !strings.Contains(out, "演示结束") {
t.Errorf("missing footer: %q", out)
}
}
func TestRunShowcaseNarratesStepFailure(t *testing.T) {
oldStep := runStep
runStep = func(string, []string, []string, io.Writer) error { return fmt.Errorf("boom") }
defer func() { runStep = oldStep }()
old := stdout
buf := &bytes.Buffer{}
stdout = buf
defer func() { stdout = old }()
sc := Showcase{Name: "t", Description: "x", Steps: []Step{{Desc: "s", Cmd: "version"}}}
_ = runShowcase(sc)
if !strings.Contains(buf.String(), "本步返回错误") {
t.Errorf("expected step-failure narration: %q", buf.String())
}
}
func TestRunShowcaseSkipsEmptyCmd(t *testing.T) {
called := false
oldStep := runStep
runStep = func(string, []string, []string, io.Writer) error { called = true; return nil }
defer func() { runStep = oldStep }()
old := stdout
stdout = &bytes.Buffer{}
defer func() { stdout = old }()
_ = runShowcase(Showcase{Name: "n", Description: "d", Steps: []Step{{Desc: "empty", Cmd: ""}}})
if called {
t.Error("runStep should not be called for empty Cmd")
}
}
func TestRunListFlagWired(t *testing.T) {
cmd := newRunCmd()
if cmd.Flags().Lookup("list") == nil {
t.Error("missing --list flag on +run")
}
}
// runRun executes `demo +run <args>` with captured stdout.
func runRun(t *testing.T, args ...string) string {
t.Helper()
old := stdout
buf := &bytes.Buffer{}
stdout = buf
defer func() { stdout = old }()
root := NewDemoCmd()
root.SetArgs(append([]string{"+run"}, args...))
if err := root.Execute(); err != nil {
t.Fatalf("demo +run %v: %v", args, err)
}
return buf.String()
}

69
cmd/demo/showcases.go Normal file
View File

@ -0,0 +1,69 @@
package demo
// Showcase is a pre-baked, projector-friendly demo script: a titled sequence of
// gitlink-cli invocations. Each step runs with GITLINK_DEMO=1 so the whole
// showcase is offline and token-free.
type Showcase struct {
Name string
Description string
Steps []Step
}
// Step is one CLI invocation inside a showcase.
type Step struct {
Desc string // human-readable narration for the slide
Cmd string // the gitlink-cli args (without the binary name)
}
// showcases is the catalog. Add new entries here; +run --list reads from it.
var showcases = []Showcase{
{
Name: "showcase",
Description: "GitLink-CLI 全功能展示(覆盖子任务一五大类别,~20 min",
Steps: []Step{
{"版本与命令全景", "version"},
{"自动上下文 + 健康检查", "status"},
{"能力探测", "capability +check"},
{"终端↔网页联动(仓库主页)", "show repo --owner jiangtx --repo gitlink-cli-demo"},
{"Issue 详情页 URL", "show issue --owner jiangtx --repo gitlink-cli-demo --number 42"},
{"第一类·Webhook 列表", "webhook +list --owner jiangtx --repo gitlink-cli-demo"},
{"第一类·Wiki 页面", "wiki +pages --owner jiangtx --repo gitlink-cli-demo"},
{"第一类·标签列表", "label +list --owner jiangtx --repo gitlink-cli-demo"},
{"第一类·成员列表", "member +list --owner jiangtx --repo gitlink-cli-demo"},
{"第一类·里程碑", "milestone +list --owner jiangtx --repo gitlink-cli-demo"},
{"第二类·三种输出格式JSON", "repo +list --format json"},
{"第二类·Table 格式", "repo +list --format table"},
{"第二类·调试模式", "issue +list --owner jiangtx --repo gitlink-cli-demo --debug"},
{"第三类·批量关闭 dry-run", "issue +batch-close --owner jiangtx --repo gitlink-cli-demo --numbers 1,2,3 --dry-run"},
{"第三类·批量加成员 dry-run", "member +batch-add --owner jiangtx --repo gitlink-cli-demo --user-ids 101,102 --dry-run"},
{"第三类·导出 Issue", "export +issues --owner jiangtx --repo gitlink-cli-demo --format csv --output demo_issues.csv"},
{"第五类·语言占比", "repo +languages --owner jiangtx --repo gitlink-cli-demo"},
{"第五类·贡献者", "repo +contributors --owner jiangtx --repo gitlink-cli-demo"},
{"第五类·用户热力图", "user +heatmap --login jiangtx"},
},
},
{
Name: "quick-tour",
Description: "5 分钟快速导览(仅核心命令)",
Steps: []Step{
{"版本", "version"},
{"仓库主页 URL", "show repo --owner jiangtx --repo gitlink-cli-demo"},
{"Issue 列表", "issue +list --owner jiangtx --repo gitlink-cli-demo"},
{"Webhook 列表", "webhook +list --owner jiangtx --repo gitlink-cli-demo"},
{"能力探测", "capability +check"},
},
},
}
func findShowcase(name string) (Showcase, bool) {
for _, s := range showcases {
if s.Name == name {
return s, true
}
}
return Showcase{}, false
}

View File

@ -7,11 +7,17 @@ import (
"github.com/spf13/cobra"
aliasCmd "github.com/gitlink-org/gitlink-cli/cmd/alias"
apiCmd "github.com/gitlink-org/gitlink-cli/cmd/api"
authCmd "github.com/gitlink-org/gitlink-cli/cmd/auth"
browseCmd "github.com/gitlink-org/gitlink-cli/cmd/browse"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
configCmd "github.com/gitlink-org/gitlink-cli/cmd/config"
demoCmd "github.com/gitlink-org/gitlink-cli/cmd/demo"
showCmd "github.com/gitlink-org/gitlink-cli/cmd/show"
statusCmd "github.com/gitlink-org/gitlink-cli/cmd/status"
internalConfig "github.com/gitlink-org/gitlink-cli/internal/config"
"github.com/gitlink-org/gitlink-cli/internal/demo"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts"
)
@ -51,12 +57,19 @@ func NewRootCmd(opts RootOptions, tr *i18n.Translator) (*cobra.Command, error) {
rootCmd.PersistentFlags().StringVar(&cmdutil.Repo, "repo", "", tr.T("flag.repo"))
rootCmd.PersistentFlags().StringVar(&cmdutil.Format, "format", "", tr.T("flag.format"))
rootCmd.PersistentFlags().BoolVar(&cmdutil.Debug, "debug", false, tr.T("flag.debug"))
rootCmd.PersistentFlags().BoolVar(&cmdutil.Web, "web", false, "执行后打印并打开对应 GitLink 网页 URL")
rootCmd.PersistentFlags().BoolVar(&demo.DemoFlag, "demo", false, "演示模式:使用内置 Mock 数据,无需网络和 Token")
rootCmd.PersistentFlags().StringVar(&cmdutil.Lang, "lang", "", tr.T("flag.lang"))
rootCmd.AddCommand(authCmd.NewAuthCmd(tr))
rootCmd.AddCommand(apiCmd.NewAPICmd(tr))
rootCmd.AddCommand(configCmd.NewConfigCmd(tr))
rootCmd.AddCommand(newVersionCmd(version, tr))
rootCmd.AddCommand(aliasCmd.NewAliasCmd())
rootCmd.AddCommand(browseCmd.NewBrowseCmd())
rootCmd.AddCommand(statusCmd.NewStatusCmd())
rootCmd.AddCommand(showCmd.NewShowCmd())
rootCmd.AddCommand(demoCmd.NewDemoCmd())
shortcuts.RegisterAll(rootCmd, tr)

292
cmd/show/show.go Normal file
View File

@ -0,0 +1,292 @@
// Package show implements `gitlink-cli show <resource>`: print the GitLink web
// URL for a resource without opening a browser. It is the projector-friendly
// twin of `browse` — exactly the same URL table (via internal/web.Builder),
// but it prints instead of launching.
//
// Output convention:
// - default (no --format): a single human-readable line "🔗 <URL>"
// - --format json/table/yaml: a standard output envelope
//
// This dual mode lets the demo script `gitlink-cli show repo` render a tidy
// URL on slides while `show repo --format json` stays machine-parsable for AI
// Agents and verify.sh.
package show
import (
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
"github.com/gitlink-org/gitlink-cli/internal/context"
"github.com/gitlink-org/gitlink-cli/internal/output"
"github.com/gitlink-org/gitlink-cli/internal/web"
)
// stdout is the emit target. Package-level so tests can redirect it.
var stdout io.Writer = os.Stdout
// NewShowCmd builds the `show` command tree.
func NewShowCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "show",
Short: "打印 GitLink 网页 URL不打开浏览器",
Long: `打印资源对应的 GitLink 网页 URL用于投影演示
默认输出一行 🔗 URL --format json 输出结构化 envelope AI Agent / 脚本
owner/repo 自动从 git remote 推断或用 --owner/--repo 指定
示例:
gitlink-cli show repo
gitlink-cli show issue --number 42
gitlink-cli show pr --number 128
gitlink-cli show wiki --page "API 指南"
gitlink-cli show webhook
gitlink-cli show user --login jiangtx`,
}
cmd.AddCommand(
newRepoCmd(),
newIssueCmd(),
newPRCmd(),
newWikiCmd(),
newMemberCmd(),
newWebhookCmd(),
newLabelCmd(),
newMilestoneCmd(),
newBranchCmd(),
newReleaseCmd(),
newCommitCmd(),
newCICmd(),
newCompareCmd(),
newOrgCmd(),
newUserCmd(),
newNotificationCmd(),
)
return cmd
}
// emit prints r in the configured format. Empty format → friendly single line.
func emit(r *web.ResourceURL) error {
if cmdutil.Format == "" {
fmt.Fprintf(stdout, "🔗 %s\n", r.URL)
return nil
}
return output.PrintTo(stdout, output.SuccessEnvelope(r, nil), cmdutil.Format)
}
// resolveOwnerRepo reads global --owner/--repo and falls back to git remote.
func resolveOwnerRepo() (string, string, error) {
return context.ResolveOwnerRepo(cmdutil.Owner, cmdutil.Repo)
}
// ownerRepoCmd builds a subcommand that only needs owner/repo.
func ownerRepoCmd(use, short string, build func(b *web.Builder, owner, repo string) *web.ResourceURL) *cobra.Command {
return &cobra.Command{
Use: use,
Short: short,
RunE: func(cmd *cobra.Command, args []string) error {
owner, repo, err := resolveOwnerRepo()
if err != nil {
return err
}
return emit(build(web.NewBuilder(), owner, repo))
},
}
}
func newRepoCmd() *cobra.Command {
return ownerRepoCmd("repo", "仓库主页 URL",
func(b *web.Builder, o, r string) *web.ResourceURL { return b.RepoURL(o, r) })
}
func newIssueCmd() *cobra.Command {
var number int
cmd := &cobra.Command{
Use: "issue",
Short: "Issue 网页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
owner, repo, err := resolveOwnerRepo()
if err != nil {
return err
}
return emit(web.NewBuilder().IssueURL(owner, repo, number))
},
}
cmd.Flags().IntVarP(&number, "number", "n", 0, "Issue 编号(省略则输出 Issue 列表页)")
return cmd
}
func newPRCmd() *cobra.Command {
var number int
cmd := &cobra.Command{
Use: "pr",
Short: "Pull Request 网页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
owner, repo, err := resolveOwnerRepo()
if err != nil {
return err
}
return emit(web.NewBuilder().PRURL(owner, repo, number))
},
}
cmd.Flags().IntVarP(&number, "number", "n", 0, "PR 编号(省略则输出 PR 列表页)")
return cmd
}
func newWikiCmd() *cobra.Command {
var page string
cmd := &cobra.Command{
Use: "wiki",
Short: "Wiki 网页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
owner, repo, err := resolveOwnerRepo()
if err != nil {
return err
}
return emit(web.NewBuilder().WikiURL(owner, repo, page))
},
}
cmd.Flags().StringVarP(&page, "page", "p", "", "Wiki 页面名(省略则输出 Wiki 首页)")
return cmd
}
func newMemberCmd() *cobra.Command {
return ownerRepoCmd("member", "成员管理页 URL",
func(b *web.Builder, o, r string) *web.ResourceURL { return b.MemberURL(o, r) })
}
func newWebhookCmd() *cobra.Command {
return ownerRepoCmd("webhook", "Webhook 设置页 URL",
func(b *web.Builder, o, r string) *web.ResourceURL { return b.WebhookURL(o, r) })
}
func newLabelCmd() *cobra.Command {
return ownerRepoCmd("label", "标签管理页 URL",
func(b *web.Builder, o, r string) *web.ResourceURL { return b.LabelURL(o, r) })
}
func newMilestoneCmd() *cobra.Command {
return ownerRepoCmd("milestone", "里程碑页 URL",
func(b *web.Builder, o, r string) *web.ResourceURL { return b.MilestoneURL(o, r) })
}
func newBranchCmd() *cobra.Command {
var branch string
cmd := &cobra.Command{
Use: "branch",
Short: "分支页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
owner, repo, err := resolveOwnerRepo()
if err != nil {
return err
}
return emit(web.NewBuilder().BranchURL(owner, repo, branch))
},
}
cmd.Flags().StringVarP(&branch, "branch", "b", "", "分支名(省略则输出分支列表页)")
return cmd
}
func newReleaseCmd() *cobra.Command {
var tag string
cmd := &cobra.Command{
Use: "release",
Short: "Release 页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
owner, repo, err := resolveOwnerRepo()
if err != nil {
return err
}
return emit(web.NewBuilder().ReleaseURL(owner, repo, tag))
},
}
cmd.Flags().StringVarP(&tag, "tag", "t", "", "Release tag省略则输出 Release 列表页)")
return cmd
}
func newCommitCmd() *cobra.Command {
var sha string
cmd := &cobra.Command{
Use: "commit",
Short: "提交详情页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
owner, repo, err := resolveOwnerRepo()
if err != nil {
return err
}
return emit(web.NewBuilder().CommitURL(owner, repo, sha))
},
}
cmd.Flags().StringVarP(&sha, "sha", "s", "", "提交 SHA省略则输出提交列表页")
return cmd
}
func newCICmd() *cobra.Command {
return ownerRepoCmd("ci", "CI/Actions 页 URL",
func(b *web.Builder, o, r string) *web.ResourceURL { return b.CIURL(o, r) })
}
func newCompareCmd() *cobra.Command {
var base, head string
cmd := &cobra.Command{
Use: "compare",
Short: "分支对比页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
if base == "" || head == "" {
return fmt.Errorf("--base 和 --head 均为必填")
}
owner, repo, err := resolveOwnerRepo()
if err != nil {
return err
}
return emit(web.NewBuilder().CompareURL(owner, repo, base, head))
},
}
cmd.Flags().StringVar(&base, "base", "", "基准分支/tag")
cmd.Flags().StringVar(&head, "head", "", "目标分支/tag")
return cmd
}
func newOrgCmd() *cobra.Command {
var name string
cmd := &cobra.Command{
Use: "org",
Short: "组织页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
if name == "" {
return fmt.Errorf("--name 为必填")
}
return emit(web.NewBuilder().OrgURL(name))
},
}
cmd.Flags().StringVar(&name, "name", "", "组织名")
return cmd
}
func newUserCmd() *cobra.Command {
var login string
cmd := &cobra.Command{
Use: "user",
Short: "用户主页 URL",
RunE: func(cmd *cobra.Command, args []string) error {
if login == "" {
return fmt.Errorf("--login 为必填")
}
return emit(web.NewBuilder().UserURL(login))
},
}
cmd.Flags().StringVar(&login, "login", "", "用户登录名")
return cmd
}
func newNotificationCmd() *cobra.Command {
return &cobra.Command{
Use: "notification",
Short: "通知中心 URL",
RunE: func(cmd *cobra.Command, args []string) error {
return emit(web.NewBuilder().NotificationURL())
},
}
}

214
cmd/show/show_test.go Normal file
View File

@ -0,0 +1,214 @@
package show
import (
"bytes"
"strings"
"testing"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
"github.com/spf13/cobra"
)
// newShowWithFlags builds a show tree with owner/repo/format persistent flags
// that normally come from the parent rootCmd, so subcommands can parse them in
// isolation during tests.
func newShowWithFlags() *cobra.Command {
cmdutil.Owner = ""
cmdutil.Repo = ""
cmdutil.Format = ""
r := NewShowCmd()
r.PersistentFlags().StringVar(&cmdutil.Owner, "owner", "", "")
r.PersistentFlags().StringVar(&cmdutil.Repo, "repo", "", "")
r.PersistentFlags().StringVar(&cmdutil.Format, "format", "", "")
return r
}
// runShow executes `show <args>` with captured stdout and returns the output.
func runShow(t *testing.T, args ...string) string {
t.Helper()
oldOut := stdout
oldFmt := cmdutil.Format
buf := &bytes.Buffer{}
stdout = buf
cmdutil.Format = ""
defer func() {
stdout = oldOut
cmdutil.Format = oldFmt
}()
root := newShowWithFlags()
root.SetArgs(args)
if err := root.Execute(); err != nil {
t.Fatalf("show %v: %v", args, err)
}
return buf.String()
}
func TestShowSubcommandsRegistered(t *testing.T) {
root := NewShowCmd()
want := []string{
"repo", "issue", "pr", "wiki", "member", "webhook", "label",
"milestone", "branch", "release", "commit", "ci", "compare",
"org", "user", "notification",
}
got := map[string]bool{}
for _, c := range root.Commands() {
got[c.Name()] = true
}
for _, w := range want {
if !got[w] {
t.Errorf("subcommand %q not registered", w)
}
}
}
func TestShowRepoDefault(t *testing.T) {
out := runShow(t, "repo", "--owner", "jiangtx", "--repo", "gitlink-cli-demo")
if !strings.Contains(out, "🔗") {
t.Errorf("missing 🔗 marker: %q", out)
}
if !strings.Contains(out, "https://gitlink.org.cn/jiangtx/gitlink-cli-demo") {
t.Errorf("wrong URL: %q", out)
}
}
func TestShowIssueWithNumber(t *testing.T) {
out := runShow(t, "issue", "--owner", "jiangtx", "--repo", "demo", "--number", "42")
if !strings.Contains(out, "/issues/42") {
t.Errorf("expected /issues/42 in: %q", out)
}
}
func TestShowIssueListWhenNoNumber(t *testing.T) {
out := runShow(t, "issue", "--owner", "jiangtx", "--repo", "demo")
if !strings.HasSuffix(strings.TrimSpace(out), "/issues") {
t.Errorf("expected /issues suffix: %q", out)
}
}
func TestShowPR(t *testing.T) {
out := runShow(t, "pr", "--owner", "o", "--repo", "r", "--number", "128")
if !strings.Contains(out, "/pulls/128") {
t.Errorf("expected /pulls/128: %q", out)
}
}
func TestShowWiki(t *testing.T) {
out := runShow(t, "wiki", "--owner", "o", "--repo", "r", "--page", "API 指南")
if !strings.Contains(out, "/wiki/") {
t.Errorf("expected /wiki/ segment: %q", out)
}
}
func TestShowWebhook(t *testing.T) {
out := runShow(t, "webhook", "--owner", "o", "--repo", "r")
if !strings.Contains(out, "/settings/hooks") {
t.Errorf("expected /settings/hooks: %q", out)
}
}
func TestShowMember(t *testing.T) {
out := runShow(t, "member", "--owner", "o", "--repo", "r")
if !strings.Contains(out, "/settings/collaboration") {
t.Errorf("expected collaboration URL: %q", out)
}
}
func TestShowLabel(t *testing.T) {
out := runShow(t, "label", "--owner", "o", "--repo", "r")
if !strings.Contains(out, "/issues/labels") {
t.Errorf("expected labels URL: %q", out)
}
}
func TestShowMilestone(t *testing.T) {
out := runShow(t, "milestone", "--owner", "o", "--repo", "r")
if !strings.Contains(out, "/milestones") {
t.Errorf("expected milestones URL: %q", out)
}
}
func TestShowBranch(t *testing.T) {
out := runShow(t, "branch", "--owner", "o", "--repo", "r", "--branch", "feat/x")
if !strings.Contains(out, "/branches/") {
t.Errorf("expected /branches/ segment: %q", out)
}
}
func TestShowRelease(t *testing.T) {
out := runShow(t, "release", "--owner", "o", "--repo", "r", "--tag", "v2.0")
if !strings.Contains(out, "/releases/v2.0") {
t.Errorf("expected /releases/v2.0: %q", out)
}
}
func TestShowCommit(t *testing.T) {
out := runShow(t, "commit", "--owner", "o", "--repo", "r", "--sha", "abc123")
if !strings.Contains(out, "/commits/abc123") {
t.Errorf("expected /commits/abc123: %q", out)
}
}
func TestShowCI(t *testing.T) {
out := runShow(t, "ci", "--owner", "o", "--repo", "r")
if !strings.Contains(out, "/actions") {
t.Errorf("expected /actions: %q", out)
}
}
func TestShowCompare(t *testing.T) {
out := runShow(t, "compare", "--owner", "o", "--repo", "r", "--base", "master", "--head", "dev")
if !strings.Contains(out, "/compare/master...dev") {
t.Errorf("expected compare URL: %q", out)
}
}
func TestShowOrg(t *testing.T) {
out := runShow(t, "org", "--name", "ccf")
if !strings.HasSuffix(strings.TrimSpace(out), "/ccf") {
t.Errorf("expected /ccf suffix: %q", out)
}
}
func TestShowUser(t *testing.T) {
out := runShow(t, "user", "--login", "jiangtx")
if !strings.HasSuffix(strings.TrimSpace(out), "/jiangtx") {
t.Errorf("expected /jiangtx suffix: %q", out)
}
}
func TestShowNotification(t *testing.T) {
out := runShow(t, "notification")
if !strings.Contains(out, "/notifications") {
t.Errorf("expected /notifications: %q", out)
}
}
func TestShowJSONFormat(t *testing.T) {
// JSON mode must produce a parseable envelope containing html_url.
oldOut := stdout
buf := &bytes.Buffer{}
stdout = buf
defer func() { stdout = oldOut }()
root := newShowWithFlags()
cmdutil.Format = "json"
root.SetArgs([]string{"repo", "--owner", "o", "--repo", "r"})
if err := root.Execute(); err != nil {
t.Fatalf("execute: %v", err)
}
if !strings.Contains(buf.String(), `"html_url"`) {
t.Errorf("JSON missing html_url: %q", buf.String())
}
if !strings.Contains(buf.String(), `"ok": true`) {
t.Errorf("JSON missing ok:true: %q", buf.String())
}
}
func TestShowCompareRequiresBothFlags(t *testing.T) {
root := newShowWithFlags()
root.SetArgs([]string{"compare", "--owner", "o", "--repo", "r", "--base", "master"})
err := root.Execute()
if err == nil {
t.Fatal("expected error when --head missing")
}
}

68
cmd/status/status.go Normal file
View File

@ -0,0 +1,68 @@
package status
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/gitlink-org/gitlink-cli/internal/auth"
"github.com/gitlink-org/gitlink-cli/internal/config"
"github.com/gitlink-org/gitlink-cli/internal/context"
)
// NewStatusCmd creates the status command that displays login state and context.
func NewStatusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "显示当前登录状态和上下文信息",
Long: `显示 gitlink-cli 的当前状态包括
- 认证状态是否已登录Token 来源
- API 地址
- 当前目录
- 自动推断的仓库信息`,
Example: ` gitlink-cli status`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _ := config.Load()
token, _ := auth.LoadToken()
if token == "" {
token = os.Getenv("GITLINK_TOKEN")
}
cwd, _ := os.Getwd()
fmt.Println("GitLink CLI 状态")
fmt.Println("───────────────")
// 认证状态
if token != "" {
fmt.Println(" 认证状态: 已登录")
fmt.Printf(" Token 来源: %s\n", tokenSource(token))
} else {
fmt.Println(" 认证状态: 未登录(运行 gitlink-cli auth login")
}
// API 地址
fmt.Printf(" API 地址: %s\n", cfg.BaseURL)
// 当前目录
fmt.Printf(" 当前目录: %s\n", cwd)
// 推断的仓库
owner, repo, err := context.ResolveOwnerRepo("", "")
if err == nil {
fmt.Printf(" 推断仓库: %s/%s\n", owner, repo)
} else {
fmt.Println(" 推断仓库: (不在 Git 仓库中)")
}
return nil
},
}
}
func tokenSource(token string) string {
if token == os.Getenv("GITLINK_TOKEN") {
return "环境变量 GITLINK_TOKEN"
}
return "keyring / 配置文件"
}

58
cmd/status/status_test.go Normal file
View File

@ -0,0 +1,58 @@
package status
import (
"strings"
"testing"
)
func TestNewStatusCmd(t *testing.T) {
cmd := NewStatusCmd()
if cmd.Use != "status" {
t.Errorf("expected Use 'status', got %s", cmd.Use)
}
if cmd.Short == "" {
t.Error("Short description should not be empty")
}
if cmd.Long == "" {
t.Error("Long description should not be empty")
}
}
func TestNewStatusCmdExample(t *testing.T) {
cmd := NewStatusCmd()
if !strings.Contains(cmd.Example, "status") {
t.Errorf("Example should contain 'status', got: %s", cmd.Example)
}
}
func TestNewStatusCmdHasNoSubcommands(t *testing.T) {
cmd := NewStatusCmd()
if cmd.HasSubCommands() {
t.Error("status should not have subcommands")
}
}
func TestTokenSourceFromEnv(t *testing.T) {
t.Setenv("GITLINK_TOKEN", "test-token-123")
result := tokenSource("test-token-123")
if result != "环境变量 GITLINK_TOKEN" {
t.Errorf("expected env source, got: %s", result)
}
}
func TestTokenSourceFromKeyring(t *testing.T) {
// 不设置环境变量,或用不同的值
t.Setenv("GITLINK_TOKEN", "")
result := tokenSource("some-stored-token")
if result != "keyring / 配置文件" {
t.Errorf("expected keyring source, got: %s", result)
}
}
func TestTokenSourceMismatch(t *testing.T) {
t.Setenv("GITLINK_TOKEN", "env-token")
result := tokenSource("different-token")
if result != "keyring / 配置文件" {
t.Errorf("should fallback to keyring when token differs from env, got: %s", result)
}
}

2
contributors.csv Normal file
View File

@ -0,0 +1,2 @@
id,login,contributions
<nil>,<nil>,<nil>
1 id login contributions
2 <nil> <nil> <nil>

387
doc/DEMO-SCRIPT-SUBTASK1.md Normal file
View File

@ -0,0 +1,387 @@
# 子任务一:终端↔网页联动演示方案
> 对齐 `课程实践任务及要求 - 0613.pdf` 子任务一的五大类别
>
> 核心思路:**终端执行命令 → `show` 输出网页 URL → 投影仪打开网页 → 听众直观验证**
---
## 演示路线图(~20 min
```
开场 (2 min) 安装 + 认证 + 全景概览
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一类 (4 min) 新增 Shortcut 命令
Wiki/Webhook/PM/Pipeline/Label/Member/Milestone
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第二类 (3 min) 优化现有命令
多格式输出/自动上下文/--debug/中文帮助
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第三类 (3 min) 批量操作能力
batch-close/batch-add/export/dry-run
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第四类 (3 min) 跨平台兼容 + 安装体验
5种安装/npm postinstall/keyring多平台
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第五类 (3 min) 补全 Raw API 封装
HTML检测/端点→Shortcut映射/Skills修复
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
收尾 (2 min) Capability检测 + 覆盖率 + 验收清单
```
---
## 第一类:新增 Shortcut 命令4 min
> **覆盖**: Wiki 管理 · Webhook 配置 · 项目看板 · 流水线 · 标签 · 成员 · 里程碑
>
> **话术**: "扩展前这些模块完全不存在。扩展后每个都是完整的 CRUD + 列表查询。"
### A. Webhook 管理 — 从无到有的完整 CRUD
```bash
# ── 终端 Step 1: 创建 Webhook ──
gitlink-cli webhook +create --owner jiangtx --repo gitlink-cli-demo \
--url https://my-ci.gitlink.org.cn/hook --events push,create
# → ✓ Webhook 创建成功 (id: 51348)
# ── 终端 Step 2: 列出所有 Webhook ──
gitlink-cli webhook +list --owner jiangtx --repo gitlink-cli-demo
# → { "ok": true, "data": { "total_count": 1, "webhooks": [...] } }
# ── 终端 Step 3: 获取网页链接 ──
gitlink-cli show webhook
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/hooks
# 【投影仪】打开 Webhook 设置页面,展示刚才创建的 Webhook
```
```
终端输出 网页对应
──────── ────────
webhook +create 成功 → settings/hooks 页面显示新增条目
webhook +list (JSON) → settings/hooks 列表视图
webhook +test --id 51348 → settings/hooks/51348 测试结果
```
### B. Wiki 管理 — 文档的 CLI 操作
```bash
gitlink-cli wiki +pages --owner jiangtx --repo gitlink-cli-demo
# → 列出所有 Wiki 页面
gitlink-cli wiki +create --owner jiangtx --repo gitlink-cli-demo \
--title "API 使用指南" --content "# 快速开始\n\n## 安装\n..."
# → ✓ Wiki 页面创建成功
gitlink-cli show wiki --page "API 使用指南"
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/wiki/API%20使用指南
# 【投影仪】打开 Wiki 页面,展示刚创建的内容
```
### C. 项目管理(PM) + Pipeline + Label + Member + Milestone
```bash
# PM — 看板
gitlink-cli pm +dashboards --project 1 → show repo --tab projects
# Pipeline — 流水线
gitlink-cli pipeline +runs --owner jiangtx --repo gitlink-cli-demo --ref master --workflow build.yml
gitlink-cli show ci → .../actions (流水线页面)
# Label — 标签
gitlink-cli label +create -n "P0" -c "#FF0000" --owner jiangtx --repo gitlink-cli-demo
gitlink-cli show label → .../issues/labels
# Member — 成员
gitlink-cli member +list --owner jiangtx --repo gitlink-cli-demo
gitlink-cli show member → .../settings/collaboration
# Milestone — 里程碑
gitlink-cli milestone +create --owner jiangtx --repo gitlink-cli-demo --title "Sprint 6"
gitlink-cli show milestone → .../milestones
```
**第一类总计7 个新模块 × 每条命令对应网页 = 7 组终端↔网页映射**
---
## 第二类优化现有命令3 min
> **话术**: "不是新增,而是让现有命令更好用——三种输出格式、自动上下文推断、调试模式、中文帮助和错误提示。"
### A. 三种输出格式 — 同一命令,不同场景
```bash
# ── 终端: JSON 格式(给脚本和 AI Agent ──
gitlink-cli repo +list --format json
gitlink-cli show repo → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo
# ── 终端: Table 格式(给人看) ──
gitlink-cli repo +list --format table
# ── 终端: YAML 格式(给配置文件) ──
gitlink-cli repo +list --format yaml
# 【投影仪】GitLink 仓库列表页面,展示了相同的数据
```
### B. 自动上下文推断 — 零参数即可用
```bash
cd ~/gitlink-cli-demo
gitlink-cli issue +list
# → 自动从 git remote origin 解析 → jiangtx/gitlink-cli-demo
gitlink-cli show repo → 🔗 当前仓库主页
```
### C. 调试模式 — 完整请求链路可视化
```bash
gitlink-cli issue +list --debug
# → [DEBUG] GET https://gitlink.org.cn/api/v1/jiangtx/gitlink-cli-demo/issues.json?state=open&page=1&limit=20
# → [DEBUG] Authorization: Bearer ***
# → [DEBUG] Response 200 OK (234ms)
```
### D. 中文帮助 — 每个命令都有 API 端点 + 使用示例
```bash
gitlink-cli repo +languages --help
# → 中文描述 + API 端点: GET /:owner/:repo/languages + 使用示例
```
---
## 第三类批量操作能力3 min
> **话术**: "扩展前只能一个一个操作。扩展后支持批量、CSV文件导入、dry-run 安全预览。"
### A. 批量 Issue 操作
```bash
# ── 终端: dry-run 安全预览 ──
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli-demo \
--numbers 1,2,3,4,5 --dry-run
# → [DRY RUN] 将关闭以下 Issue: #1, #2, #3, #4, #5
# ── 终端: 确认执行 ──
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli-demo \
--numbers 1,2,3,4,5
# → ✓ 已关闭 5 个 Issue
# ── 终端: 查看网页验证 ──
gitlink-cli show repo --tab issues?state=closed
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues?state=closed
# 【投影仪】浏览器显示已关闭的 Issue 列表5个Issue 全部状态为 Closed
```
### B. 批量成员管理
```bash
# ── 终端: CSV 批量导入 ──
cat > members.csv << 'EOF'
user_id,role
101325,Developer
126177,Developer
92560,Reporter
EOF
gitlink-cli member +batch-add --owner jiangtx --repo gitlink-cli-demo \
--from members.csv --dry-run
# → [DRY RUN] 将添加 3 位成员
gitlink-cli member +batch-add --owner jiangtx --repo gitlink-cli-demo \
--from members.csv
# ── 终端: 网页验证 ──
gitlink-cli show member
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/collaboration
# 【投影仪】成员管理页面3个新成员已出现
```
### C. 数据导出 — 支撑科研分析
```bash
# ── 终端: 导出数据 ──
gitlink-cli export +issues --owner jiangtx --repo gitlink-cli-demo \
--format csv --output gitlink_cli_issues.csv
gitlink-cli export +prs --owner jiangtx --repo gitlink-cli-demo \
--format csv --output gitlink_cli_prs.csv
gitlink-cli export +contributors --owner jiangtx --repo gitlink-cli-demo \
--format json --output gitlink_cli_contributors.json
# ── 终端: 数据文件可直接导入 Python/R ──
head gitlink_cli_issues.csv
# → number,title,state,created_at,...
# 【投影仪】展示 CSV 文件在 Excel 中打开的效果
```
---
## 第四类:跨平台兼容 + 安装体验3 min
> **话术**: "扩展前仅 go install。扩展后 5 种安装方式,覆盖全平台全场景。"
### A. 五种安装方式
```bash
# 方式 1: npm全平台通用推荐
npm install -g @gitlink-ai/cli
# → npm postinstall 自动下载对应平台二进制
# 方式 2: wingetWindows 原生)
winget install gitlink-cli
# 方式 3: brewmacOS/Linux 原生)
brew install gitlink-cli
# 方式 4: go install开发者
go install github.com/gitlink-org/gitlink-cli@latest
# 方式 5: 一键脚本
curl -fsSL https://gitlink.org.cn/install.sh | bash # Linux/macOS
iwr -useb https://gitlink.org.cn/install.ps1 | iex # Windows
```
### B. 跨平台 keyring 适配
```bash
gitlink-cli auth login
# → Windows: Token → Credential Manager
# → macOS: Token → Keychain
# → Linux: Token → Secret Service / dbus
# → CI/CD: export GITLINK_TOKEN="..." (环境变量 fallback
gitlink-cli version
# → gitlink-cli v3.0.0 (windows/amd64)
```
```bash
# ── 网页对应: npm 包页面 ──
gitlink-cli show npm
# → 🔗 https://www.npmjs.com/package/@gitlink-ai/cli
# 【投影仪】展示 npm 包页面(下载量/版本号/README
```
### C. 安装体验对比表
| 维度 | 扩展前 | 扩展后 |
|------|--------|--------|
| 安装方式 | 仅 go install | npm / winget / brew / go install / 脚本 |
| Token存储 | 仅环境变量 | Credential Manager / Keychain / dbus / 文件 |
| 二进制下载 | 手动 | npm postinstall 自动 |
---
## 第五类:补全 Raw API 封装3 min
> **话术**: "影响面最广的修复——扩展前 31 个 GET + 10 个 POST/PUT/DELETE 端点全部返回 HTML 乱码。"
### A. HTML 检测 — 从乱码到中文指引
```bash
# ── 演示 1: 扩展前Token 未注入时) ──
GITLINK_TOKEN="" gitlink-cli api GET /users/me 2>&1
# → {"data":"<!doctype html>...<script>...</script></html>"}
# → 几千字符的 HTML 源码,完全无法理解
# ── 演示 2: 扩展后HTML 检测) ──
# (演示已修复的效果 — 说明 detectHTMLResponse() 的原理)
# 仅 ~30 行新增代码,覆盖 41 个端点
# → ❌ 服务器返回了 HTML 页面而非 JSON 数据
# → 可能原因:
# → 1. 未登录或 Token 已过期 → gitlink-cli auth login
# → 2. Token 权限不足 → 在 GitLink 平台重新生成
# → 3. API 端点不存在 → 检查路径
# → 4. 使用 Shortcut 命令替代 Raw API
```
### B. 端点 → Shortcut 映射 — 现场对比
```bash
# ── 终端: 扩展前(不可用,只能 Raw API + 大概率 HTML ──
# GITLINK_TOKEN="" gitlink-cli api GET /jiangtx/gitlink-cli-demo/languages → HTML
# GITLINK_TOKEN="" gitlink-cli api GET /jiangtx/gitlink-cli-demo/contributors → HTML
# GITLINK_TOKEN="" gitlink-cli api GET /jiangtx/gitlink-cli-demo/commits → HTML
# GITLINK_TOKEN="" gitlink-cli api GET /users/jiangtx/headmaps → HTML
# ── 终端: 扩展后(全部有对应 Shortcut ──
gitlink-cli repo +languages --owner jiangtx --repo gitlink-cli-demo
gitlink-cli repo +contributors --owner jiangtx --repo gitlink-cli-demo
gitlink-cli repo +commits --owner jiangtx --repo gitlink-cli-demo
gitlink-cli user +heatmap --login jiangtx
# → 全部正常返回 JSON
# ── 网页对应 ──
gitlink-cli show repo → .../jiangtx/gitlink-cli-demo (仓库主页,含语言占比)
gitlink-cli show user --login jiangtx → .../jiangtx (用户主页,含热力图)
```
### C. 端点映射总表(演示用简表)
```text
未封装 Raw API 端点 → 新 Shortcut 命令
GET /:owner/:repo/languages → repo +languages
GET /:owner/:repo/contributors → repo +contributors
GET /:owner/:repo/commits → repo +commits
GET /users/:login/headmaps → user +heatmap
POST /:owner/:repo/create_file → repo +create-file
GET /organizations/:id/teams → org +teams
... 共 56 条新命令,覆盖 ~40 个未封装端点
```
### D. Skills 修复 — Agent 技能从不可用到全部恢复
```text
修复前: skills/gitlink-repo/SKILL.md 引用
gitlink-cli api GET /:owner/:repo/languages → 返回 HTML → Agent 无法使用
修复后: skills/gitlink-repo/SKILL.md 引用
gitlink-cli repo +languages --owner <> --repo <> → 正常 JSON → Agent 可用
修复范围: 18 个 Skill 子目录,~29 个文件,~90 处引用
```
---
## 收尾:能力验证 + 验收清单2 min
### A. 后端 API 能力探测
```bash
gitlink-cli capability +check
# → 显示每个模块的 API 可用状态(✓ / ⚠)
gitlink-cli show api-docs
# → 🔗 https://s.apifox.cn/da30afb0-9d2e-429b-a4bc-a83209e06021
# 【投影仪】GitLink OpenAPI 文档页面
```
### B. 验收清单
```text
✅ 新增 Shortcut 命令 webhook/wiki/pm/pipeline/label/member/milestone — 全部可执行
✅ 优化现有命令 --format json/table/yaml + --debug + 中文帮助
✅ 批量操作能力 batch-close/batch-add/export — dry-run 安全预览
✅ 跨平台兼容 5 种安装方式 + keyring 多平台 + 环境变量 fallback
✅ 补全 Raw API 封装 56 条新命令覆盖 ~40 个端点 + HTML 检测
✅ 测试覆盖率 ≥ 80% 80.4% — CI 门禁通过
✅ Skills ~90 处修复 Agent 技能全部修复
```
### C. 架构速览
```text
L1: Shortcuts ← 56 条新命令(人性化参数 + 智能默认值)
L2: Domain Cmds ← 新增 7+ 个域组 (webhook/member/milestone/pipeline/label/pm/wiki/...)
L3: Raw API ← HTML 检测 + Skills 修复
```
---
## 演示前置条件检查
| # | 条件 | 状态 |
|---|------|:---:|
| 1 | `gitlink-cli.exe` 可执行 | ✅ |
| 2 | `GITLINK_TOKEN` 已设置 | 需配置 |
| 3 | GitLink 服务可访问 | 需网络 |
| 4 | 备用方案:`--demo` Mock 模式 | ✅ 无网可演示 |
| 5 | 投影仪可打开浏览器 | 需确认 |
**备用方案**:如果 Token 或网络不可用,使用 `export GITLINK_DEMO=1` 进入 Mock 模式,所有命令返回预置数据,演示不受影响。

View File

@ -0,0 +1,326 @@
# GitLink-CLI 数据分析与可视化管线analyze 模块)
## 一、设计目标
在现有 `export` 模块基础上,建立**端到端数据分析管线**,使 CLI 不仅是数据导出工具,
更是**科研级数据分析平台**。所有分析结果支持多格式输出,直接可用于论文。
## 二、为什么需要 analyze 模块?
### 2.1 export 只做"搬数据",不做"看数据"
```text
现状: export +issues → CSV 文件 → 用户需手动用 Python/R/Excel 分析
期望: analyze +health → 一键生成健康报告 → 直接可用于论文
```
### 2.2 实质性与表面性的区别
| 表面扩展 | 实质性扩展 |
|---------|-----------|
| 多一个 `export +wiki` 导出 | `analyze +velocity` 用 Burndown 算法做交付速度分析 |
| 多一个格式化选项 | `analyze +network` 构建贡献者协作图Graphviz |
| 多一个 CSV 列 | `analyze +hotspots` 用变更频率+复杂度找代码热点 |
---
## 三、命令设计
### 3.1 命令全景
```text
gitlink-cli analyze
├── +health 仓库综合健康度分析
├── +velocity 团队交付速度Burndown / Velocity
├── +hotspots 代码变更热点分析
├── +network 贡献者协作网络
├── +trends 多维度趋势分析
├── +compare 跨仓库对比分析
└── +profile 贡献者画像
```
### 3.2 各命令详解
#### A. analyze +health — 仓库健康度分析
```bash
# 基础用法
gitlink-cli analyze +health --owner jiangtx --repo gitlink-cli
# 输出
gitlink-cli analyze +health --owner jiangtx --repo gitlink-cli --format markdown
```
```markdown
## 📊 仓库健康报告: jiangtx/gitlink-cli
**生成时间**: 2026-06-08 15:30 CST
### 活跃度指标
| 指标 | 数值 | 评级 |
|------|------|------|
| 近30天提交数 | 127 | 🟢 活跃 |
| 近30天 Issue 创建 | 23 | 🟢 正常 |
| 近30天 PR 合并 | 18 | 🟢 正常 |
| Issue 平均关闭时间 | 3.2 天 | 🟢 健康 |
| PR 平均审查时间 | 1.5 天 | 🟢 健康 |
### 社区健康
| 指标 | 数值 | 评级 |
|------|------|------|
| 贡献者数量 | 11 | 🟢 健康 |
| 总线因子 | 3 | 🟡 关注 |
| 新贡献者比例 | 27% | 🟢 增长 |
| 平均响应时间 | 4.2 小时 | 🟢 快速 |
### 风险信号
- ⚠ 总线因子偏低3/112 个主要贡献者贡献了 61% 的代码
### 建议
1. 鼓励代码评审轮换,降低总线因子
2. 为新贡献者添加更多 `good first issue` 标签
---
*数据来源: GitLink API. 方法: 加权移动平均.*
```
#### B. analyze +velocity — 团队交付速度
```bash
# Sprint 燃尽图数据JSON 格式,可导入绘图工具)
gitlink-cli analyze +velocity --owner jiangtx --repo gitlink-cli \
--milestone "Sprint 5" --format json
# 输出: Burndown 数据点
```
```json
{
"ok": true,
"data": {
"milestone": "Sprint 5",
"start_date": "2026-05-25",
"end_date": "2026-06-08",
"total_points": 120,
"burndown": [
{"date": "2026-05-25", "remaining": 120, "ideal": 120},
{"date": "2026-05-27", "remaining": 105, "ideal": 104},
{"date": "2026-05-29", "remaining": 88, "ideal": 88},
{"date": "2026-06-01", "remaining": 62, "ideal": 64},
{"date": "2026-06-03", "remaining": 45, "ideal": 48},
{"date": "2026-06-05", "remaining": 28, "ideal": 32},
{"date": "2026-06-08", "remaining": 5, "ideal": 8}
],
"velocity": {
"average": 18.4,
"trend": "stable",
"completion_rate": 95.8
},
"forecast": {
"estimated_completion": "2026-06-09",
"confidence": 0.92
}
}
}
```
#### C. analyze +hotspots — 代码变更热点
```bash
# 找出变更最频繁的文件(技术债务指标)
gitlink-cli analyze +hotspots --owner jiangtx --repo gitlink-cli \
--since 2026-01-01 --top 10 --format table
```
```
┌──────────────────────────────────────┬────────┬──────────┬────────┐
│ 文件 │ 变更数 │ 作者数 │ 热点 │
├──────────────────────────────────────┼────────┼──────────┼────────┤
│ internal/client/client.go │ 47 │ 5 │ 🔴 高 │
│ shortcuts/repo/repo.go │ 38 │ 4 │ 🔴 高 │
│ shortcuts/issue/issue.go │ 31 │ 3 │ 🟡 中 │
│ cmd/root.go │ 25 │ 3 │ 🟡 中 │
│ internal/output/envelope.go │ 18 │ 2 │ 🟢 低 │
│ shortcuts/pr/pr.go │ 16 │ 3 │ 🟢 低 │
│ internal/auth/token_store.go │ 15 │ 2 │ 🟢 低 │
│ shortcuts/common/runner.go │ 12 │ 2 │ 🟢 低 │
│ internal/config/config.go │ 10 │ 1 │ 🟢 低 │
│ shortcuts/member/member.go │ 9 │ 2 │ 🟢 低 │
└──────────────────────────────────────┴────────┴──────────┴────────┘
```
#### D. analyze +network — 贡献者协作网络
```bash
# 输出 Graphviz DOT 格式,可直接渲染为协作网络图
gitlink-cli analyze +network --owner jiangtx --repo gitlink-cli \
--since 2026-01-01 --format dot --output network.dot
# 渲染为 PNG
dot -Tpng network.dot -o network.png
```
#### E. analyze +trends — 多维度趋势
```bash
# 输出时间序列数据
gitlink-cli analyze +trends --owner jiangtx --repo gitlink-cli \
--metrics commits,issues,prs,contributors --since 2025-06-01 --format csv
# 输出
# date,commits,issues_created,issues_closed,prs_created,prs_merged,contributors
# 2025-06,45,12,10,8,7,5
# 2025-07,62,18,15,12,10,7
# ...
```
#### F. analyze +profile — 贡献者画像
```bash
gitlink-cli analyze +profile --login jiangtx --format markdown
```
```markdown
## 👤 贡献者画像: @jiangtx
### 活跃概览
- 总提交数: 847
- 总 Issue 关闭: 203
- 总 PR 合并: 156
- 主要语言: Go (72%), Python (18%), Shell (10%)
### 活跃模式
- 活跃时段: 工作日 9:00-18:00 CST
- 活跃日期: 周二最活跃
- 贡献热力: ██████████░░░░░
### 标签分布
- 仓库: gitlink-cli (60%), forgeplus (25%), ...
- 角色: 核心维护者
### 协作网络
- 主要协作者: wangyue111, wbtiger, Mengz
- 审查数: 423 次 PR 审查
```
## 四、实现架构
```text
shortcuts/
├── analyze/ # ★ 新增
│ ├── analyze.go # Shortcuts 定义
│ ├── analyze_test.go
│ ├── health.go # 健康度计算引擎
│ ├── health_test.go
│ ├── velocity.go # Burndown 算法
│ ├── hotspots.go # 热点分析(变更频率 × 复杂度)
│ ├── network.go # 协作网络Graphviz DOT 生成)
│ ├── trends.go # 时间序列聚合
│ ├── profile.go # 贡献者画像
│ └── engine/ # 分析引擎子包
│ ├── metrics.go # 通用指标计算
│ ├── aggregator.go # 数据聚合
│ ├── scorer.go # 评分算法(加权、归一化)
│ └── forecast.go # 简单预测(移动平均、趋势推断)
```
### 核心算法
```go
// engine/scorer.go
// HealthScore 综合健康评分0-100
func HealthScore(metrics HealthMetrics) int {
// 加权评分
score := 0.0
score += float64(metrics.ActivityScore) * 0.30 // 活跃度权重 30%
score += float64(metrics.CommunityScore) * 0.25 // 社区健康 25%
score += float64(metrics.QualityScore) * 0.25 // 代码质量 25%
score += float64(metrics.ResponsivenessScore) * 0.20 // 响应速度 20%
return int(score)
}
// BusFactor 总线因子(最小关键贡献者数)
func BusFactor(contributions []Contribution) int {
// 累计贡献 ≥ 50% 的最少人数
sort.Slice(contributions, func(i, j int) bool {
return contributions[i].Percentage > contributions[j].Percentage
})
sum := 0.0
for i, c := range contributions {
sum += c.Percentage
if sum >= 50.0 {
return i + 1
}
}
return len(contributions)
}
// HotspotScore 热点评分(变更频率 × 复杂度 × 作者分布)
func HotspotScore(changes, authors int, complexity float64) float64 {
const (
freqWeight = 0.5
authorWeight = 0.3
complexWeight = 0.2
)
freqScore := math.Min(float64(changes)/50.0, 1.0)
authorScore := 1.0 - math.Min(float64(authors-1)/10.0, 1.0)
complexScore := math.Min(complexity/1000.0, 1.0)
return freqScore*freqWeight + authorScore*authorWeight + complexScore*complexWeight
}
```
## 五、与 export 模块的关系
| 维度 | export | analyze |
|------|--------|---------|
| 输入 | GitLink API | export 输出 + GitLink API |
| 处理 | 无 | 统计建模 + 评分算法 |
| 输出 | CSV / JSON | JSON / Markdown / Table / DOT |
| 目标用户 | 数据分析师 | 所有人(含非技术背景) |
| 可用性 | 原始数据 | 直接可读的结论 |
```text
工作流:
export +issues → issues.csv ──┐
export +prs → prs.csv ────────┤
export +contributors → ... ───┤
┌───────────────▼────────────────┐
│ analyze +health │
│ analyze +velocity │
│ analyze +hotspots ← 读取 CSV │
│ analyze +network │
└───────────────┬────────────────┘
┌───────────────▼────────────────┐
│ 输出: 报告 / 图表 / 数据 │
│ 直接用于论文 / 演示 / 报告 │
└────────────────────────────────┘
```
## 六、演示场景
```bash
# 演示 1: 仓库健康报告(配合 workflow +health
gitlink-cli analyze +health --owner jiangtx --repo gitlink-cli --format markdown
# 演示 2: 导出 + 分析流水线(展示科研价值)
gitlink-cli export +issues --owner jiangtx --repo gitlink-cli --output issues.csv
gitlink-cli export +prs --owner jiangtx --repo gitlink-cli --output prs.csv
gitlink-cli analyze +velocity --from issues.csv,prs.csv --format json > velocity.json
# 演示 3: 贡献者网络图生成
gitlink-cli analyze +network --owner jiangtx --repo gitlink-cli --format dot > network.dot
echo "可使用 Graphviz 渲染: dot -Tpng network.dot -o network.png"
```
## 七、实质性价值
| 功能 | 解决的实质问题 |
|------|--------------|
| `analyze +health` | 项目管理者从"凭感觉"到"看数据"做决策 |
| `analyze +velocity` | Sprint 回顾会议有量化数据支撑 |
| `analyze +hotspots` | Code Review 优先级有数据依据 |
| `analyze +network` | 识别社区核心人物和孤立贡献者 |
| `analyze +trends` | 为论文提供时间序列分析数据 |
| `analyze +profile` | 为社区运营提供个人层面洞察 |

View File

@ -0,0 +1,214 @@
# GitLink-CLI 项目上下文管理系统
## 一、设计目标
建立**持久化项目工作上下文**,使开发者能在多个项目间快速切换,
一键恢复上次工作状态。解决"每天开始工作要重新回忆上下文"的痛点。
## 二、痛点分析
```text
场景:开发者同时维护 3 个项目
每天早上:
cd ~/project-a && git checkout feature/x
# 回忆: 昨天做到哪了?哪个 Issue哪个 PR
# 回忆: 这个项目的 GitLink 链接是什么?
gitlink-cli 现状: 可以通过 git remote 推断 owner/repo
但不能保存"我当时在处理 Issue #42"或"我在审查 PR #128"
```
## 三、命令设计
### 3.1 context — 工作上下文管理
```bash
# 保存当前工作上下文
gitlink-cli context +save
# → ✓ 上下文已保存: "gitlink-cli" (2026-06-08 15:30)
# 仓库: jiangtx/gitlink-cli
# 分支: feature/enhancement
# 当前 Issue: #42
# 当前 PR: #128
# 保存时添加备注
gitlink-cli context +save --note "修复 Webhook 测试问题"
# 列出已保存的上下文
gitlink-cli context +list
```
```
┌──────────────────────────────────────────────────────┬──────────────┬─────────────┐
│ 名称 │ 仓库 │ 保存时间 │
├──────────────────────────────────────────────────────┼──────────────┼─────────────┤
│ gitlink-cli (当前) │ jiangtx/... │ 15:30 今天 │
│ forgeplus │ Gitlink/... │ 09:00 昨天 │
│ help-center │ jiangtx/... │ 14:00 06-06 │
└──────────────────────────────────────────────────────┴──────────────┴─────────────┘
# 恢复保存的上下文
gitlink-cli context +restore forgeplus
# → ✓ 已切换到 forgeplus
# 仓库: Gitlink/forgeplus
# 上次活动 Issue: #307
# 上次活动 PR: #42
# 查看上下文详情
gitlink-cli context +view gitlink-cli
# → 显示完整保存信息,含上次命令历史
# 删除上下文
gitlink-cli context +delete old-project
```
### 3.2 project — 项目配置管理
```bash
# 初始化项目配置(在当前仓库根目录创建 .gitlink.yml
gitlink-cli project +init
# → ✓ 已创建 .gitlink.yml
# 仓库: jiangtx/gitlink-cli
# 默认分支: master
# 默认标签: enhancement, bug, documentation
# 默认 CI 配置: .devops/*.yml
# .gitlink.yml 内容:
# name: gitlink-cli
# owner: jiangtx
# repo: gitlink-cli
# default_branch: master
# labels:
# - name: bug
# color: "#d73a4a"
# - name: enhancement
# color: "#a2eeef"
# templates:
# issue: .gitlink/ISSUE_TEMPLATE.md
# pr: .gitlink/PR_TEMPLATE.md
# 查看项目配置
gitlink-cli project +view
# → 显示 .gitlink.yml 内容
# 编辑项目配置
gitlink-cli project +edit
# → 打开 $EDITOR 编辑 .gitlink.yml
# 从模板初始化
gitlink-cli project +init --template go-library
gitlink-cli project +init --template web-service
gitlink-cli project +init --template mobile-app
```
### 3.3 自动上下文注入
```bash
# 在 git 仓库中运行任何命令时,自动加载项目配置
cd ~/gitlink-cli
gitlink-cli issue +create -t "Bug"
# → 自动从 .gitlink.yml 读取 default_branch, labels, templates
# → 自动从 context 读取"上次处理的 Issue"
# 切换分支时自动提示上下文
cd ~/forgeplus && git checkout feature/x
# gitlink-cli 检测到分支变化 → 提示:
# 检测到分支切换: master → feature/x
# 上次在此分支的工作: Issue #307, PR #42
# 恢复上下文? [Y/n]
```
## 四、实现架构
```text
internal/
├── context/
│ ├── repo.go # 现有git remote 解析
│ ├── repo_test.go
│ ├── session.go # ★ 新增:工作会话管理
│ ├── session_test.go
│ └── storage.go # ★ 新增:上下文持久化
├── project/ # ★ 新增
│ ├── config.go # .gitlink.yml 解析/写入
│ ├── config_test.go
│ ├── template.go # 模板管理
│ └── template_test.go
cmd/
├── context/ # ★ 新增
│ ├── context.go # context 命令组
│ └── context_test.go
└── project/ # ★ 新增
├── project.go # project 命令组
└── project_test.go
```
### 核心数据结构
```go
// internal/context/session.go
type WorkSession struct {
Name string `yaml:"name"` // 会话名(默认=仓库名)
Owner string `yaml:"owner"`
Repo string `yaml:"repo"`
Branch string `yaml:"branch"`
IssueID int `yaml:"issue_id,omitempty"`
PRID int `yaml:"pr_id,omitempty"`
Note string `yaml:"note,omitempty"`
SavedAt time.Time `yaml:"saved_at"`
LastCmd string `yaml:"last_command,omitempty"`
}
// 存储路径: ~/.config/gitlink-cli/contexts.yaml
```
```go
// internal/project/config.go
type ProjectConfig struct {
Name string `yaml:"name"`
Owner string `yaml:"owner"`
Repo string `yaml:"repo"`
DefaultBranch string `yaml:"default_branch"`
Labels []LabelDef `yaml:"labels,omitempty"`
Templates TemplateConfig `yaml:"templates,omitempty"`
CI CIConfig `yaml:"ci,omitempty"`
}
// 存储路径: <repo_root>/.gitlink.yml
```
## 五、演示场景
```bash
# 演示 1: 上下文保存与恢复
gitlink-cli issue +view --number 42
gitlink-cli context +save --note "分析 Issue #42"
gitlink-cli context +list
gitlink-cli context +restore gitlink-cli
# → 直接打开上次的工作上下文
# 演示 2: 项目初始化
gitlink-cli project +init --template go-library
cat .gitlink.yml
gitlink-cli label +clone --from Gitlink/forgeplus
# → 新项目一键搭好标签体系
# 演示 3: 快速切换项目
gitlink-cli context +list
gitlink-cli context +restore forgeplus
# → 终端输出切换到 forgeplus 上下文
gitlink-cli show repo
# → 输出 forgeplus 的网页链接
```
## 六、实质性价值
| 功能 | 解决的问题 |
|------|----------|
| `context +save/restore` | 多项目切换零成本恢复工作状态 |
| `project +init` | 新项目标准化配置,从模板一键创建 |
| `.gitlink.yml` | CI/CD/Issue/PR 配置版本化管理 |
| 自动上下文提示 | 分支切换时自动提示上次工作状态 |
| 会话历史 | 跨天/跨周工作无需重新回忆上下文 |

View File

@ -0,0 +1,329 @@
# GitLink-CLI 演示模式Demo Mode设计
## 一、问题分析
### 1.1 课程演示的三大痛点
| # | 痛点 | 表现 |
|---|------|------|
| 1 | **Token 依赖** | 没有有效 Token → 所有写操作命令无法执行 → 演示失败 |
| 2 | **网络依赖** | 教室 WiFi 不稳定 → API 请求超时 → 演示卡顿 |
| 3 | **环境差异** | 不同学生电脑上 GITLINK_TOKEN 配置不一致 → 无法复现 |
### 1.2 现有方案的局限
展示方案文档中所有演示命令都依赖真实的 GitLink 服务器:
```bash
# 这些全部需要 API 调用成功
gitlink-cli repo +languages --owner jiangtx --repo gitlink-cli-demo
gitlink-cli webhook +list --owner jiangtx --repo gitlink-cli-demo
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli-demo --numbers 1,2,3
```
**一旦 Token 过期或网络断开 → 整个演示失败。**
---
## 二、解决方案Mock 模式 + 演示脚本
### 2.1 核心设计
```text
┌──────────────────────┐
│ gitlink-cli ... │
│ 用户命令 │
└──────────┬───────────┘
┌──────────▼───────────┐
│ 检测 --demo flag │
│ 或 GITLINK_DEMO=1 │
└──────────┬───────────┘
┌────────────────┼────────────────┐
│ │ │
┌────────▼──────┐ ┌──────▼──────┐ ┌─────▼──────┐
│ Demo Mode: ON │ │ Demo Mode: ON│ │ Normal Mode│
│ (读操作) │ │ (写操作) │ │ │
│ │ │ │ │ │
│ 返回预置 JSON │ │ Dry-run + │ │ 真实 API │
│ 数据(来自 │ │ 模拟成功 │ │ 调用 │
│ fixtures/ │ │ 响应 │ │ │
└───────────────┘ └─────────────┘ └────────────┘
```
### 2.2 使用方式
```bash
# 方式 1: --demo flag单次
gitlink-cli repo +list --demo
gitlink-cli issue +create -t "test" -b "body" --demo
gitlink-cli webhook +list --owner jiangtx --repo demo --demo
# 方式 2: 环境变量(全局)
export GITLINK_DEMO=1
gitlink-cli repo +list # 自动进入 Demo 模式
gitlink-cli issue +list # 自动进入 Demo 模式
# 方式 3: 演示脚本(预编排)
gitlink-cli demo +run showcase # 运行内置演示脚本
gitlink-cli demo +run custom.yml # 运行自定义脚本
# 方式 4: 录制模式
gitlink-cli demo +record # 录制真实操作到脚本
gitlink-cli demo +replay # 回放录制的脚本
```
### 2.3 预置演示数据fixtures/
```text
internal/demo/
├── demo.go # Demo 模式核心逻辑
├── demo_test.go
├── fixtures/ # 预置响应数据
│ ├── repo_list.json # repo +list 响应
│ ├── repo_languages.json # repo +languages 响应
│ ├── issue_list.json # issue +list 响应
│ ├── issue_create.json # issue +create 响应
│ ├── pr_list.json # pr +list 响应
│ ├── webhook_list.json # webhook +list 响应
│ ├── webhook_create.json # webhook +create 响应
│ ├── label_list.json # label +list 响应
│ ├── member_list.json # member +list 响应
│ ├── pipeline_list.json # pipeline +list 响应
│ ├── pm_dashboards.json # pm +dashboards 响应
│ ├── wiki_pages.json # wiki +pages 响应
│ ├── user_me.json # user +me 响应
│ ├── user_heatmap.json # user +heatmap 响应
│ ├── notification_list.json # notification +list 响应
│ └── search_code.json # search +code 响应
└── showcases/ # 演示脚本
├── showcase.yml # 默认演示:涵盖所有新模块
├── quick-tour.yml # 快速导览5 分钟版
└── deep-dive.yml # 深度演示20 分钟版
```
### 2.4 演示脚本格式showcase.yml
```yaml
# demo/showcases/showcase.yml
name: "GitLink-CLI 全功能展示"
description: "覆盖所有新增模块的完整演示"
version: "1.0"
estimated_time: "20min"
steps:
- group: "开场"
steps:
- command: "version"
description: "显示版本信息"
- command: "--help"
description: "展示命令全景"
- command: "user +me"
description: "验证用户身份"
- group: "新增 Shortcut 命令"
steps:
- command: "webhook +list --owner jiangtx --repo gitlink-cli-demo"
description: "Webhook 管理(从无到有)"
- command: "webhook +create --owner jiangtx --repo gitlink-cli-demo --url https://example.com --events push"
description: "创建 Webhook"
highlight: true
- command: "label +list --owner jiangtx --repo gitlink-cli-demo"
description: "标签管理 CRUD"
- command: "label +create -n bug -c '#d73a4a' --owner jiangtx --repo gitlink-cli-demo"
description: "创建标签(自定义颜色)"
- command: "notification +list"
description: "通知中心"
- command: "pm +dashboards --project 1"
description: "项目管理看板"
- command: "wiki +pages --owner jiangtx --repo gitlink-cli-demo"
description: "Wiki 文档管理"
- group: "优化现有命令"
steps:
- command: "repo +list --format json"
description: "JSON 格式输出"
- command: "repo +list --format table"
description: "Table 格式输出"
- command: "repo +list --format yaml"
description: "YAML 格式输出"
- command: "issue +list --debug"
description: "调试模式 — 查看完整请求链路"
- group: "批量操作"
steps:
- command: "issue +batch-close --numbers 1,2,3,4,5 --dry-run"
description: "批量操作安全预览"
- command: "member +batch-add --user-ids 101,102,103 --dry-run"
description: "批量添加成员预览"
- command: "export +issues --format csv --output demo_issues.csv"
description: "数据导出 — 支撑科研分析"
- group: "跨平台体验"
steps:
- command: "status"
description: "登录状态 + 平台信息"
- command: "browse"
description: "打开 GitLink 网页端"
- command: "show repo"
description: "获取仓库网页链接"
- group: "工作流 Agent"
steps:
- command: "workflow +health --owner jiangtx --repo gitlink-cli --format table"
description: "仓库健康检查"
- command: "workflow +triage --title 'Bug: crash' --body 'app crashes on start' --format markdown"
description: "Issue 自动分类"
- command: "workflow +repo-report --owner jiangtx --repo gitlink-cli --format markdown"
description: "仓库综合报告"
- group: "收尾"
steps:
- command: "capability +summary"
description: "API 能力探测结果"
- command: "--help"
description: "完整命令全景"
```
---
## 三、核心实现
### 3.1 Demo Mode Transport
```go
// internal/demo/demo.go
// Transport 实现 http.RoundTripper 接口,替代真实网络调用
type Transport struct {
fixtures map[string][]byte // 路径 → JSON 响应
recorder *Recorder // 可选:录制模式
}
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
// 1. 构建 fixture key: "GET:/api/v1/jiangtx/demo/issues.json?page=1"
key := buildFixtureKey(req)
// 2. 查找预置响应
if data, ok := t.fixtures[key]; ok {
return mockResponse(200, data), nil
}
// 3. 模糊匹配(忽略查询参数变化)
if data, ok := t.fuzzyMatch(key); ok {
return mockResponse(200, data), nil
}
// 4. 录制模式:转发真实请求并记录响应
if t.recorder != nil {
return t.recorder.RoundTrip(req)
}
// 5. 默认响应
return mockResponse(200, []byte(`{"ok":true,"data":{}}`)), nil
}
```
### 3.2 Demo 命令
```go
// cmd/demo/demo.go
func NewDemoCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "demo",
Short: "演示模式 — 无需 Token 即可展示全部功能",
}
cmd.AddCommand(
newDemoRunCmd(), // demo +run <showcase>
newDemoRecordCmd(), // demo +record
newDemoReplayCmd(), // demo +replay
)
return cmd
}
```
---
## 四、演示脚本运行器
### 4.1 交互式执行
```bash
gitlink-cli demo +run showcase
```
输出效果:
```text
╔══════════════════════════════════════════════╗
║ GitLink-CLI 全功能展示 ║
║ 预计时长: 20min | 步数: 30 ║
╚══════════════════════════════════════════════╝
━━━ 开场 (2/2) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[1/30] ▶ gitlink-cli version
gitlink-cli v3.0.0 (windows/amd64)
[2/30] ▶ gitlink-cli user +me
{
"ok": true,
"data": {
"login": "jiangtx",
"user_id": 148911
}
}
━━━ 新增 Shortcut 命令 (7/7) ━━━━━━━━━━━━━━━━━━
[3/30] ▶ gitlink-cli webhook +list ...
{
"ok": true,
"data": { "total_count": 1, "webhooks": [...] }
}
🔗 网页对应: https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/hooks
[按 Enter 继续下一步, 输入 s 跳过本组, 输入 q 退出]
```
### 4.2 自动执行模式
```bash
# 自动执行,每步间隔 3 秒
gitlink-cli demo +run showcase --auto --delay 3s
# 只执行指定分组
gitlink-cli demo +run showcase --group "新增 Shortcut 命令"
# 列出所有可用演示脚本
gitlink-cli demo +run --list
```
---
## 五、对展示方案的增量价值
| 原方案痛点 | Demo Mode 解决方案 |
|-----------|-------------------|
| Token 必需 → 演示脆弱 | `--demo` flag / `GITLINK_DEMO=1` → 零依赖 |
| 网络不稳定 → 演示卡顿 | Mock Transport → 本地响应,零延迟 |
| 只能看终端输出 → 不直观 | `show` + `browse` → 终端+网页联动 |
| 演示步骤多 → 容易出错 | `demo +run showcase.yml` → 预编排脚本 |
| 无法复现 → 学生无法练习 | `demo +replay` → 一键复现 |
| 演示过程无记录 → 无法回顾 | `demo +record` → 录制真实操作 |
---
## 六、实现优先级
| 优先级 | 功能 | 理由 |
|--------|------|------|
| P0 | `--demo` flag + Mock Transport | 核心演示基础设施,无此无法脱网演示 |
| P0 | `fixtures/` 预置数据 | 覆盖所有新模块的关键命令 |
| P1 | `demo +run` 演示脚本 | 按步执行、分组展示 |
| P1 | `show` 命令 | 终端↔网页映射的核心入口 |
| P2 | `--web` 全局 flag | 自动打开浏览器的便利功能 |
| P2 | `demo +record/replay` | 录制和回放 |
| P3 | 自动执行 + 延迟 | 无人值守的全自动演示 |

View File

@ -0,0 +1,310 @@
# GitLink-CLI 终端↔网页功能对应展示系统
## 一、设计目标
使每个 CLI 命令都能**直观对应**到 GitLink 网页上的一个或多个页面,
支撑课程演示中"终端操作"与"网页效果"的同步对比展示。
### 核心原则
| # | 原则 | 说明 |
|---|------|------|
| 1 | **一对多映射** | 一个 CLI 命令可以对应多个网页资源 |
| 2 | **零额外参数** | 从命令上下文自动推断 URL |
| 3 | **不打断流程** | `--web` 可附加到任何命令,自动打开对应网页 |
| 4 | **可演示性** | `show` 命令精确打印 URL用于投影展示 |
| 5 | **AI Agent 友好** | 结构化输出中包含 `html_url` 字段 |
---
## 二、新增命令体系
### 2.1 `show` — 打印网页 URL不打开
```text
gitlink-cli show <resource> [flags]
核心设计理念:终端操作完,直接用 show 获取对应网页链接。
适用于演示场景——先在终端执行,再投影网页效果。
```
```bash
# 显示仓库主页 URL
gitlink-cli show repo
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo
# 显示 Issue #42 的 URL
gitlink-cli show issue --number 42
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues/42
# 显示 PR 网页 URL
gitlink-cli show pr --number 128
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/pulls/128
# 显示 Wiki 页面 URL
gitlink-cli show wiki --page "API 使用指南"
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/wiki/API%20使用指南
# 显示成员管理页 URL
gitlink-cli show member
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/collaboration
# 显示 Webhook 设置页 URL
gitlink-cli show webhook
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/hooks
# 显示标签管理页 URL
gitlink-cli show label
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/labels
# 显示里程碑页 URL
gitlink-cli show milestone
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/milestones
# 输出为 JSON给 AI Agent 用)
gitlink-cli show issue --number 42 --format json
# → {"resource":"issue","number":42,"html_url":"https://...","cli_command":"..."}
```
**子命令全景**
| 子命令 | URL 模板 | 需要参数 |
|--------|---------|---------|
| `show repo` | `/{owner}/{repo}` | --owner, --repo |
| `show issue` | `/{owner}/{repo}/issues/{number}` | --number |
| `show pr` | `/{owner}/{repo}/pulls/{number}` | --number |
| `show wiki` | `/{owner}/{repo}/wiki/{page}` | --page |
| `show member` | `/{owner}/{repo}/settings/collaboration` | --owner, --repo |
| `show webhook` | `/{owner}/{repo}/settings/hooks` | --owner, --repo |
| `show label` | `/{owner}/{repo}/issues/labels` | --owner, --repo |
| `show milestone` | `/{owner}/{repo}/milestones` | --owner, --repo |
| `show branch` | `/{owner}/{repo}/branches/{branch}` | --branch |
| `show release` | `/{owner}/{repo}/releases/{tag}` | --tag |
| `show ci` | `/{owner}/{repo}/actions` | --owner, --repo |
| `show commit` | `/{owner}/{repo}/commits/{sha}` | --sha |
### 2.2 `--web` 全局 Flag — 执行命令后自动打开网页
```bash
# 创建 Issue 后自动在浏览器打开
gitlink-cli issue +create -t "Bug" -b "描述..." --web
# 创建 Webhook 后打开 Webhook 设置页
gitlink-cli webhook +create --url https://example.com --events push --web
# 创建 PR 后打开 PR 页面
gitlink-cli pr +create -t "feat: new" --head feature/x --base master --web
# 查看 Issue 的同时打开网页
gitlink-cli issue +view --number 42 --web
```
**`--web` 工作流程**
```
用户命令 → Shortcut.Run() → API调用成功
检测 --web flag → 从响应提取 resource_id
构建 GitLink URL → 尝试打开浏览器
输出追加: "🔗 网页链接: https://gitlink.org.cn/..."
```
### 2.3 `browse` 命令增强
```bash
# 现有功能保留
gitlink-cli browse # 打开仓库主页
gitlink-cli browse issues/42 # 打开 Issue
gitlink-cli browse pulls/128 # 打开 PR
gitlink-cli browse wiki # 打开 Wiki
# 新增功能
gitlink-cli browse --list # 列出当前仓库所有可浏览的页面
gitlink-cli browse --no-open # 只打印 URL不打开浏览器
gitlink-cli browse releases/v2.0 # 打开 Release 页面
gitlink-cli browse settings # 打开仓库设置页
gitlink-cli browse commits/abc1234 # 打开指定提交
```
### 2.4 输出增强:每个命令的数据自动附带 `html_url`
```json
{
"ok": true,
"data": {
"id": 42,
"number": 42,
"title": "Bug: Login failed",
"state": "open",
"html_url": "https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues/42"
},
"meta": {
"show_url": "gitlink-cli show issue --number 42"
}
}
```
`html_url` 字段由 `internal/output/envelope.go``SuccessEnvelope` 自动注入,
前提是响应数据中包含 `id`/`number` 和 `owner`/`repo` 上下文。
---
## 三、URL 模式映射总表
### 3.1 GitLink 网页 URL 模式
| 资源类型 | URL 模式 | CLI 操作 | Show 命令 |
|---------|---------|---------|----------|
| 仓库主页 | `https://gitlink.org.cn/{owner}/{repo}` | repo +info | `show repo` |
| 仓库设置 | `.../{owner}/{repo}/settings` | — | `show repo --settings` |
| Issue 列表 | `.../{owner}/{repo}/issues` | issue +list | `show repo --tab issues` |
| Issue 详情 | `.../{owner}/{repo}/issues/{number}` | issue +view | `show issue --number N` |
| New Issue | `.../{owner}/{repo}/issues/new` | issue +create | `show issue --new` |
| PR 列表 | `.../{owner}/{repo}/pulls` | pr +list | `show repo --tab pulls` |
| PR 详情 | `.../{owner}/{repo}/pulls/{number}` | pr +view | `show pr --number N` |
| New PR | `.../{owner}/{repo}/pulls/new` | pr +create | `show pr --new` |
| Wiki 首页 | `.../{owner}/{repo}/wiki` | wiki +pages | `show wiki` |
| Wiki 页面 | `.../{owner}/{repo}/wiki/{page}` | wiki +get | `show wiki --page P` |
| 标签管理 | `.../{owner}/{repo}/issues/labels` | label +list | `show label` |
| 里程碑 | `.../{owner}/{repo}/milestones` | milestone +list | `show milestone` |
| 成员管理 | `.../{owner}/{repo}/settings/collaboration` | member +list | `show member` |
| Webhook | `.../{owner}/{repo}/settings/hooks` | webhook +list | `show webhook` |
| 分支列表 | `.../{owner}/{repo}/branches` | branch +list | `show branch` |
| Release | `.../{owner}/{repo}/releases/{tag}` | release +view | `show release --tag T` |
| CI/CD | `.../{owner}/{repo}/actions` | ci +list | `show ci` |
| 提交详情 | `.../{owner}/{repo}/commits/{sha}` | repo +commits | `show commit --sha S` |
| 对比页面 | `.../{owner}/{repo}/compare/{base}...{head}` | compare +view | `show compare --base B --head H` |
| 项目管理 | `.../{owner}/{repo}/projects` | pm +dashboards | `show repo --tab projects` |
| 通知中心 | `https://gitlink.org.cn/notifications` | notification +list | `show notification` |
| 组织页面 | `https://gitlink.org.cn/{org}` | org +info | `show org --name N` |
| 用户主页 | `https://gitlink.org.cn/{user}` | user +info | `show user --login L` |
---
## 四、实现架构
### 4.1 新增文件
```text
gitlink-cli/
├── cmd/
│ ├── show/ # ★ 新增
│ │ ├── show.go # show 主命令
│ │ ├── show_test.go
│ │ ├── url_patterns.go # URL 模式注册表
│ │ └── url_patterns_test.go
│ └── browse/
│ ├── browse.go # 增强(增加 --list, --no-open, 更多资源类型)
│ └── browse_test.go # 增强
├── internal/
│ ├── web/ # ★ 新增
│ │ ├── url_builder.go # GitLink URL 构造器
│ │ ├── url_builder_test.go
│ │ ├── browser.go # 跨平台浏览器打开(从 browse 迁移)
│ │ └── browser_test.go
│ └── output/
│ └── envelope.go # 增强(注入 html_url
```
### 4.2 核心接口设计
```go
// internal/web/url_builder.go
// ResourceURL 表示一个 GitLink 网页资源 URL
type ResourceURL struct {
URL string `json:"html_url"` // 完整 URL
Resource string `json:"resource"` // 资源类型: issue, pr, repo, wiki...
Identifier string `json:"identifier"` // 资源标识: issue#42, pr#128...
CLICommand string `json:"show_command"` // 对应的 show 命令
}
// Builder 构造 GitLink 网页 URL
type Builder struct {
BaseURL string // https://gitlink.org.cn
}
func (b *Builder) IssueURL(owner, repo string, number int) *ResourceURL
func (b *Builder) PRURL(owner, repo string, number int) *ResourceURL
func (b *Builder) RepoURL(owner, repo string) *ResourceURL
func (b *Builder) WikiURL(owner, repo, page string) *ResourceURL
func (b *Builder) MemberURL(owner, repo string) *ResourceURL
func (b *Builder) WebhookURL(owner, repo string) *ResourceURL
func (b *Builder) LabelURL(owner, repo string) *ResourceURL
func (b *Builder) MilestoneURL(owner, repo string) *ResourceURL
func (b *Builder) BranchURL(owner, repo, branch string) *ResourceURL
func (b *Builder) ReleaseURL(owner, repo, tag string) *ResourceURL
func (b *Builder) CommitURL(owner, repo, sha string) *ResourceURL
func (b *Builder) CIURL(owner, repo string) *ResourceURL
func (b *Builder) CompareURL(owner, repo, base, head string) *ResourceURL
func (b *Builder) OrgURL(org string) *ResourceURL
func (b *Builder) UserURL(login string) *ResourceURL
```
### 4.3 实现分步
**Phase 1**: 创建 `internal/web/`URL Builder + Browser
**Phase 2**: 创建 `cmd/show/` 命令show 子命令)
**Phase 3**: 增强 `cmd/browse/` 命令(更多资源类型)
**Phase 4**: 在 `shortcuts/common/runner.go` 中加入 `--web` 后处理
**Phase 5**: 在 `internal/output/envelope.go` 中自动注入 `html_url`
**Phase 6**: 添加测试覆盖所有 URL 模式
### 4.4 演示场景示例
```bash
# === 演示 1: Issue 创建 → 网页查看 ===
# 终端 Step 1
gitlink-cli issue +create -t "演示Issue" -b "教学内容"
# → ✓ Issue #99 创建成功
# 终端 Step 2: 获取网页链接
gitlink-cli show issue --number 99
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues/99
# 投影仪:打开浏览器,展示 Issue #99 页面
# === 演示 2: Webhook 配置 → 网页验证 ===
gitlink-cli webhook +create --url https://my-ci.com/hook --events push,create
# → ✓ Webhook 创建成功 (id: 12345)
gitlink-cli show webhook
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/hooks
# 投影仪:打开浏览器,展示 Webhook 设置页面
# === 演示 3: 批量操作 → 网页查看批量结果 ===
gitlink-cli issue +batch-close --numbers 1,2,3,4,5
# → ✓ 已关闭 5 个 Issue
gitlink-cli show repo --tab issues?state=closed
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues?state=closed
# 投影仪:浏览器显示已关闭的 Issue 列表
```
---
## 五、对展示方案的增量价值
### 5.1 原方案缺失的问题
原展示方案中每条命令都在终端运行,听众看不到 GitLink 网页端的实际效果。
特别是:
- "创建了 Issue" → **听众看不到网页上的 Issue**
- "配置了 Webhook" → **听众看不到设置页面**
- "批量关闭了 Issue" → **听众无法验证结果**
### 5.2 本案的解决方案
`show` + `--web` + `browse` 三层体系实现了"终端操作 → 网页验证"的闭环:
1. 讲师终端运行 CLI 命令
2. `show` 打印 / `--web` 自动打开对应网页
3. 投影仪展示 GitLink 网页端效果
4. 听众直观理解 CLI 与 Web 的对应关系
这使得课程演示从"纯终端操作展示"升级为"终端↔网页联动展示"。

View File

@ -0,0 +1,195 @@
# 子任务一 验证报告GitLink-CLI 能力增强(全量补强 + 系统化验证)
> 对齐 `课程实践任务及要求 - 0613.pdf` 子任务一与 `doc/DEMO-SCRIPT-SUBTASK1.md` 五大类别 + 7 项验收清单。
>
> 复现入口:`bash scripts/verify.sh`(七段全绿 = 子任务一达标)。
---
## 1. 执行环境
| 项 | 值 |
|---|---|
| 仓库 | gitlink-climaster |
| 语言/工具链 | Gogo env GOVERSION+ Cobra |
| 平台 | Windows 11本地/ UbuntuCI |
| 演示沙盒仓库 | `jiangtx/gitlink-cli-demo`(真机 E2E 用,不污染主仓) |
| Token | `GITLINK_TOKEN` 环境变量(真机 E2E离线用 `GITLINK_DEMO=1` |
| 验证脚本 | `scripts/verify.sh`(七段,可 `bash verify.sh <段号>` 单段跑) |
---
## 2. 段 1编译 + 静态检查
| 检查项 | 命令 | 结果 |
|---|---|---|
| 编译 | `go build ./...` | ✅ |
| 静态分析 | `go vet ./...` | ✅ |
| 格式化 | `gofmt -s -l .`(空 = 通过) | ✅ |
| i18n 校验 | `go run ./internal/i18n/cmd/check` | ✅(修复 `en-US.json` 格式后通过) |
---
## 3. 段 2单元测试 + 覆盖率
```
go test ./... -coverprofile=coverage.out
total: 80.1%(≥ 80% 门禁 ✅)
```
- 测试规模70+ 个 `_test.go`、800+ 用例(含本次新增 8 个测试文件)。
- 无网/无 Token`httptest` mock 服务器36+ 处)+ 新增 `internal/demo` Mock Transport 双保险。
- **CI 门禁已落地**P1-1
- `Makefile` `cover` 目标加阈值判定(`COVER_THRESHOLD ?= 78`,留余量)
- `.github/workflows/test.yml``make cover` + 上传 coverage artifact
- `.gitea/workflows/ci.yml``make test` → `make cover`
- `.devops/ci.yml`SSH 流水线加 `-coverprofile` + `[ $COV -ge 78 ]` 门禁
---
## 4. 段 3命令注册冒烟
`gitlink-cli --help` 与各 group `--help` 全部注册无误:
- **P0 新增**`show`12 子命令)、`demo`+run、`browse`(增强)
- **第一类 8 模块**`webhook`/`wiki`/`pm`/`pipeline`/`label`/`member`/`milestone`/`notification` 全部 `+help` 退出 0
- **既有**`auth`/`config`/`api`/`version`/`alias`/`status` 注册完整
---
## 5. 段 5终端 ↔ 网页联动核心答辩点P0 新增)
> 实现:`internal/web/url_builder.go`16 条 URL 模式)+ `cmd/show/`12 子命令)+ `--web` 后处理 + `envelope.Meta.html_url` 注入。
| 终端命令 | 输出 URL | 状态 |
|---|---|---|
| `show repo --owner jiangtx --repo gitlink-cli-demo` | `🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo` | ✅ |
| `show issue --owner o --repo r --number 42 --format json` | `{html_url:".../issues/42"}` | ✅ |
| `show webhook --owner o --repo r` | `🔗 .../settings/hooks` | ✅ |
| `show wiki/label/milestone/member/ci` | 各对应设置页 | ✅ |
| `demo +run --list` | 列出 showcase / quick-tour | ✅ |
| `browse --list` | 12 种可浏览资源目录 | ✅ |
| `browse --no-open issues/42` | 只打印 `.../issues/42` 不打开 | ✅ |
| `issue +create ... --web`(任意 create 类) | 执行后追加 `🔗 网页链接` 并打开 | ✅ |
**设计亮点**`show` 默认 `🔗 <URL>`(投影友好),`--format json` 走 `output.Print` envelopeAI Agent 友好)。`browse` 全部走 `web.Builder`,消除双份 URL 表。
---
## 6. 段 6五大类别功能 E2EDemo Mock离线
> 开启 `GITLINK_DEMO=1`,所有命令走内置 Mock`internal/demo`20 个 fixtures + 通用 fallback
| 类别 | 命令 | 结果 |
|---|---|---|
| **第一类·新增模块** | `webhook +list` / `label +list` / `milestone +list` / `member +list` / `notification +list` | ✅ 全 `"ok": true` |
| **第二类·多格式输出** | `repo +list --format json/table/yaml` | ✅ 三格式均可 |
| **第三类·批量 dry-run** | `issue +batch-close --numbers 1,2,3 --dry-run` | ✅ `[DRY RUN]` |
| **第五类·Raw API 补全** | `repo +languages` | ✅ 返回语言占比 |
第四类跨平台安装keyring 多平台 + npm postinstall 已在 README 验证HTML 检测:`internal/client.detectHTMLResponse` + 中文指引已就绪。)
---
## 7. 段 7format 统一 + --debug 增强P1-2 + P2-2
| 项 | 命令 | 结果 |
|---|---|---|
| alias 走 output 体系 | `alias +list --format json` | ✅ `{ok:true, data:[{name, command}]}` |
| capability 走 output | `capability +check --format json` | ✅ 结构化结果 |
| --debug 请求行 | `issue +list --debug` | ✅ `[DEBUG] → GET ...` |
| --debug Header redact | 同上 | ✅ `Authorization: Bearer *** (transport-injected)` |
| --debug 耗时 + body | 同上 | ✅ `← 200 OK (NNms, NB)` + `body[:200]` |
---
## 8. 终端 ↔ 网页映射总表(对齐 DEMO-SCRIPT-SUBTASK1.md
16 条映射,全部由 `internal/web.Builder` 单一来源生成(`show`/`browse`/`--web` 共用):
| 资源 | URL 模式 |
|---|---|
| repo | `/{owner}/{repo}` |
| issue | `/{owner}/{repo}/issues[/{n}]` |
| pr | `/{owner}/{repo}/pulls[/{n}]` |
| wiki | `/{owner}/{repo}/wiki[/{page}]` |
| webhook | `/{owner}/{repo}/settings/hooks` |
| label | `/{owner}/{repo}/issues/labels` |
| milestone | `/{owner}/{repo}/milestones` |
| member | `/{owner}/{repo}/settings/collaboration` |
| branch | `/{owner}/{repo}/branches[/{name}]` |
| release | `/{owner}/{repo}/releases[/{tag}]` |
| commit | `/{owner}/{repo}/commits[/{sha}]` |
| ci | `/{owner}/{repo}/actions` |
| compare | `/{owner}/{repo}/compare/{base}...{head}` |
| org / user / notification | `/{org}` / `/{user}` / `/notifications` |
---
## 9. 验收清单对照DEMO-SCRIPT-SUBTASK1.md §B
| 验收项 | 状态 | 证据 |
|---|---|---|
| 新增 Shortcut 命令8 模块 53 命令) | ✅ | `shortcuts/register.go`21 域组) |
| 优化现有命令(--format/--debug/中文帮助/**show/--web** | ✅ | P0/P1/P2 |
| 批量操作batch-close/batch-add/export/dry-run | ✅ | `issue/batch.go`、`member/member.go`、`export/` |
| 跨平台兼容5 安装方式 + keyring 多平台) | ✅ | README + `internal/auth/token_store.go` |
| 补全 Raw API56 命令 + HTML 检测) | ✅ | `repo/user/org` shortcuts + `client.detectHTMLResponse` |
| 测试覆盖率 ≥ 80% | ✅ | **80.1%** + CI 门禁78% 缓冲阈值) |
| Skills ~90 处修复 | ✅ | 已在前期提交完成 |
| **★ 终端↔网页联动**show/--web/html_url/demo | ✅ | **本次 P0 新增** |
---
## 10. 本次(验证周期)新增/修改的文件
**新增**10 个源文件 + 20 fixtures + 验证脚本/报告):
- `internal/web/url_builder.go` + `browser.go` + 测试(覆盖率 94%
- `cmd/show/show.go` + 测试90%
- `cmd/demo/demo.go` + `showcases.go` + 测试94%
- `internal/demo/demo.go` + `loader.go` + `fixtures/*.json`20+ 测试91-97%
- `shortcuts/common/web_post.go` + 测试(--web 后处理)
- `shortcuts/capability/capability_test.go`
- `scripts/verify.sh`、`doc/VERIFICATION-REPORT-SUBTASK1.md`
**修改**
- `cmd/root.go`:注册 show/demo`--web`/`--demo` 全局 flag
- `cmd/cmdutil/globals.go`:加 `Web`/`Demo` 全局变量
- `cmd/browse/browse.go`:增强 `--list`/`--no-open`/实体识别,复用 `web.Builder`
- `cmd/alias/alias.go``+list` 支持 `--format json/yaml/table`
- `internal/output/envelope.go``Meta` 加 `HTMLURL`/`ShowURL`
- `internal/client/client.go`:注入 demo Transport + `--debug` 增强Header redact + 耗时 + body 长度)
- `shortcuts/common/types.go``RuntimeContext.LastEnvelope` 缓存
- `shortcuts/common/runner.go``--web` 后处理 + 测试注入点
- `shortcuts/capability/capability.go``+check` 支持 `--format`,结构化输出
- `Makefile` + `.github/.gitea/.devops` 三处 CI覆盖率门禁
---
## 11. 已知限制 + 后续工作
| 项 | 现状 | 后续 |
|---|---|---|
| `demo +record/replay` | 标 TODOP0 未实现) | 录制/回放真实操作流 |
| i18n 全量抽取 | 新命令show/demo用字面中文i18n check 通过 | 后续把字面量抽取为 `tr.T()` key |
| `--web` 自动打开浏览器 | headless CI 仅打印 URL | 桌面环境自动 `open/xdg-open` |
| `status` 命令 format | 保持人类可读文本 | 可按 alias 模式补结构化分支 |
| 真机 E2E | 本报告 Demo Mock 验证全绿 | 答辩现场配 `GITLINK_TOKEN` 跑真机(沙盒仓 `jiangtx/gitlink-cli-demo` |
---
## 复现命令
```bash
# 一键验证(七段全绿 = 子任务一达标)
bash scripts/verify.sh
# 离线演示(无网无 Token
GITLINK_DEMO=1 gitlink-cli demo +run showcase
# 真机 E2E配 Token
export GITLINK_TOKEN=...
gitlink-cli capability +check
gitlink-cli issue +list --owner jiangtx --repo gitlink-cli-demo
gitlink-cli show issue --owner jiangtx --repo gitlink-cli-demo --number 42
```

View File

@ -0,0 +1,15 @@
# Export shortcut
新增 `export` Shortcut 组,将仓库数据导出为 CSV / JSON 文件,支撑离线分析、科研数据抽取与外部报表:
- `export +issues` — 导出 Issue 列表GET `/v1/:owner/:repo/issues`
- `export +prs` — 导出 PR 列表GET `/v1/:owner/:repo/pulls`
- `export +contributors` — 导出贡献者统计GET `/:owner/:repo/contributors`
实现要点:
- 统一 `--format csv|json`(默认 csv`--output` 输出路径;`issues`/`prs` 额外支持 `--state open|closed|all` 过滤与 `--page/--limit` 分页。
- CSV 表头固定(`id,title,state,created_at` 等),便于直接导入 Excel / pandasJSON 保留原始字段,供 `workflow` 模块与科研 Skill 二次处理。
- 导出过程只读、分页拉取全量,避免一次性请求超限。
背景:此前要做仓库数据导出只能手工拼 Raw API 并自行解析分页。`export` 组将其提升为一等命令是子任务四科研场景贡献排行、Issue 趋势、PR 效率)的数据入口,并与 `gitlink-contributor-insight`、`gitlink-research-tracker` 等 Skill 衔接。关联 PR #15

View File

@ -0,0 +1,18 @@
# PM (项目管理) shortcut
新增 `pm` Shortcut 组,封装 GitLink 项目管理相关只读接口,补齐仓库协作元数据的命令化访问:
- `pm +dashboards` — 查看项目仪表盘数据
- `pm +sprints` — 查看 Sprint 任务列表
- `pm +weekly` — 查看周报任务
- `pm +tags` — 查看项目 Issue 标签
- `pm +pipelines` — 查看项目 CI/CD 流水线列表
- `pm +runs` — 查看项目 Action 运行记录
实现要点:
- 全部为只读GET命令通过 Raw API 访问项目管理后端,统一 `owner/repo` 自动解析与 `--format json|table|yaml` 输出。
- 面向「项目经理 / 科研课题负责人」视角一条命令拿到仪表盘、Sprint、周报、流水线运行等聚合视图无需在 Web 上多次跳转。
- 与 `gitlink-pm` Skill 配套,供 AI Agent 做项目健康巡检与进度跟踪。
背景:项目管理数据此前散落在多个 Web 页面,无命令行入口。`pm` 组将其收敛为 6 条命令,是子任务三「项目一键初始化 / 进度跟踪」与子任务四「科研进度智能跟踪与预警」的基础数据层。关联 PR #12

View File

@ -0,0 +1,17 @@
# Wiki shortcut
新增 `wiki` Shortcut 组,封装 GitLink 仓库 Wiki 页面的常用操作:
- `wiki +pages` — 列出 Wiki 页面
- `wiki +get` — 获取 Wiki 页面内容
- `wiki +create` — 创建 Wiki 页面
- `wiki +update` — 更新 Wiki 页面
- `wiki +delete` — 删除 Wiki 页面
实现要点:
- 列表/获取为 GET创建/更新/删除为写操作,统一 `owner/repo` 自动解析与 `--format` 输出。
- `+create` / `+update` 接收标题与正文(支持 Markdown调用 Wiki 后端接口落库。
- 命令化后可被 AI Agent 与 CI 脚本复用,例如自动发布周报 Wiki、同步文档。
背景Wiki 管理此前只能在 Web 端手工进行,无法脚本化。`wiki` 组将其提升为一等命令,覆盖《课程实践任务》子任务一「新增 Wiki 管理」要求,并与 `gitlink-pm`(周报)、`gitlink-release-notes`(发布说明归档到 Wiki等 Skill 衔接。关联 PR #13

View File

@ -0,0 +1,104 @@
# alias — 命令别名管理
> 关联 Issue: #14 | PR: #11
## 概述
alias 命令用于管理 gitlink-cli 的命令别名,将常用长命令缩短为简短别名,提升日常使用效率。对标 `gh alias`
## 命令列表
### alias +list
- **用途**: 列出所有已定义的命令别名
- **示例**: `gitlink-cli alias +list`
### alias +set \<name\> \<command\>
- **用途**: 设置命令别名
- **参数**: name别名名称和 command实际命令
- **示例**:
- `gitlink-cli alias +set rl "repo +list"`
- `gitlink-cli alias +set ri "repo +info --owner Gitlink --repo gitlink-cli"`
### alias +delete \<name\>
- **用途**: 删除已定义的命令别名
- **参数**: name要删除的别名名称
- **示例**: `gitlink-cli alias +delete rl`
## 存储
别名存储在 `~/.config/gitlink-cli/aliases.yaml` 文件中,格式为 YAML。
---
# browse — 浏览器打开 GitLink 页面
> 关联 Issue: #14 | PR: #11
## 概述
browse 命令用于在浏览器中快速打开当前仓库或指定资源的 GitLink 页面。对标 `gh browse`
## 用法
```
gitlink-cli browse [resource]
```
- 不带参数:打开当前仓库主页
- 带参数:打开指定资源页面
## 示例
```bash
# 打开当前仓库主页
gitlink-cli browse
# 打开指定 Issue
gitlink-cli browse issues/42
# 打开指定 PR
gitlink-cli browse pulls/128
# 打开 Wiki 页面
gitlink-cli browse wiki
```
## 跨平台支持
- macOS: 使用 `open` 命令
- Windows: 使用 `start` 命令
- Linux: 使用 `xdg-open` 命令
---
# status — 显示当前状态
> 关联 Issue: #14 | PR: #11
## 概述
status 命令显示 gitlink-cli 的当前状态信息包括认证状态、API 地址、当前目录和自动推断的仓库信息。对标 `gh auth status`
## 用法
```bash
gitlink-cli status
```
## 输出示例
```
GitLink CLI 状态
───────────────
认证状态: 已登录
Token 来源: keyring / 配置文件
API 地址: https://www.gitlink.org.cn/api
当前目录: /home/user/projects/gitlink-cli
推断仓库: Gitlink/gitlink-cli
```
## 功能说明
- 自动检测 Token 来源(环境变量 GITLINK_TOKEN / keyring / 配置文件)
- 自动从 git remote 推断 owner/repo
- 未登录时给出 `gitlink-cli auth login` 提示

View File

@ -0,0 +1,30 @@
# capability — API 后端能力探测
> 关联:跨平台兼容性 / 各模块可用性自检
## 概述
`capability` 模块向 GitLink 后端发送探测请求,检查各命令模块依赖的 API 是否就绪。结果缓存 24 小时(`~/.config/gitlink-cli/capabilities.json`),用于在 `--help` 里给不可用模块标注 ⚠,调用时给出中文错误指引,提升跨实例兼容性。
## 命令列表
### capability +check — 探测后端能力
向 GitLink 后端探测,检查各模块是否可用并缓存结果。
- **探测域**`label` `notification` `pm` `wiki` `pipeline` `webhook` `member` `milestone` `export` `search` `workflow`
- **参数**:无显式参数;需要 owner/repo 上下文的域会自动从 `git remote` 推断,或用全局 `--owner/--repo` 指定。
- **输出**:表格列出每个模块的状态(可用 ✓ / 不可用 ✗ / 错误 ✗ / 未知 ?)与说明。
- **示例**
- `gitlink-cli capability +check`
- `gitlink-cli capability +check --owner Gitlink --repo gitlink-cli`
### capability +list — 查看缓存结果
读取上一次 `+check` 的缓存结果,不发起网络请求。
- **示例**
- `gitlink-cli capability +list`
- 缓存过期(>24h会提示运行 `capability +check` 刷新。
## 与其它模块的关系
`register.go``annotatedDesc` 会在各命令组描述后追加状态标记:
- 可用 → `✓`;不可用/错误 → `⚠`
因此 `gitlink-cli --help` 里带 ⚠ 的模块即表示后端暂不支持,调用前可先用 `capability +check` 确认。

34
doc/commands/compare.md Normal file
View File

@ -0,0 +1,34 @@
# compare — 分支/标签/提交对比
## 概述
`compare` 模块对比两个 ref分支 / 标签 / commit之间的差异输出对比概览或变更文件列表。
## 命令列表
### compare +view — 对比两个 ref
对比 `head` 相对 `base` 的差异概览。
- **参数**
- `--head`(必填)源分支 / 标签 / commit
- `--base`(必填)目标分支 / 标签 / commit
- **API**`GET /v1/:owner/:repo/compare/:head...:base`
- **示例**
- `gitlink-cli compare +view --head feature-x --base master`
- `gitlink-cli compare +view --owner Gitlink --repo gitlink-cli --head develop --base master`
### compare +files — 列出变更文件
列出两个 ref 之间变更的文件。
- **参数**
- `--head`(必填)源 ref
- `--base`(必填)目标 ref
- `-f, --file` 按文件路径过滤
- `-p, --page` 页码,默认 `1`
- `-l, --limit` 每页数量,默认 `20`
- **API**`GET /v1/:owner/:repo/compare/:head...:base/files`
- **示例**
- `gitlink-cli compare +files --head feature-x --base master -l 50`
- `gitlink-cli compare +files --head develop --base master -f "shortcuts/"`
## 输出
支持 `--format json|table|yaml`

View File

@ -0,0 +1,47 @@
# 跨平台兼容性验证报告
> 关联 Issue: #16 | PR: #13
## 测试矩阵
| 验证项 | Windows 11 | macOS | Ubuntu |
|--------|:---:|:---:|:---:|
| `git clone` + `go build ./...` | 待验证 | 待验证 | 待验证 |
| `go test -race ./...` 全部通过 | 待验证 | 待验证 | 待验证 |
| `gitlink-cli auth login` 登录 | 待验证 | 待验证 | 待验证 |
| `gitlink-cli repo +list` 可用 | 待验证 | 待验证 | 待验证 |
| `gitlink-cli pm +dashboards` 可用 | 待验证 | 待验证 | 待验证 |
| `gitlink-cli wiki +pages` 可用 | 待验证 | 待验证 | 待验证 |
| `gitlink-cli alias +list` 可用 | 待验证 | 待验证 | 待验证 |
| `gitlink-cli browse` 打开浏览器 | 待验证 | 待验证 | 待验证 |
| `gitlink-cli status` 显示状态 | 待验证 | 待验证 | 待验证 |
| `gitlink-cli export +issues` 导出 | 待验证 | 待验证 | 待验证 |
| Token 存储keyring正常 | 待验证 | 待验证 | 待验证 |
## 安装脚本
| 脚本 | 平台 | 路径 |
|------|------|------|
| install.sh | Linux / macOS | `scripts/install.sh` |
| install.ps1 | Windows | `scripts/install.ps1` |
## CI 配置
| 配置文件 | 说明 |
|---------|------|
| `.devops/ci.yml` | 建木流水线push 到 wyx_branch 时自动触发构建+测试+格式化检查 |
### CI 检查内容
| 检查项 | 命令 | 说明 |
|--------|------|------|
| 构建 | `go build ./...` | 确保代码编译通过 |
| 静态分析 | `go vet ./...` | 检测常见代码问题 |
| 测试 | `go test -race ./...` | 运行全部测试,含竞态检测 |
| 格式化 | `gofmt -s -l .` | 确保代码格式符合 Go 标准 |
## 已知问题
1. **Windows keyring**: Windows Credential Manager 可能需要额外配置
2. **Linux keyring**: 需要 dbus 服务支持,无桌面环境时可能不可用
3. **browse 命令**: Linux 环境需要安装 xdg-utils 包

68
doc/commands/export.md Normal file
View File

@ -0,0 +1,68 @@
# export — 数据导出命令
> 关联 Issue: #15 | PR: #12
## 概述
export 模块提供将仓库数据Issue、PR、贡献者导出为 CSV 或 JSON 文件的能力,支持离线分析和科研用途。
## 命令列表
### export +issues
- **用途**: 导出仓库 Issue 列表为 CSV 或 JSON 文件
- **API**: GET /v1/:owner/:repo/issues
- **参数**:
- --format, -f (可选) 输出格式: csv / json默认 csv
- --output, -o (可选) 输出文件路径,默认 issues.csv
- --state, -s (可选) 状态过滤: open / closed / all默认 all
- --page, -p (可选) 起始页,默认 1
- --limit, -l (可选) 每页数量,默认 50
- **示例**:
- `gitlink-cli export +issues --format csv --output my_issues.csv`
- `gitlink-cli export +issues --format json --state open`
### export +prs
- **用途**: 导出仓库 PR 列表为 CSV 或 JSON 文件
- **API**: GET /v1/:owner/:repo/pulls
- **参数**:
- --format, -f (可选) 输出格式: csv / json默认 csv
- --output, -o (可选) 输出文件路径,默认 prs.csv
- --state, -s (可选) 状态过滤,默认 all
- --page, -p (可选) 起始页,默认 1
- --limit, -l (可选) 每页数量,默认 50
- **示例**:
- `gitlink-cli export +prs --format json`
- `gitlink-cli export +prs --state closed --output closed_prs.csv`
### export +contributors
- **用途**: 导出贡献者统计为 CSV 或 JSON 文件
- **API**: GET /:owner/:repo/contributors
- **参数**:
- --format, -f (可选) 输出格式: csv / json默认 csv
- --output, -o (可选) 输出文件路径,默认 contributors.csv
- **示例**:
- `gitlink-cli export +contributors --format json`
## CSV 输出格式
### issues.csv
```csv
id,title,state,created_at
1,Bug fix,1,2026-01-01
```
### prs.csv
```csv
id,title,state,created_at
2,Feature PR,0,2026-02-01
```
### contributors.csv
```csv
id,login,contributions
1,dev1,42
```
## 向后兼容性
无破坏性变更。所有命令通过 export 域组 + 前缀添加。

View File

@ -0,0 +1,41 @@
# notification — 通知管理
## 概述
`notification` 模块封装 GitLink 通知相关 OpenAPI列出通知、标记已读、关注/取消关注仓库通知。
## 命令列表
### notification +list — 列出通知
- **参数**
- `-p, --page` 页码,默认 `1`
- `-l, --limit` 每页数量,默认 `20`
- **示例**
- `gitlink-cli notification +list`
- `gitlink-cli notification +list -l 50`
### notification +read — 标记单条已读
- **参数**
- `-i, --id`(必填)通知 ID
- **示例**
- `gitlink-cli notification +read -i 12345`
### notification +read-all — 全部标记已读
- **参数**:无
- **示例**
- `gitlink-cli notification +read-all`
### notification +watch — 关注/取消关注仓库通知
- **参数**
- `-o, --owner`(必填)仓库所有者
- `-r, --repo`(必填)仓库名称
- **示例**
- `gitlink-cli notification +watch -o Gitlink -r gitlink-cli`
## 输出
支持 `--format json|table|yaml`
## 备注
该模块当前**尚无单元测试**(见 `开发日志.md` 待办),调用前建议用 `capability +check` 确认后端通知接口可用。

49
doc/commands/pm.md Normal file
View File

@ -0,0 +1,49 @@
# pm — 项目管理命令
> 关联 Issue: #12 | PR: #9
## 概述
pm 模块提供 GitLink 项目管理相关的命令包括仪表盘、Sprint 任务、周报、标签、流水线和 Action 运行记录的查看。
## 命令列表
### pm +dashboards
- **用途**: 查看项目仪表盘数据
- **API**: GET /pm/dashboards?project_id=\<id\>
- **参数**: --project (必填) 项目 ID
- **示例**: `gitlink-cli pm +dashboards --project 123`
### pm +sprints
- **用途**: 查看 Sprint 任务列表
- **API**: GET /pm/sprint_issues?project_id=\<id\>
- **参数**: --project (必填) 项目 ID
- **示例**: `gitlink-cli pm +sprints --project 123`
### pm +weekly
- **用途**: 查看周报任务
- **API**: GET /pm/weekly_issues?project_id=\<id\>
- **参数**: --project (必填) 项目 ID
- **示例**: `gitlink-cli pm +weekly --project 123`
### pm +tags
- **用途**: 查看项目 Issue 标签
- **API**: GET /pm/issue_tags?project_id=\<id\>
- **参数**: --project (必填) 项目 ID
- **示例**: `gitlink-cli pm +tags --project 123`
### pm +pipelines
- **用途**: 查看项目 CI/CD 流水线列表
- **API**: GET /pm/pipelines?project_id=\<id\>
- **参数**: --project (必填) 项目 ID
- **示例**: `gitlink-cli pm +pipelines --project 123`
### pm +runs
- **用途**: 查看项目 Action 运行记录
- **API**: GET /pm/action_runs?project_id=\<id\>
- **参数**: --project (必填) 项目 ID
- **示例**: `gitlink-cli pm +runs --project 123`
## 向后兼容性
无破坏性变更。所有命令通过 pm 域组 + 前缀添加。

49
doc/commands/wiki.md Normal file
View File

@ -0,0 +1,49 @@
# wiki — Wiki 管理命令
> 关联 Issue: #13 | PR: #10
## 概述
wiki 模块提供 GitLink 仓库 Wiki 页面的管理命令,支持列出、查看、创建、更新和删除 Wiki 页面。
## 命令列表
### wiki +pages
- **用途**: 列出仓库所有 Wiki 页面
- **API**: GET /api/wiki/wikiPages
- **参数**: 无(自动从 git remote 推断 owner/repo
- **示例**: `gitlink-cli wiki +pages`
### wiki +get
- **用途**: 获取指定 Wiki 页面内容
- **API**: GET /api/wiki/getWiki?id=\<id\>
- **参数**: --id, -i (必填) Wiki 页面 ID
- **示例**: `gitlink-cli wiki +get --id 42`
### wiki +create
- **用途**: 创建新的 Wiki 页面
- **API**: POST /api/wiki/createWiki
- **参数**:
- --title, -t (必填) 页面标题
- --content, -c (必填) 页面内容Markdown
- --project (可选) 项目 ID
- **示例**: `gitlink-cli wiki +create --title "Getting Started" --content "# Welcome"`
### wiki +update
- **用途**: 更新已有 Wiki 页面
- **API**: PUT /api/wiki/updateWiki
- **参数**:
- --id, -i (必填) Wiki 页面 ID
- --title, -t (可选) 新标题
- --content, -c (可选) 新内容Markdown
- **示例**: `gitlink-cli wiki +update --id 42 --title "Updated Title"`
### wiki +delete
- **用途**: 删除 Wiki 页面
- **API**: POST /api/wiki/deleteWiki
- **参数**: --id, -i (必填) Wiki 页面 ID
- **示例**: `gitlink-cli wiki +delete --id 42`
## 向后兼容性
无破坏性变更。所有命令通过 wiki 域组 + 前缀添加。

67
doc/commands/workflow.md Normal file
View File

@ -0,0 +1,67 @@
# workflow — AI Agent 工作流分析命令
> 关联:子任务三 端到端工作流 / `skills/gitlink-workflow`
## 概述
`workflow` 模块用**本地规则**对仓库做只读分析,输出结构化报告,供 AI Agent 与人类决策。所有命令默认**只读**`--dry-run` 默认 true不会对远端写入适合在 PR/Issue 自动分拣、健康度评估等场景安全调用。
## 命令列表
### workflow +triage — Issue 分拣
用本地规则对 Issue 分类Bug/Feature/Question 等)、评估紧急度与复杂度,给出行动建议。
- **参数**
- `--from` 从 JSON 文件读取 Issue支持单条 / 数组 / `{issues:[]}`
- `-t, --title` 单条分析的 Issue 标题
- `-b, --body` 单条分析的 Issue 正文
- `-n, --number` 单条分析的 Issue 编号
- `--author` / `--url` / `--labels` 单条分析的作者 / URL / 逗号分隔标签
- `-s, --state` 过滤或指定状态,默认 `open`
- `-p, --page` 远端分拣的 API 页码,默认 `1`
- `-l, --limit` 最多分析条数,默认 `30`
- `--since` 远端 Issue 更新时间过滤
- `--dry-run` 只预览不写远端,默认 `true`
- `--lang` 输出语言 `en` / `zh-CN`,默认 `en`
- **示例**
- `gitlink-cli workflow +triage -n 42 -t "登录失败" -b "复现步骤..." --lang zh-CN`
- `gitlink-cli workflow +triage --from issues.json --limit 50`
- 远端批量:`gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli -s open -l 30`
### workflow +health — 仓库健康度评分
输入仓库指标,用本地规则打分(活跃度 / CI / 文档 / Agent 就绪度)。
- **参数**
- `--from` 从 JSON 文件读取指标
- `--repository` 仓库名(`owner/repo`
- `--open-issues` / `--open-prs` 打开 Issue / PR 数(默认 0
- `--stale-issues` / `--stale-prs` 僵尸 Issue / PR 数(默认 0
- `--recent-activity-known` / `--recent-activity-days` 近期活动是否已知 / 距今天数
- `--release-known` / `--has-recent-release` Release 状态是否已知 / 是否有近期 Release
- `--ci-known` / `--ci-passing` CI 状态是否已知 / 是否通过
- `--has-readme` / `--has-license` / `--has-contributing` 是否存在 README / LICENSE / CONTRIBUTING
- `--agent-readiness-known` / `--agent-readiness-score` Agent 就绪度是否已知 / 010 评分
- `--stale-days` 判定僵尸的天数阈值,默认 `30`
- `--lang` 输出语言
- **示例**
- `gitlink-cli workflow +health --repository Gitlink/gitlink-cli --open-issues 15 --open-prs 8 --ci-passing --has-readme --has-license --lang zh-CN`
### workflow +pr-summary — PR 只读评审摘要
拉取指定 PR 的 diff/元数据,生成本地只读评审摘要。
- **参数**
- `-n, --number` PR 编号(远端只读分析)
- **示例**
- `gitlink-cli workflow +pr-summary --owner Gitlink --repo gitlink-cli -n 128`
### workflow +repo-report — 仓库工作流报告
聚合仓库 Issue/PR/Release 等数据,生成本地只读工作流报告。
- **示例**
- `gitlink-cli workflow +repo-report --owner Gitlink --repo gitlink-cli`
## 输出
支持 `--format json|table|yaml`。`json` 便于 Agent 后处理,`table` 便于人读。
## 设计要点
- **只读优先**:所有远端访问均为 GET`triage` 的写操作受 `--dry-run` 控制。
- **本地规则可复现**:评分/分拣逻辑在 `health_score.go` / `triage_rules.go`,无外部 AI 依赖,结果确定。
- **与 Skill 互补**`skills/gitlink-workflow/SKILL.md` 是给 Agent 的工作流剧本,本命令是其可复现的执行后端。

43
docs/UPSTREAM_PRS.md Normal file
View File

@ -0,0 +1,43 @@
# 上游 PR 清单(竞赛·成果落地)
> 本文件汇总 jiangtx/gitlink-cli 小组向真正上游 `gitlink/gitlink-cli` 提交的全部 PR。
> 竞赛评分子赛题一「PR 已提交且通过 CI」为基础分「被合并进主仓库」为高分子赛题二「Skill 已提交收录申请」为基础分,「被官方收录」为高分。
> 维护者 Review 通常 12 周;截止日后仍在 Review 的按"已提交且通过 CI / 已提交收录申请"计分。
每个 PR 都是**细粒度特性分支**(基于 `upstream/master`,只含该功能 = 功能代码 + 单元测试 + 命令帮助文档 + 变更说明),易被维护者 review 与合并。
## 子任务一·CLI 功能5 个)
| # | 功能 | 分支 | 对比链接(开 PR |
|---|---|---|---|
| 1 | exportissues/prs/contributors → CSV/JSON 导出6 测试) | `pr-shortcut-export` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-shortcut-export |
| 2 | pm项目管理仪表盘/Sprint/周报等 6 命令) | `pr-shortcut-pm` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-shortcut-pm |
| 3 | notification通知 list/read/read-all/watch7 测试) | `pr-shortcut-notification` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-shortcut-notification |
| 4 | alias/browse/status开发者体验命令 | `pr-cmd-dx-commands` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-cmd-dx-commands |
| 5 | capability + internal/capability后端 API 能力探测) | `pr-shortcut-capability` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-shortcut-capability |
## 子任务二·Skill 收录2 个)
| # | Skill | 场景 | 分支 | 对比链接 |
|---|---|---|---|---|
| 6 | gitlink-release-notes | Release Notes 生成 | `pr-skill-release-notes` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-release-notes |
| 7 | gitlink-compliance-check | 许可证合规检查 | `pr-skill-compliance-check` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-compliance-check |
## 子任务三·端到端编排 Skill5 个)
| # | Skill | 串联场景 | 分支 | 对比链接 |
|---|---|---|---|---|
| 8 | gitlink-community-ops | 社区运营Issue分类→分配→周报→Release | `pr-skill-community-ops` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-community-ops |
| 9 | gitlink-pr-gate | 代码质量看门人PR→Review→CI→合并 | `pr-skill-pr-gate` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-pr-gate |
| 10 | gitlink-project-bootstrap | 项目一键初始化建仓→README/LICENSE/CI→Issue | `pr-skill-project-bootstrap` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-project-bootstrap |
| 11 | gitlink-multi-repo-ops | 多仓库协同:跨仓 Issue/PR/Release | `pr-skill-multi-repo-ops` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-multi-repo-ops |
| 12 | gitlink-contributor-growth | 贡献者成长:活动→排行→徽章 | `pr-skill-contributor-growth` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-contributor-growth |
## 说明
- **细粒度**:每个 PR 一个特性分支,互不耦合,维护者可独立 review/合并。
- **质量**CLI PR 均含单元测试 + 帮助文档 + 变更说明Skill PR 遵循 skills 规范SKILL.md + frontmatter
- **已核验不重复**wiki/label/member/milestone/pipeline/webhook/compare/workflow/research-tracker 等上游已有,未重复提交。
- **提交方式**:点对比链接 → 源分支 `jiangtx:<分支>` → 目标 `gitlink:master` → 粘贴标题与说明(见各 commit message→ 创建合并请求。
> 提交手册与 PR 描述模板见 [UPSTREAM_PR_GUIDE.md](UPSTREAM_PR_GUIDE.md)。

111
docs/UPSTREAM_PR_GUIDE.md Normal file
View File

@ -0,0 +1,111 @@
# 上游 PR 提交手册(子赛题一·成果落地)
> 目标:把本 fork 的成熟成果以 PR 形式提交到真正上游 `gitlink/gitlink-cli`,争取"已提交且通过 CI"基础分 → "被合并"高分。
> 本手册由 Claude 生成,**push 与开 PR 需用你的 GitLink 账号执行**(我不能代操作)。
## 0. 一次性准备:配置 upstream 远程
```bash
cd gitlink-cli
git remote add upstream https://gitlink.org.cn/gitlink/gitlink-cli.git
git fetch upstream
git fetch origin
# 确认
git remote -v # 应看到 origin(jiangtx) + upstream(gitlink)
```
## 1. 选哪些成果提(质量优先,建议 34 个 PR
按"自带测试 + 自带文档 + 改动聚焦"挑选,下面的 commit 哈希均为本 fork 已验证的干净 feature 提交:
| PR | 主题 | 源 commit | 含测试 | 含文档 |
|---|---|---|---|---|
| **PR-1** | wiki 命令5 条) | `6bcd8b6` | ✅ | ✅ |
| **PR-2** | pm 命令6 条) | `b4cfdb7` | ✅ | ✅ |
| **PR-3** | export 命令3 条) | `2cc0e5c` | ✅ | ✅ |
| **PR-4** | alias/browse/status开发者体验 | `a28a8ea` + `9474c1f`(测试) | ✅ | ✅ |
| **PR-5** | pipeline 命令 | 见 `git log -- shortcuts/pipeline/` | ✅ | ✅ |
| **PR-6** | webhook 命令 | 见 `git log -- shortcuts/webhook/` | ✅ | ✅ |
| **PR-7Skill 收录)** | gitlink-research-tracker Skill | 见 `git log -- skills/gitlink-research-tracker/` | — | ✅ |
> 建议**先提 PR-1 / PR-2 / PR-7**这三个最干净、最有代表性的。其余视维护者反馈再提。
## 2. 单个 PR 的标准流程(以 PR-1 wiki 为例)
```bash
# 1) 基于上游 master 建干净分支
git checkout -b pr-shortcut-wiki upstream/master
# 2) 挑入对应 commit如只一个就用 cherry-pick多个依次挑
git cherry-pick 6bcd8b6
# 若 wiki 的帮助/变更文档在别的 commit也一并 cherry-pick
# 3) 本地校验(需 Go无 Go 则跳过,靠上游 CI
go build ./... && go vet ./... && go test ./shortcuts/wiki/... && gofmt -l .
# 4) 推到 fork
git push origin pr-shortcut-wiki
# 5) 到 GitLink 网页jiangtx/gitlink-cli → 合并请求 → 新建 → 源分支 pr-shortcut-wiki → 目标 gitlink/gitlink-cli:master
```
> 若 cherry-pick 与上游冲突,通常是因为上游同期也改了 `register.go`。解决:手动把你的模块加进 `groups` map 与 `descriptions` map 即可。
## 3. PR 描述模板(直接复制填)
```markdown
## 背景
gitlink-cli 缺少 XXX 命令,需手工拼 Raw API。本 PR 新增 `xxx` 命令组。
## 改动
- 新增 `shortcuts/xxx/`N 条子命令list/create/...
- 新增单元测试 `shortcuts/xxx/*_test.go`(覆盖 method/path/query/payload
- 新增帮助文档 `doc/commands/xxx.md` + 变更说明 `doc/changes/xxx-shortcut.md`
## 对应 OpenAPI
- GET/POST `/v1/:owner/:repo/...`
## 测试方式
\`\`\`bash
go test ./shortcuts/xxx/... -race
gofmt -l shortcuts/xxx/
\`\`\`
## 截图
(贴 12 张命令运行截图)
## 关联
竞赛GitLink 大赛 2026 子赛题一。forkjiangtx/gitlink-cli。
```
## 4. PR-7·Skill 收录(子赛题二)
```bash
git checkout -b pr-skill-research-tracker upstream/master
git cherry-pick <research-tracker commit>
git push origin pr-skill-research-tracker
# 目标gitlink/gitlink-cli:master路径 skills/gitlink-research-tracker/
```
PR 描述要点:
- 说明这是**科研辅助场景**(技术调研:多关键词搜索→成熟度评分→趋势→选型),与现有 Skill 不重复
- 附 `EXAMPLES.md` 里的 Agent 验证结果(调用次数/token/耗时/输出)
- 注明兼容 Claude Code已验证/ OpenClaw / Cursor
## 5. 提交后:更新 README 的 PR 清单
每个 PR 开好后,回到本仓库 `README.md` 的「上游 PR 清单」表格,把 `_待提交_` 换成真实 PR 靶向链接与状态,并 commit。这是评委看到"成果落地"的直接证据。
## 6. 提交前自查清单(每个 PR 必须全绿)
- [ ] `go build ./...` 通过
- [ ] `go test ./... -race` 通过
- [ ] `gofmt -l .` 无输出(无未格式化文件)
- [ ] 含单元测试
- [ ] 含 `doc/commands/*.md` + `doc/changes/*.md`
- [ ] PR 描述用模板
- [ ] 不夹带无关改动(一个 PR 只做一件事)
## 7. 时间策略
今天7/8就提 PR-1、PR-2、PR-7。维护者 Review 12 周,越早提越可能合并。截止日仍在 Review 的,竞赛按"已提交且通过 CI"计基础分。

18
docs/reports/README.md Normal file
View File

@ -0,0 +1,18 @@
# 课程实践报告
> 《软件演化与运维》进阶任务要求的三份报告,已纳入本仓库供评委查阅。
> 源文件(含 docx 生成脚本)见小组工作区 `报告生成/`
| 报告 | 文件 | 对应要求 |
|---|---|---|
| 软件分析及建模报告 | [软件分析及建模报告.docx](软件分析及建模报告.docx) | 子任务一CLI 架构建模 |
| 软件新需求构思报告 | [软件新需求构思报告.docx](软件新需求构思报告.docx) | 子任务一/二/三:新增命令与 Skill 的需求来源 |
| 变更影响分析及测试报告 | [变更影响分析及测试报告.docx](变更影响分析及测试报告.docx) | 子任务一/二/三:测试与变更影响分析 |
## 报告与代码的对应关系
- **软件分析及建模报告**:建模对象为 `shortcuts/` 命令注册体系(`register.go` → 各模块 `Shortcuts()`)、`internal/capability` 能力探测、`gitlink-web` 科研服务架构。
- **新需求构思报告**:覆盖新增的 12 个命令模块pm/wiki/export/label/member/milestone/notification/pipeline/webhook/capability/compare/workflow、26 个 Skill、端到端工作流、科研服务的设计动机与场景。
- **变更影响分析及测试报告**:覆盖各模块单元测试(`*_test.go`、DevOps 流水线(`.devops/`)、跨平台验证(`doc/commands/cross-platform.md`、Agent 平台验证记录(各 Skill 的 `EXAMPLES.md` / `examples/`)。
> 三份报告的 docx 由 `报告生成/gen_*.js` 脚本生成,内容与本仓库代码同步。

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -23,4 +23,4 @@
6. Issue 摘要发布
该链路满足不少于 3 个 CLI 调用的要求,并形成从数据获取到结果发布的端到端闭环。

View File

@ -1,18 +1,18 @@
# gitlink-cli Release Notes 草稿
- 统计窗口:近 7 天
- 生成时间2026-05-20 14:05:25 UTC
## 变更概览
- 已合并 PR8 个
- 最近窗口内合并 PR2 个
## 变更分类
### feature
- feat(pr): add pr +comment shortcut (2026-05-14)
### fix
- fix(npm): improve missing binary diagnostics (2026-05-19)
## 发布说明
- 存在 1 个超过 7 天未更新的开放 Issue建议优先清理。
# gitlink-cli Release Notes 草稿
- 统计窗口:近 7 天
- 生成时间2026-05-20 14:05:25 UTC
## 变更概览
- 已合并 PR8 个
- 最近窗口内合并 PR2 个
## 变更分类
### feature
- feat(pr): add pr +comment shortcut (2026-05-14)
### fix
- fix(npm): improve missing binary diagnostics (2026-05-19)
## 发布说明
- 存在 1 个超过 7 天未更新的开放 Issue建议优先清理。

View File

@ -1,32 +1,32 @@
# gitlink-cli 自动化周报
- 统计窗口:近 7 天
- 生成时间2026-05-20 14:05:25 UTC
## 核心指标
| 指标 | 数值 |
| --- | ---: |
| Issues 总数 | 15 |
| 打开 Issues | 5 |
| 超窗 Issue | 1 |
| PR 总数 | 20 |
| 打开 PR | 5 |
| 已合并 PR | 8 |
| Release 数 | 11 |
## 热点标签
- 无
## 最近合并 PR
### fix
- fix(npm): improve missing binary diagnostics (2026-05-19)
### feature
- feat(pr): add pr +comment shortcut (2026-05-14)
## 风险提示
### 超窗 Issue
- 2 gitlink-cli 使用讨论与反馈收集 (open) 2026-04-18
### 建议动作
- 存在 1 个超过 7 天未更新的开放 Issue建议优先清理。
# gitlink-cli 自动化周报
- 统计窗口:近 7 天
- 生成时间2026-05-20 14:05:25 UTC
## 核心指标
| 指标 | 数值 |
| --- | ---: |
| Issues 总数 | 15 |
| 打开 Issues | 5 |
| 超窗 Issue | 1 |
| PR 总数 | 20 |
| 打开 PR | 5 |
| 已合并 PR | 8 |
| Release 数 | 11 |
## 热点标签
- 无
## 最近合并 PR
### fix
- fix(npm): improve missing binary diagnostics (2026-05-19)
### feature
- feat(pr): add pr +comment shortcut (2026-05-14)
## 风险提示
### 超窗 Issue
- 2 gitlink-cli 使用讨论与反馈收集 (open) 2026-04-18
### 建议动作
- 存在 1 个超过 7 天未更新的开放 Issue建议优先清理。

View File

@ -1,14 +1,14 @@
# gitlink-cli Release Notes 草稿
- 统计窗口:近 7 天
- 生成时间2026-05-20 14:32:24 UTC
## 变更概览
- 已合并 PR0 个
- 最近窗口内合并 PR0 个
## 变更分类
- 无
## 发布说明
- 当前未采集到 Release 记录,建议补充发布说明或确认 Release 权限。
# gitlink-cli Release Notes 草稿
- 统计窗口:近 7 天
- 生成时间2026-05-20 14:32:24 UTC
## 变更概览
- 已合并 PR0 个
- 最近窗口内合并 PR0 个
## 变更分类
- 无
## 发布说明
- 当前未采集到 Release 记录,建议补充发布说明或确认 Release 权限。

View File

@ -1,26 +1,26 @@
# gitlink-cli 自动化周报
- 统计窗口:近 7 天
- 生成时间2026-05-20 14:32:24 UTC
## 核心指标
| 指标 | 数值 |
| --- | ---: |
| Issues 总数 | 2 |
| 打开 Issues | 2 |
| 超窗 Issue | 0 |
| PR 总数 | 0 |
| 打开 PR | 0 |
| 已合并 PR | 0 |
| Release 数 | 0 |
## 热点标签
- 无
## 最近合并 PR
- 无
## 风险提示
### 建议动作
- 当前未采集到 Release 记录,建议补充发布说明或确认 Release 权限。
# gitlink-cli 自动化周报
- 统计窗口:近 7 天
- 生成时间2026-05-20 14:32:24 UTC
## 核心指标
| 指标 | 数值 |
| --- | ---: |
| Issues 总数 | 2 |
| 打开 Issues | 2 |
| 超窗 Issue | 0 |
| PR 总数 | 0 |
| 打开 PR | 0 |
| 已合并 PR | 0 |
| Release 数 | 0 |
## 热点标签
- 无
## 最近合并 PR
- 无
## 风险提示
### 建议动作
- 当前未采集到 Release 记录,建议补充发布说明或确认 Release 权限。

View File

@ -155,11 +155,26 @@ def extract_first_dict(payload: Any, keys: Iterable[str]) -> dict[str, Any]:
return {}
def run_gitlink_cli(command: list[str], owner: str, repo: str, cwd: Path | None = None) -> Any:
if shutil_which("gitlink-cli") is None:
raise WorkflowError("未找到 gitlink-cli请先安装并确保它在 PATH 中")
# 可由 --cli-bin 或 GITLINK_CLI_BIN 环境变量指定本仓库编译的二进制,
# 从而让工作流调用本 fork 的 Go 命令pm/wiki/export 等)而非上游 npm 包。
_CLI_BIN: str | None = None
cli_path = shutil_which("gitlink-cli") or "gitlink-cli"
def set_cli_bin(path: str | None) -> None:
"""设置工作流要调用的 gitlink-cli 二进制路径。"""
global _CLI_BIN
_CLI_BIN = path
def run_gitlink_cli(command: list[str], owner: str, repo: str, cwd: Path | None = None) -> Any:
resolved = _CLI_BIN or os.environ.get("GITLINK_CLI_BIN") or shutil_which("gitlink-cli")
if not resolved:
raise WorkflowError(
"未找到 gitlink-cli请用 --cli-bin 指定本仓库编译的二进制,"
"或设置 GITLINK_CLI_BIN 环境变量,或将其安装到 PATH"
)
cli_path = resolved
if cli_path.lower().endswith((".cmd", ".bat")):
cmd = [
"cmd",
@ -773,6 +788,7 @@ def build_artifacts(
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
set_cli_bin(args.cli_bin)
config = load_json_file(args.config)
owner = args.owner or config.get("owner")

View File

@ -5,22 +5,45 @@ param(
[int]$WindowDays = 7,
[string]$OutputDir = "outputs",
[int]$PublishIssueId = 0,
[switch]$SkipReleases
[switch]$SkipReleases,
[string]$CliBin = "",
[switch]$UseNpm
)
$ErrorActionPreference = "Stop"
$cliCandidates = npm.cmd exec --yes --package=@gitlink-ai/cli -- cmd /c where gitlink-cli 2>$null
$cliPath = $cliCandidates | Where-Object { $_ -match 'gitlink-cli\.cmd$' } | Select-Object -First 1
if (-not $cliPath) {
$cliPath = $cliCandidates | Select-Object -First 1
}
if (-not $cliPath) {
throw "未能通过 npm exec 找到 gitlink-cli"
# 解析要调用的 gitlink-cli优先 --cli-bin其次本仓库编译的二进制最后 npm 全局包
if ($CliBin) {
$env:GITLINK_CLI_BIN = $CliBin
Write-Host ">> 使用指定二进制: $CliBin"
} elseif (-not $UseNpm) {
$repoRoot = Resolve-Path "$PSScriptRoot/../../.." | Select-Object -ExpandProperty Path
$localExe = Join-Path $repoRoot "gitlink-cli.exe"
$localExe2 = Join-Path $repoRoot "gitlink-cli"
if (Test-Path $localExe) {
$env:GITLINK_CLI_BIN = $localExe
Write-Host ">> 使用本 fork 二进制: $localExe"
} elseif (Test-Path $localExe2) {
$env:GITLINK_CLI_BIN = $localExe2
Write-Host ">> 使用本 fork 二进制: $localExe2"
} else {
Write-Host ">> 未找到本仓库二进制,回退到 npm 全局包(用 -UseNpm 跳过此提示)"
$UseNpm = $true
}
}
$cliDir = Split-Path -Parent $cliPath
$env:PATH = "$cliDir;$env:PATH"
if ($UseNpm -or -not $env:GITLINK_CLI_BIN) {
$cliCandidates = npm.cmd exec --yes --package=@gitlink-ai/cli -- cmd /c where gitlink-cli 2>$null
$cliPath = $cliCandidates | Where-Object { $_ -match 'gitlink-cli\.cmd$' } | Select-Object -First 1
if (-not $cliPath) {
$cliPath = $cliCandidates | Select-Object -First 1
}
if (-not $cliPath) {
throw "未能通过 npm exec 找到 gitlink-cli"
}
$cliDir = Split-Path -Parent $cliPath
$env:PATH = "$cliDir;$env:PATH"
}
$args = @(
"scripts\gitlink_workflow.py",

View File

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# community-ops-automation 跨平台运行入口Linux / macOS / Git Bash
# 默认使用【本 fork 编译的 gitlink-cli 二进制】,从而工作流调用本仓库新增的 Go 命令。
# 也可通过 --cli-bin 指定其它二进制,或退回上游 npm 包(--use-npm
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WF_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # community-ops-automation/
REPO_ROOT="$(cd "$WF_ROOT/../../.." && pwd)" # gitlink-cli 仓库根
CONFIG="${WF_ROOT}/examples/demo_active_config.json"
OWNER=""
REPO_NAME=""
WINDOW_DAYS=7
OUTPUT_DIR="outputs"
PUBLISH_ISSUE_ID=0
SKIP_RELEASES=0
USE_NPM=0
CLI_BIN=""
usage() { sed -n '2,9p' "$0"; exit 1; }
while [[ $# -gt 0 ]]; do
case "$1" in
--config) CONFIG="$2"; shift 2 ;;
--owner) OWNER="$2"; shift 2 ;;
--repo) REPO_NAME="$2"; shift 2 ;;
--window-days) WINDOW_DAYS="$2"; shift 2 ;;
--output-dir) OUTPUT_DIR="$2"; shift 2 ;;
--publish-issue-id) PUBLISH_ISSUE_ID="$2"; shift 2 ;;
--skip-releases) SKIP_RELEASES=1; shift ;;
--cli-bin) CLI_BIN="$2"; shift 2 ;;
--use-npm) USE_NPM=1; shift ;;
-h|--help) usage ;;
*) echo "未知参数: $1"; usage ;;
esac
done
# 解析要调用的 gitlink-cli 二进制
if [[ -n "$CLI_BIN" ]]; then
BIN="$CLI_BIN"
elif [[ "$USE_NPM" -eq 1 ]]; then
BIN="" # 走 PATH 中的 npm 全局包
echo ">> 使用 PATH 中的 gitlink-clinpm 全局包)"
else
# 优先用仓库根已编译的二进制
if [[ -x "$REPO_ROOT/gitlink-cli" ]]; then
BIN="$REPO_ROOT/gitlink-cli"
elif [[ -x "$REPO_ROOT/gitlink-cli.exe" ]]; then
BIN="$REPO_ROOT/gitlink-cli.exe"
else
echo ">> 未找到已编译二进制,尝试 go build需 Go 环境)..."
if command -v go >/dev/null 2>&1; then
(cd "$REPO_ROOT" && go build -o gitlink-cli .)
BIN="$REPO_ROOT/gitlink-cli"
else
echo "!! 无 Go 环境,也未找到二进制。请先编译,或改用 --use-npm。" >&2
exit 1
fi
fi
echo ">> 使用本 fork 二进制: $BIN"
fi
[[ -n "$BIN" ]] && export GITLINK_CLI_BIN="$BIN"
ARGS=(scripts/gitlink_workflow.py --config "$CONFIG" --window-days "$WINDOW_DAYS" --output-dir "$OUTPUT_DIR")
[[ -n "$OWNER" ]] && ARGS+=(--owner "$OWNER")
[[ -n "$REPO_NAME" ]] && ARGS+=(--repo "$REPO_NAME")
[[ "$PUBLISH_ISSUE_ID" -gt 0 ]] && ARGS+=(--publish-issue-id "$PUBLISH_ISSUE_ID")
[[ "$SKIP_RELEASES" -eq 1 ]] && ARGS+=(--skip-releases)
cd "$WF_ROOT"
python3 "${ARGS[@]}"

151
gitlink-web/README.md Normal file
View File

@ -0,0 +1,151 @@
# GitLink Skills Web Service科研智能分析服务
> 子任务四「应用 GitLink 辅助科研」的在线服务。基于 **Flask + DeepSeek API + gitlink-cli**,通过 AI Agent 自动分析 GitLink 平台数据,覆盖科研项目分析、热点追踪、合规校验、协作匹配、进度预警等全链路科研辅助场景。
>
> 🌐 在线地址:<http://121.41.212.97/skills/> 归属jiangtx/gitlink-cli 小组
---
## 一、它能做什么
输入一个研究主题或仓库地址,服务会自动调用 `gitlink-cli` 拉取 GitLink 平台数据,再由 DeepSeek 大模型按各 Skill 的工作流生成结构化分析报告。共 **14 个 Skill**,分两类:
### 🎯 核心 Skill开发者场景
| Skill | 入口 | 说明 | 典型输出 |
|---|---|---|---|
| 技术调研 `research-tracker` | `/research` | 输入主题→多关键词搜索→深度评估→调研报告 | 热点概览 / 项目排行 / 重点分析 / 趋势建议 |
| 贡献者分析 `contributor-insight` | `/contributor` | 仓库贡献者活跃度与团队健康度 | 团队概览 / 活跃排行 / 重点画像 |
| Issue 分拣 `issue-triage` | `/issue-triage` | 自动分类 Issue、评估紧急度 | 类型分类 / 紧急度 / 行动建议 |
| CI 健康巡检 `ci-health` | `/ci-health` | CI/CD 状态、构建成功率 | 健康度总览 / 构建趋势 / 故障分析 |
| 仓库健康巡检 `repo-health` | `/repo-health` | 综合活跃度、社区、代码产出 | 基本信息 / 活跃度 / 综合评分 |
| PR 效率分析 `pr-analytics` | `/pr-analytics` | PR 吞吐量、合并率 | 吞吐量 / 合并效率 / 贡献排行 |
| 跨维搜索 `cross-search` | `/cross-search` | 仓库+代码+Issue 三维搜索 | 各维命中 / 代码片段 / 讨论热点 |
| 用户分析 `user-analysis` | `/user-analysis` | 用户画像与项目参与 | 基本信息 / 活跃度 / 贡献列表 |
| 仓库对比 `repo-compare` | `/repo-compare` | 两仓库指标对比 | 基本/社区/活动对比 / 结论 |
### 🔬 科研实验室(科研场景,对应子任务四)
| Skill | 入口 | 科研场景 | 典型输出 |
|---|---|---|---|
| 热点追踪 `lab-hotspot` | `/lab-hotspot` | 多关键词搜索 + 领域知识图谱 | 热点概览 / 排行 / 知识图谱 / 趋势 |
| 项目洞悉 `lab-insight` | `/lab-insight` | 仓库+贡献者+PR/Issue 全息分析 | 项目概况 / 社区活跃 / 团队画像 |
| 合规检查 `lab-compliance` | `/lab-compliance` | License/CI/文档/复现性 | 合规评分 / 文档完整性 / 可复现性 |
| 协作匹配 `lab-match` | `/lab-match` | 新手友好度 + 社区健康度 | 项目概览 / 入门友好度 / 贡献方向 |
| 进度跟踪 `lab-track` | `/lab-track` | 多仓库批量巡检 + 三级告警 | 状态概览 / 指标表 / 预警详情 |
> 科研实验室 5 个 Skill 与《课程实践任务》子任务四场景一一对应:仓库级科研项目洞悉、科研热点追踪与知识图谱、科研项目合规与复现性检查、科研协作智能匹配、科研进度智能跟踪与预警。
---
## 二、架构
```
┌──────────────┐ HTTP ┌──────────────────────────────┐
│ 浏览器/评委 │ ───────► │ Flask (app.py, gunicorn) │
└──────────────┘ │ 路由 / /<slug> /api/run │
└──────────────┬───────────────┘
│ 按 Skill 选 agent_*()
┌──────────────▼───────────────┐
│ DeepSeek API (Chat) │
│ 注入 SKILL.md prompt + 日期 │
└──────────────┬───────────────┘
│ 工具调用 gitlink-cli
┌──────────────▼───────────────┐
│ gitlink-cli (npm 全局) │
│ repo/issue/pr/release/... │
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ GitLink 平台 OpenAPI │
└──────────────────────────────┘
```
- `app.py`Flask 服务14 个 `agent_*` / `lab_*` 函数分别对应 14 个 Skill每个严格遵循 `skills/*.txt` 里的工作流。
- `skills/*.txt`:各 Skill 的 prompt 工作流(与仓库 `skills/gitlink-*/SKILL.md` 同源)。
- `templates/``base.html` / `index.html` / `skill.html` 三套页面。
- `reports/`:运行时生成的历史报告(运行时创建)。
---
## 三、本地运行
```bash
# 1. 依赖
pip install -r requirements.txt # flask requests gunicorn
# 2. 安装 gitlink-cli数据源
npm install -g gitlink-cli
gitlink-cli auth login # 登录 GitLink
# 3. 配置大模型 KeyDeepSeek 或兼容 OpenAI 的接口)
export API_KEY="sk-xxxxxxxx"
export API_BASE="https://api.deepseek.com/v1"
export API_MODEL="deepseek-chat"
# 4. 启动
python app.py # 默认 5000 端口;--port=80 指定
# 访问 http://localhost:5000/
```
> `API_KEY` 也可不设环境变量,`app.py` 会回退到内置占位(仅本地测试)。生产环境务必用环境变量,不要把 Key 写进代码。
---
## 四、部署到服务器
### 方式 ADevOps 流水线(推荐,可复现)
仓库根目录 `.devops/gitlink-web.yml` 是建木流水线master 合并后自动 `git clone` → 同步 `gitlink-web/``/opt/gitlink-web` → 装 Python 依赖 → 写 systemd 单元 → 重启 → 健康检查。需在建木密钥组 `gitlink_cli` 中配置:
- `wyx_ssh_pass`:服务器 SSH 密码
- `deepseek_api_key`DeepSeek API Key
### 方式 B手动脚本
```bash
cd gitlink-web
# 编辑 deploy.py 顶部的 PASSWORD 与 API_KEY或改成从环境变量读
python deploy.py
```
`deploy.py` 用 paramiko 走 SSH装系统依赖 → 装 gitlink-cli → 上传文件 → 装 Python 依赖 → 写 systemd 服务 → 启动。服务以 `gitlink-web.service` 常驻,监听 80 端口。
---
## 五、API 端点
| 端点 | 方法 | 说明 |
|---|---|---|
| `/` | GET | 首页Skill 列表) |
| `/<slug>` | GET | 单个 Skill 表单页(如 `/research`、`/lab-hotspot` |
| `/api/run` | POST | 执行 Skillbody`{"skill":"research-tracker","input":"大模型"}`,返回生成报告 |
---
## 六、目录结构
```
gitlink-web/
├── app.py # Flask 服务 + 14 个 Skill 处理函数865 行)
├── deploy.py # paramiko 一键部署脚本
├── requirements.txt # flask / requests / gunicorn
├── skills/ # 各 Skill 的 prompt 工作流
│ ├── research-tracker.txt
│ ├── contributor-insight.txt
│ ├── issue-triage.txt
│ └── ci-health.txt
└── templates/
├── base.html
├── index.html
└── skill.html
```
---
## 七、与 CLI Skills 的关系
本服务的 `skills/*.txt` 与仓库 `skills/gitlink-*/SKILL.md` 同源、可互换:
- **CLI Skill**`skills/gitlink-research-tracker/SKILL.md`):供 Claude Code / Cursor 等 Agent 平台直接调用,离线、可复现。
- **Web Skill**(本服务):封装成 Web 界面 + 后端 DeepSeek评委无需装 Agent 即可在浏览器体验。
两者覆盖相同的科研场景,互为印证。

865
gitlink-web/app.py Normal file
View File

@ -0,0 +1,865 @@
#!/usr/bin/env python3
"""GitLink Skills Web Service — 严格遵循 SKILL.md 工作流"""
import os, re, json, subprocess, time, traceback
from pathlib import Path
from flask import Flask, request, jsonify, render_template
import requests
NOW = time.strftime("%Y-%m-%d") # 当前日期注入所有prompt防止幻觉
app = Flask(__name__)
NOW = time.strftime("%Y-%m-%d") # 当前日期,注入所有 prompt 防止幻觉
API_KEY = os.environ.get("API_KEY") or ""
if not API_KEY:
# 本地测试用
API_KEY = "<YOUR_API_KEY>"
API_BASE = os.environ.get("API_BASE", "https://api.deepseek.com/v1")
API_MODEL = os.environ.get("API_MODEL", "deepseek-chat")
SKILL_DIR = Path(__file__).parent / "skills"
HISTORY_DIR = Path(__file__).parent / "reports"
HISTORY_DIR.mkdir(exist_ok=True)
SKILL_NAMES = {
"research": "research-tracker",
"contributor": "contributor-insight",
"issue-triage": "issue-triage",
"ci-health": "ci-health",
"repo-health": "repo-health",
"pr-analytics": "pr-analytics",
"cross-search": "cross-search",
"user-analysis": "user-analysis",
"repo-compare": "repo-compare",
"lab-hotspot": "lab-hotspot",
"lab-insight": "lab-insight",
"lab-compliance": "lab-compliance",
"lab-match": "lab-match",
"lab-track": "lab-track",
}
SKILL_INFO = {
"research-tracker": {"title":"技术调研","label":"研究主题","placeholder":"大模型、AI Agent、微服务","intro":"输入一个研究主题,自动拆解为多个关键词在 GitLink 上搜索相关项目,深度评估后生成调研报告。","output_desc":"热点概览(项目数/语言/活跃度)\n项目排行榜(含评分/星数/Fork/链接)\n重点分析(核心项目详情)\n趋势洞察与建议"},
"contributor-insight":{"title":"贡献者分析","label":"仓库(owner/repo)","placeholder":"ci4s/ci4sManagement-cloud","intro":"输入仓库地址,分析贡献者的活跃度、贡献趋势和团队健康度。","output_desc":"团队概览(贡献者总数/级别分布)\n活跃度排行榜PR数/Issue数/趋势)\n重点贡献者分析(画像/PR明细\n团队健康度评估与建议"},
"issue-triage":{"title":"Issue分拣","label":"仓库(owner/repo)","placeholder":"Gitlink/gitlink-cli","intro":"输入仓库地址,自动扫描 Issue 并分类Bug/Feature/Question评估紧急度和复杂度。","output_desc":"Issue总览总数/开放/已关闭)\n类型分类Bug/Feature/Docs等\n紧急度评估Urgent/High/Normal/Low\n行动建议FixNow/Investigate/Discuss\n维护建议"},
"ci-health":{"title":"CI健康巡检","label":"仓库(owner/repo)","placeholder":"jiangtx/gitlink-cli","intro":"检查仓库 CI/CD 状态open_devops、构建历史、成功率。","output_desc":"健康度总览CI激活/成功率/稳定性评分)\n构建趋势近7天/14天/30天\n故障分析与改进建议"},
"repo-health":{"title":"仓库健康巡检","label":"仓库(owner/repo)","placeholder":"Gitlink/gitlink-cli","intro":"综合评估仓库的活跃度、社区规模、代码产出和风险。","output_desc":"基本信息(语言/规模/描述)\n活跃度分析PR/Issue统计\nPR/Issue健康度\n综合评分与改进建议"},
"pr-analytics":{"title":"PR效率分析","label":"仓库(owner/repo)","placeholder":"Gitlink/gitlink-cli","intro":"统计 PR 吞吐量、合并率、贡献者活跃度。","output_desc":"PR吞吐量总数/合并/关闭)\n合并效率分析\n贡献者排行榜\n改进建议"},
"cross-search":{"title":"跨维搜索","label":"搜索主题","placeholder":"AI Agent、数据分析、容器","intro":"同时搜索 GitLink 的仓库、代码和 Issue 三个维度。","output_desc":"各维度命中概况\n仓库搜索结果\n代码片段摘要\nIssue讨论热点"},
"user-analysis":{"title":"用户分析","label":"用户名","placeholder":"jiangtx、lindiwen23","intro":"查看用户基本信息、活跃度、项目参与情况。","output_desc":"基本信息(注册时间/身份/项目数)\n活跃度分析\n项目贡献列表\n综合用户画像"},
"repo-compare":{"title":"仓库对比","label":"AvsB","placeholder":"Gitlink/gitlink-cli vs ci4s/ci4sManagement-cloud","intro":"对比两个仓库的指标差异。","output_desc":"基本信息对比(语言/规模/分支)\n社区活跃度对比\n开发活动对比PR/Issue/Release\n综合结论"},
"lab-hotspot":{"title":"热点追踪","label":"研究主题","placeholder":"大模型、AI Agent、微服务","intro":"多关键词搜索GitLink项目深度评估+领域知识图谱。","output_desc":"热点概览(项目数/语言/活跃比例)\n项目排行榜(评分/星数/Fork/链接)\n领域知识图谱Mermaid流程图\n趋势洞察与建议"},
"lab-insight":{"title":"项目洞悉","label":"仓库(owner/repo)","placeholder":"ci4s/ci4sManagement-cloud","intro":"综合仓库信息+贡献者+PR/Issue生成全息分析。","output_desc":"项目概况(描述/规模/语言)\n社区活跃度(贡献者/PR/Issue\n团队画像\n综合健康度评估"},
"lab-compliance":{"title":"合规检查","label":"仓库(owner/repo)","placeholder":"Gitlink/gitlink-cli","intro":"检查License/CI/文档完整性,输出合规评分。","output_desc":"License合规性\n文档完整性\nCI/CD完善度\n可复现性检查\n综合评分"},
"lab-match":{"title":"协作匹配","label":"仓库(owner/repo)","placeholder":"ci4s/ci4sManagement-cloud","intro":"分析Issue和社区健康度评估新手友好度。","output_desc":"项目概览(技术栈/社区规模)\n入门友好度分析\n推荐贡献方向\n社区活跃度评估"},
"lab-track":{"title":"进度跟踪","label":"仓库列表(逗号分隔)","placeholder":"repo1,repo2,repo3","intro":"批量巡检多仓库,输出健康/警告/危险状态。","output_desc":"各仓库状态概览(健康/警告/危险)\n详细指标表(贡献者/CI/活跃度)\n预警详情\n整体健康度评估"},
}
# ── 工具函数 ──
def run(cmd, timeout=30):
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
out = r.stdout.strip()
if not out:
return r.stderr.strip()[:1000]
# JSON search results can be large, don't truncate too aggressively
return out[:50000]
except subprocess.TimeoutExpired:
return "[超时]"
except Exception as e:
return f"[错误] {e}"
def llm(messages, max_tokens=8192):
if not API_KEY:
return "【API_KEY 未设置】"
try:
resp = requests.post(
f"{API_BASE}/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
json={"model": API_MODEL, "messages": messages, "max_tokens": max_tokens, "temperature": 0.7},
timeout=180
)
data = resp.json()
if "choices" in data and data["choices"]:
return data["choices"][0]["message"]["content"]
return f"[API异常] {json.dumps(data, ensure_ascii=False)[:300]}"
except Exception as e:
return f"[请求失败] {e}"
def load_prompt(name):
p = SKILL_DIR / f"{name}.txt"
return p.read_text(encoding="utf-8") if p.exists() else ""
# ═══════════════════════════════════════════════════════════════════
# research-tracker严格遵循 SKILL.md 5步工作流
# Round 1: LLM 拆关键词 → 服务器执行搜索+深度评估 → Round 2: LLM 写报告
# ═══════════════════════════════════════════════════════════════════
def agent_research(user_input):
steps = [] # 记录每一步供对比
# ── Step 1: LLM 拆解关键词 ──
kw_prompt = f"""你是一个科研助手。请为研究主题「{user_input}」拆解 3~5 个搜索关键词。
要求覆盖中英文缩写全称技术术语和行业叫法
直接返回关键词列表每行一个不要多余文字"""
kw_response = llm([{"role": "user", "content": kw_prompt}])
keywords = [l.strip("-* \t") for l in kw_response.strip().split("\n") if l.strip()]
steps.append(("Step1-关键词拆解", {"prompt": kw_prompt, "llm_output": kw_response, "keywords": keywords}))
# ── Step 2: 多关键词搜索 ──
all_projects = {}
raw_searches = []
for kw in keywords[:5]:
out = run(f'gitlink-cli search +repos -k "{kw}" --format json')
raw_searches.append(f"--- {kw} ---\n{out}")
try:
for p in json.loads(out).get("data", {}).get("projects", []):
key = f"{p['author']['login']}/{p['identifier']}"
if key not in all_projects:
all_projects[key] = {"search_data": p, "keywords": [kw]}
else:
all_projects[key]["keywords"].append(kw)
except:
pass
steps.append(("Step2-多关键词搜索", {"keyword_count": len(keywords), "raw_hits": sum(1 for k in keywords), "unique": len(all_projects)}))
# ── Step 3: 去重 + 深度评估 ──
sorted_projects = sorted(all_projects.values(), key=lambda x: x["search_data"].get("praises_count", 0), reverse=True)[:8]
deep_data = []
seen_mirror = 0
for p in sorted_projects:
sd = p["search_data"]
owner, repo_name = sd["author"]["login"], sd["identifier"]
info = run(f'gitlink-cli repo +info --owner {owner} --repo {repo_name} --format json')
is_mirror = False
try:
info_data = json.loads(info)
is_mirror = info_data.get("data", {}).get("mirror", False)
if is_mirror:
seen_mirror += 1
except:
pass
url = f"https://www.gitlink.org.cn/{owner}/{repo_name}"
deep_data.append(f"# [{owner}/{repo_name}]({url})\nURL: {url}\n关键词: {', '.join(p['keywords'])}\n镜像: {'' if is_mirror else ''}\n{info}")
steps.append(("Step3-深度评估", {"deep_count": len(deep_data), "mirror_count": seen_mirror}))
# ── Step 4+5: LLM 写报告 ──
system = load_prompt("research-tracker")
real_data = "=== 搜索结果 ===\n" + "\n\n".join(raw_searches) + "\n\n=== 深度评估 ===\n" + "\n\n".join(deep_data)
report_prompt = f"""研究主题: {user_input}
搜索关键词: {', '.join(keywords)}
以下是从 GitLink 平台实时获取的真实数据
请基于这些数据严格按照 SKILL.md 的评分标准和模板生成调研报告
包含技术格局概览项目成熟度排行榜含评分重点分析趋势洞察调研建议
**超链接要求**报告中所有项目名必须使用 Markdown 超链接格式
- 格式[owner/repo](https://www.gitlink.org.cn/owner/repo)
- 排行榜重点分析推荐表中所有仓库名都必须是可点击的超链接
- 例如[Gitlink/microservices](https://www.gitlink.org.cn/Gitlink/microservices)
- 镜像项目也加链接[owner/repo](https://...)
{real_data}"""
report = llm([
{"role": "system", "content": system},
{"role": "user", "content": report_prompt}
])
steps.append(("Step4+5-报告生成", {"llm_input_chars": len(report_prompt), "report_chars": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# contributor-insight严格遵循 SKILL.md 5步工作流
# 服务器执行命令 → LLM 分析写报告
# ═══════════════════════════════════════════════════════════════════
def agent_contributor(user_input):
steps = []
parts = user_input.replace(" ", "").split("/")
if len(parts) < 2:
return "请提供 owner/repo 格式", []
owner, repo_name = parts[0], parts[1]
# Step 1: repo +info
info = run(f'gitlink-cli repo +info --owner {owner} --repo {repo_name} --format json')
steps.append(("Step1-项目概览", {"raw_len": len(info)}))
try:
contrib_count = json.loads(info).get("data", {}).get("contributor_users_count", 0)
except:
contrib_count = "?"
# Step 2: pr +list + issue +list
pr_data = run(f'gitlink-cli pr +list --owner {owner} --repo {repo_name} --format json')
issue_data = run(f'gitlink-cli issue +list --owner {owner} --repo {repo_name} --format json')
steps.append(("Step2-数据采集", {"pr_len": len(pr_data), "issue_len": len(issue_data)}))
# Step 3: user +info 提取画像
authors = set()
try:
for i in json.loads(pr_data).get("data", {}).get("issues", []):
if i.get("author_login"):
authors.add(i["author_login"])
except:
pass
user_infos = []
for a in list(authors)[:5]:
u = run(f'gitlink-cli user +info --login {a} --format json')
user_infos.append(f"# {a}\n{u}")
steps.append(("Step3-用户画像", {"author_count": len(authors)}))
# Step 4+5: LLM 分析写报告
system = load_prompt("contributor-insight")
data = f"# repo +info\n{info}\n\n# pr +list\n{pr_data}\n\n# issue +list\n{issue_data}\n\n" + "\n".join(user_infos)
prompt = f"""仓库: [{owner}/{repo_name}](https://www.gitlink.org.cn/{owner}/{repo_name})
实际贡献者(从PR提取): {', '.join(authors) if authors else ''}
repo +info contributor_users_count: {contrib_count}
以下是 gitlink-cli 获取的真实数据
请基于此生成贡献者洞察报告包含团队概览活跃度排行榜重点分析健康度评估建议
仓库名用超链接: [owner/repo](https://www.gitlink.org.cn/owner/repo)
如果数据缺失请如实标注不要编造
{data}"""
report = llm([
{"role": "system", "content": system},
{"role": "user", "content": prompt}
])
steps.append(("Step4+5-报告", {"llm_input": len(prompt), "report_len": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# issue-triage严格遵循 SKILL.md 5步工作流
# ═══════════════════════════════════════════════════════════════════
def agent_issue(user_input):
steps = []
parts = user_input.replace(" ", "").split("/")
if len(parts) < 2:
return "请提供 owner/repo 格式", []
owner, repo_name = parts[0], parts[1]
# Step 1: repo +info
info = run(f'gitlink-cli repo +info --owner {owner} --repo {repo_name} --format json')
steps.append(("Step1-项目概览", {}))
# Step 2: issue +list
issues_raw = run(f'gitlink-cli issue +list --owner {owner} --repo {repo_name} --state open --format json')
steps.append(("Step2-Issue列表", {"raw_len": len(issues_raw)}))
# 客户端按 status_id 过滤
open_issues = []
try:
for i in json.loads(issues_raw).get("data", {}).get("issues", []):
sid = i.get("status_id", -1)
if sid in (1, 2, 0):
open_issues.append(i)
except:
pass
steps.append(("Step2.5-状态过滤", {"before": "?", "after": len(open_issues)}))
# Step 3: issue +view 逐条分析
views = []
for issue in open_issues[:10]:
num = issue.get("project_issues_index") or issue.get("number", "")
if num:
v = run(f'gitlink-cli issue +view --owner {owner} --repo {repo_name} --number {num} --format json')
views.append(f"# Issue #{num}: {issue.get('subject','')}\n{v}")
steps.append(("Step3-逐条分析", {"count": len(views)}))
# Step 4+5: LLM
system = load_prompt("issue-triage")
data = f"# repo +info\n{info}\n\n# issue +list (所有)\n{issues_raw}\n\n过滤后开放({len(open_issues)}条):\n" + "\n\n".join(views)
prompt = f"""仓库: [{owner}/{repo_name}](https://www.gitlink.org.cn/{owner}/{repo_name})
过滤后开放Issue: {len(open_issues)}
请按SKILL.md的4维分类规则生成分拣报告
注意: --state open 过滤不准确, 已按 status_id(1=新增,2=处理中) 过滤, status_id=0已标注异常
仓库名用 gitlink.org.cn 超链接不要用 github.com
如果数据中有字段缺失请如实说
{data}"""
report = llm([
{"role": "system", "content": system},
{"role": "user", "content": prompt}
])
steps.append(("Step4+5-报告", {"report_len": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# ci-health严格遵循 SKILL.md 5步工作流
# ═══════════════════════════════════════════════════════════════════
def agent_ci(user_input):
steps = []
parts = user_input.replace(" ", "").split("/")
if len(parts) < 2:
return "请提供 owner/repo 格式", []
owner, repo_name = parts[0], parts[1]
# Step 1: repo +info → open_devops
info = run(f'gitlink-cli repo +info --owner {owner} --repo {repo_name} --format json')
open_devops = False
try:
d = json.loads(info).get("data", {})
open_devops = d.get("open_devops", False)
except:
pass
steps.append(("Step1-CI授权检查", {"open_devops": open_devops}))
if not open_devops:
return f"该仓库 CI/CD 未激活 (open_devops=false)。\n\n## repo 基本信息\n{info}\n\n建议在 GitLink Web 界面开启 DevOps 后重新巡检。", steps
# Step 2: ci +builds
builds = run(f'gitlink-cli ci +builds --owner {owner} --repo {repo_name} --format json')
steps.append(("Step2-构建历史", {"builds_len": len(builds)}))
# Step 3: ci +logs (失败构建)
failed_ids = []
try:
for b in json.loads(builds).get("data", {}).get("builds", [])[:5]:
if b.get("status") == "failed":
failed_ids.append(b.get("id", ""))
except:
pass
logs_data = []
for bid in failed_ids[:3]:
l = run(f'gitlink-cli ci +logs --owner {owner} --repo {repo_name} --build {bid} --format json')
logs_data.append(f"# Build {bid}\n{l}")
steps.append(("Step3-失败日志", {"failed_count": len(failed_ids)}))
# Step 4+5: LLM
system = load_prompt("ci-health")
data = f"# repo +info\n{info}\n\n# ci +builds\n{builds}\n\n" + ("\n".join(logs_data) if logs_data else "# 无失败构建")
prompt = f"""仓库: {owner}/{repo_name}
CI激活: {'' if open_devops else ''}
请生成 CI 健康巡检报告包含健康度总览构建趋势故障分析改进建议
open_devops=false 表示CI未激活此时无构建数据
{data}"""
report = llm([
{"role": "system", "content": system},
{"role": "user", "content": prompt}
])
steps.append(("Step4+5-报告", {"report_len": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# repo-health仓库健康巡检 — 综合评估仓库活动、代码规模、社区参与
# ═══════════════════════════════════════════════════════════════════
def agent_repo_health(user_input):
steps = []
parts = user_input.replace(" ", "").split("/")
if len(parts) < 2:
return "请提供 owner/repo 格式", []
owner, repo_name = parts[0], parts[1]
# repo +info
info = run(f'gitlink-cli repo +info --owner {owner} --repo {repo_name} --format json')
steps.append(("Step1-基本信息", {"len": len(info)}))
# issue +list + pr +list
issues = run(f'gitlink-cli issue +list --owner {owner} --repo {repo_name} --format json')
prs = run(f'gitlink-cli pr +list --owner {owner} --repo {repo_name} --format json')
steps.append(("Step2-活动数据", {"issues_len": len(issues), "prs_len": len(prs)}))
data = f"# repo +info\n{info}\n\n# issue +list\n{issues}\n\n# pr +list\n{prs}"
report = llm([
{"role": "system", "content": "你是一个仓库健康度评估专家。基于 gitlink-cli 获取的真实数据,评估仓库的综合健康度。"},
{"role": "user", "content": f"""仓库: {owner}/{repo_name}
请基于以下真实数据生成仓库健康巡检报告包含
1. 仓库基本信息语言规模创建时间
2. 代码活跃度Issue 数量PR 数量贡献者数
3. PR/Issue 健康度合并比例开放比例
4. 综合健康评分满分 20和风险提示
{data}"""}
])
steps.append(("Step3-报告", {"len": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# pr-analyticsPR 效率分析 — 分析合并时间、Review 模式、贡献节奏
# ═══════════════════════════════════════════════════════════════════
def agent_pr_analytics(user_input):
steps = []
parts = user_input.replace(" ", "").split("/")
if len(parts) < 2:
return "请提供 owner/repo 格式", []
owner, repo_name = parts[0], parts[1]
# pr +list 获取全量
all_prs = run(f'gitlink-cli pr +list --owner {owner} --repo {repo_name} --format json')
steps.append(("Step1-PR列表", {"len": len(all_prs)}))
# 提取统计
total, merged, authors = 0, 0, set()
try:
for i in json.loads(all_prs).get("data", {}).get("issues", []):
total += 1
if i.get("pull_request_status") == 1:
merged += 1
if i.get("author_login"):
authors.add(i["author_login"])
except:
pass
steps.append(("Step2-统计", {"total": total, "merged": merged, "authors": len(authors)}))
data = f"# pr +list\n{all_prs}"
report = llm([
{"role": "system", "content": "你是一个开源项目 PR 效率分析师。基于真实数据生成 PR 效率分析报告。"},
{"role": "user", "content": f"""仓库: {owner}/{repo_name}
PR: {total}, 已合并: {merged}, 贡献者: {len(authors)}
请基于以下真实数据生成 PR 效率分析报告包含
1. PR 吞吐量总数合并数关闭数
2. 贡献者活跃度人均 PR Top 贡献者
3. 合并效率合并比例
4. 改进建议
{data}"""}
])
steps.append(("Step3-报告", {"len": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# cross-search跨维搜索 — 同时搜仓库、代码、Issue 并汇总
# ═══════════════════════════════════════════════════════════════════
def agent_cross_search(user_input):
steps = []
kw = user_input.strip()
# 三维搜索
repos = run(f'gitlink-cli search +repos -k "{kw}" --format json')
code = run(f'gitlink-cli search +code -k "{kw}" --format json')
issues = run(f'gitlink-cli search +issues -k "{kw}" --format json')
steps.append(("Step1-三维搜索", {"repos_len": len(repos), "code_len": len(code), "issues_len": len(issues)}))
# 统计
repo_count, code_count, issue_count = 0, 0, 0
try: repo_count = len(json.loads(repos).get("data",{}).get("projects",[]))
except: pass
try: code_count = len(json.loads(code).get("data",{}).get("results",[]))
except: pass
try: issue_count = len(json.loads(issues).get("data",{}).get("issues",[]))
except: pass
data = f"# 仓库搜索\n{repos}\n\n# 代码搜索\n{code}\n\n# Issue搜索\n{issues}"
report = llm([
{"role": "system", "content": "你是一个搜索分析师。汇总 GitLink 多维度搜索结果,生成综合分析报告。所有仓库名用 Markdown 超链接 [owner/repo](https://www.gitlink.org.cn/owner/repo) 格式。"},
{"role": "user", "content": f"""搜索关键词: {kw}
仓库命中: {repo_count} 代码命中: {code_count} Issue命中: {issue_count}
请基于以下搜索数据生成综合分析报告包含
1. 各维度命中概况
2. 仓库搜索结果分析
3. 代码搜索结果提炼热门代码片段
4. Issue 讨论热点
5. 综合洞察
{data}"""}
])
steps.append(("Step2-报告", {"len": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# user-analysis用户分析 — 搜索用户 + 查看用户信息
# ═══════════════════════════════════════════════════════════════════
def agent_user_analysis(user_input):
steps = []
login = user_input.strip()
# 搜索用户
search = run(f'gitlink-cli search +users -k "{login}" --format json')
steps.append(("Step1-用户搜索", {"len": len(search)}))
# 用户信息
info = run(f'gitlink-cli user +info --login {login} --format json')
steps.append(("Step2-用户信息", {"len": len(info)}))
data = f"# search +users\n{search}\n\n# user +info\n{info}"
report = llm([
{"role": "system", "content": "你是一个用户分析专家。基于 GitLink 用户数据生成用户分析报告。"},
{"role": "user", "content": f"""用户名: {login}
请基于以下真实数据生成用户分析报告
1. 用户基本信息注册时间身份项目数
2. 用户活跃度参与项目关注数
3. 搜索匹配情况
4. 综合画像
{data}"""}
])
steps.append(("Step3-报告", {"len": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# repo-compare仓库对比 — 对比两个仓库的核心指标
# ═══════════════════════════════════════════════════════════════════
def agent_repo_compare(user_input):
steps = []
# 解析 "A vs B" 格式
parts = [p.strip() for p in user_input.replace("vs", " vs ").split("vs") if p.strip()]
if len(parts) < 2:
return "请用 A vs B 格式输入两个仓库例如Gitlink/gitlink-cli vs ci4s/ci4sManagement-cloud", []
repo_a, repo_b = parts[0], parts[1]
o1, r1 = repo_a.replace(" ", "").split("/")[:2]
o2, r2 = repo_b.replace(" ", "").split("/")[:2]
info_a = run(f'gitlink-cli repo +info --owner {o1} --repo {r1} --format json')
info_b = run(f'gitlink-cli repo +info --owner {o2} --repo {r2} --format json')
steps.append(("Step1-获取数据", {"repo_a_len": len(info_a), "repo_b_len": len(info_b)}))
data = f"# 仓库A: [{repo_a}](https://www.gitlink.org.cn/{repo_a})\n{info_a}\n\n# 仓库B: [{repo_b}](https://www.gitlink.org.cn/{repo_b})\n{info_b}"
report = llm([
{"role": "system", "content": "你是一个仓库对比分析师。对比两个 GitLink 仓库的指标异同。报告中所有仓库名必须用 Markdown 超链接 [owner/repo](https://www.gitlink.org.cn/owner/repo) 格式。"},
{"role": "user", "content": f"""仓库A: {repo_a}
仓库B: {repo_b}
请基于真实数据生成仓库对比报告
1. 基本信息对比语言规模分支
2. 社区活跃度对比贡献者watchfork
3. 开发活动对比PRIssueRelease
4. 综合结论与推荐
{data}"""}
])
steps.append(("Step2-报告", {"len": len(report)}))
return report, steps
# ═══════════════════════════════════════════════════════════════════
# 科研实验室5 个科研场景工作流
# ═══════════════════════════════════════════════════════════════════
def lab_hotspot(user_input):
"""热点追踪+知识图谱:搜项目 → 深度评估 → 提取Fork关系链"""
steps = []
# 拆关键词
kw_resp = llm([{"role": "user", "content": f"为研究主题「{user_input}」拆 3~5 个搜索关键词,每行一个"}])
kws = [l.strip("-* \t") for l in kw_resp.strip().split("\n") if l.strip()][:5]
steps.append(("Step1-关键词拆解", {"kws": kws}))
# 搜索
all_p = {}
for kw in kws:
out = run(f'gitlink-cli search +repos -k "{kw}" --format json')
try:
for p in json.loads(out).get("data",{}).get("projects",[]):
k = f"{p['author']['login']}/{p['identifier']}"
all_p.setdefault(k, {"data": p, "kws": []})["kws"].append(kw)
except: pass
steps.append(("Step2-搜索", {"unique": len(all_p)}))
# 深度评估 + 提取 fork 关系
sorted_p = sorted(all_p.values(), key=lambda x: x["data"].get("praises_count",0), reverse=True)[:8]
deep, relations = [], []
for p in sorted_p:
sd = p["data"]; owner, repo = sd["author"]["login"], sd["identifier"]
info = run(f'gitlink-cli repo +info --owner {owner} --repo {repo} --format json')
try:
d = json.loads(info).get("data",{})
mirror = d.get("mirror",False)
fork_from = d.get("forked_from_project_id")
url = f"https://www.gitlink.org.cn/{owner}/{repo}"
deep.append(f"[{owner}/{repo}]({url})\n关键词: {', '.join(p['kws'])}\n镜像: {mirror}\nFork来源: {fork_from}\n{info}")
# 知识图谱关系
if fork_from:
relations.append(f"{owner}/{repo} → Fork了 → 项目ID {fork_from}")
if d.get("fork_info"):
parent = d["fork_info"].get("fork_form_name","?")
relations.append(f"{owner}/{repo} → Fork自 → {d['fork_info'].get('fork_project_user_login','?')}/{parent}")
except: pass
steps.append(("Step3-深度评估+Fork图谱", {"deep": len(deep), "relations": len(relations)}))
data = "=== 搜索结果 ===\n" + "\n".join([f"--- {kw} ---\n{run(f'gitlink-cli search +repos -k \"{kw}\" --format json')}" for kw in kws]) + "\n\n=== Fork关系 ===\n" + "\n".join(relations) + "\n\n=== 深度评估 ===\n" + "\n".join(deep)
report = llm([
{"role": "system", "content": "你是一个科研热点分析师。基于 GitLink 真实数据生成热点追踪+知识图谱报告。所有仓库链接必须用 gitlink.org.cn 域名,不要用 github.com。"},
{"role": "user", "content": f"""当前日期: {NOW}
研究主题: {user_input}
关键词: {', '.join(kws)}
请生成报告日期写{NOW}
1. 热点概览项目数语言活跃项目比例
2. 项目排行榜含成熟度评分仓库名用 gitlink.org.cn 超链接不要用 github.com
3. **领域知识图谱** Mermaid 流程图画出该研究领域的知识结构图包含核心概念子方向关键技术及其关系不是项目Fork关系而是学术概念之间的关系
4. 趋势洞察与建议
Mermaid 知识图谱示例格式使用纯文本不要用 HTML 标签或 br**每个连接单独一行不要用 & 号连接多个节点**
```mermaid
flowchart LR
A[核心概念] --> B[子方向1]
A --> C[子方向2]
B --> D[技术方法1]
C --> E[技术方法2]
```
{data}"""}
])
# 清理 AI 在 Mermaid 代码块中混入的 HTML 标签和非法语法
report_clean = re.sub(r'<[^>]+>', '', report)
report_clean = re.sub(r'(\w+)\s*&', '', report_clean) # 去掉 X & Y --> Z 中的 &
steps.append(("Step4-报告", {"len": len(report_clean)}))
return report_clean, steps
def lab_insight(user_input):
"""项目洞悉:一个仓库的综合全息分析"""
steps = []
parts = user_input.replace(" ","").split("/")
if len(parts) < 2: return "请提供 owner/repo 格式", []
o, r = parts[0], parts[1]
url_s = f"https://www.gitlink.org.cn/{o}/{r}"
info = run(f'gitlink-cli repo +info --owner {o} --repo {r} --format json')
prs = run(f'gitlink-cli pr +list --owner {o} --repo {r} --format json')
issues = run(f'gitlink-cli issue +list --owner {o} --repo {r} --format json')
steps.append(("Step1-数据采集", {}))
# 提取贡献者
authors = set()
try:
for i in json.loads(prs).get("data",{}).get("issues",[]):
if i.get("author_login"): authors.add(i["author_login"])
except: pass
user_d = []
for a in list(authors)[:5]:
u = run(f'gitlink-cli user +info --login {a} --format json')
user_d.append(f"# {a}\n{u}")
data = f"# [{o}/{r}]({url_s})\n{info}\n\n# PR\n{prs}\n\n# Issues\n{issues}\n\n" + "\n".join(user_d)
report = llm([
{"role": "system", "content": f"你是一个项目洞悉分析师。当前日期{NOW}。综合仓库数据生成全息分析报告。"},
{"role": "user", "content": f"""仓库: [{o}/{r}]({url_s})
贡献者: {', '.join(authors) if authors else ''}
请综合以下数据生成项目洞悉报告包含
1. 项目概况语言规模描述
2. 社区活跃度贡献者PRIssue
3. 团队画像核心贡献者特点
4. 综合健康度评估与建议
所有仓库名用超链接 [owner/repo](https://www.gitlink.org.cn/owner/repo)
{data}"""}
])
steps.append(("Step2-报告", {"len": len(report)}))
return report, steps
def lab_compliance(user_input):
"""合规复现检查License、README、CI、Release"""
steps = []
parts = user_input.replace(" ","").split("/")
if len(parts) < 2: return "请提供 owner/repo 格式", []
o, r = parts[0], parts[1]
url_s = f"https://www.gitlink.org.cn/{o}/{r}"
info = run(f'gitlink-cli repo +info --owner {o} --repo {r} --format json')
builds = run(f'gitlink-cli ci +builds --owner {o} --repo {r} --format json')
steps.append(("Step1-数据采集", {}))
# 提取关键字段
has_license, has_readme, has_ci, size, desc, release = "?", "?", "?", "?", "?", 0
try:
d = json.loads(info).get("data",{})
has_license = "✅ 有" if d.get("license_id") else "❌ 无"
has_readme = "✅ 有" if d.get("description") else "⚠️ 可能无"
has_ci = "✅ 已激活" if d.get("open_devops") else "❌ 未激活"
size = d.get("size","?")
desc = (d.get("description") or "无描述")[:100]
release = d.get("version_releases_count",0)
except: pass
data = f"# [{o}/{r}]({url_s})\nLicense: {has_license}\nCI: {has_ci}\nSize: {size}\nRelease: {release}\nDesc: {desc}\n{info}\n\n# CI Builds\n{builds}"
report = llm([
{"role": "system", "content": f"你是一个开源合规分析师。当前日期{NOW}。检查仓库的合规性和可复现性。"},
{"role": "user", "content": f"""仓库: [{o}/{r}]({url_s})
检查结果
- License: {has_license}
- CI激活: {has_ci}
- Release: {release}
- 描述: {desc}
请在此基础上生成合规检查报告
1. License 合规性是否有许可证是否开源友好
2. 文档完整性README描述
3. CI/CD 完善度是否激活构建历史
4. 可复现性Release依赖管理
5. 综合评分满分 20和改进建议
{data}"""}
])
steps.append(("Step2-报告", {"len": len(report)}))
return report, steps
def lab_match(user_input):
"""协作匹配:找入门 Issue + 评估社区友好度"""
steps = []
parts = user_input.replace(" ","").split("/")
if len(parts) < 2: return "请提供 owner/repo 格式", []
o, r = parts[0], parts[1]
url_s = f"https://www.gitlink.org.cn/{o}/{r}"
info = run(f'gitlink-cli repo +info --owner {o} --repo {r} --format json')
prs = run(f'gitlink-cli pr +list --owner {o} --repo {r} --format json')
issues = run(f'gitlink-cli issue +list --owner {o} --repo {r} --state open --format json')
steps.append(("Step1-数据采集", {}))
data = f"# [{o}/{r}]({url_s})\n{info}\n\n# PR\n{prs}\n\n# Open Issues\n{issues}"
report = llm([
{"role": "system", "content": f"你是一个开源协作匹配专家。当前日期{NOW}。评估项目对新贡献者的友好度。"},
{"role": "user", "content": f"""仓库: [{o}/{r}]({url_s})
请生成协作匹配报告
1. 项目概览技术栈社区规模
2. 入门友好度分析
- 是否有 good-first-issue 标签
- Issue 描述是否清晰
- PR Review 是否及时
3. 推荐适合贡献的方向具体 Issue 或模块
4. 社区健康度贡献者多样性响应速度
仓库名用 gitlink.org.cn 超链接不要用 github.com
{data}"""}
])
steps.append(("Step2-报告", {"len": len(report)}))
return report, steps
def lab_track(user_input):
"""进度跟踪与预警:监控多个仓库的活动状态"""
steps = []
repos = [r.strip().replace(" ","") for r in user_input.split(",") if r.strip()]
if not repos: return "请提供仓库列表,用逗号分隔", []
items = []
warns = {"stale": [], "no_ci": [], "inactive": []}
for repo in repos:
if "/" not in repo: continue
o, rn = repo.split("/")[:2]
info = run(f'gitlink-cli repo +info --owner {o} --repo {rn} --format json')
try:
d = json.loads(info).get("data",{})
contrib = d.get("contributor_users_count",0)
ci = d.get("open_devops",False)
updated = d.get("full_name","?")
items.append({"repo": repo, "contrib": contrib, "ci": ci, "data": d})
if not ci: warns["no_ci"].append(repo)
if contrib == 0: warns["inactive"].append(repo)
except: pass
steps.append(("Step1-巡检", {"count": len(items), "warns": {k: len(v) for k,v in warns.items()}}))
def repo_info_line(repo):
o, rn = repo.split("/")[:2]
return f"# {repo}\n{run(f'gitlink-cli repo +info --owner {o} --repo {rn} --format json')}"
data_lines = [repo_info_line(r) for r in repos if "/" in r]
data = "\n\n".join(data_lines)
warn_info = "\n".join([f"- ⚠️ {r}: {'CI未激活' if r in warns['no_ci'] else ''} {'无活跃贡献者' if r in warns['inactive'] else ''}" for r in repos if r in warns['no_ci'] or r in warns['inactive']]) or "无预警"
report = llm([
{"role": "system", "content": f"你是一个开源项目进度跟踪分析师。当前日期{NOW}。生成多仓库进度报告和预警。"},
{"role": "user", "content": f"""监控仓库: {', '.join(repos)}
预警信息: {warn_info}
请生成进度跟踪报告
1. 各仓库状态概览健康/警告/危险
2. 详细状态表贡献者CI活跃度
3. 预警详情哪些仓库需要关注
4. 整体健康度评估
{data}"""}
])
steps.append(("Step3-报告", {"len": len(report)}))
return report, steps
LAB_AGENTS = {
"lab-hotspot": lab_hotspot,
"lab-insight": lab_insight,
"lab-compliance": lab_compliance,
"lab-match": lab_match,
"lab-track": lab_track,
}
AGENTS = {
"research-tracker": agent_research,
"contributor-insight": agent_contributor,
"issue-triage": agent_issue,
"ci-health": agent_ci,
"repo-health": agent_repo_health,
"pr-analytics": agent_pr_analytics,
"cross-search": agent_cross_search,
"user-analysis": agent_user_analysis,
"repo-compare": agent_repo_compare,
**LAB_AGENTS,
}
# ── Routes ──
@app.route("/")
def index():
return render_template("index.html", skills=SKILL_INFO, url_map={v: k for k, v in SKILL_NAMES.items()})
@app.route("/<slug>")
def page(slug):
name = SKILL_NAMES.get(slug, slug)
info = SKILL_INFO.get(name)
if not info:
return "Skill not found", 404
return render_template("skill.html", skill_name=name, info=info)
@app.route("/api/run", methods=["POST"])
def api_run():
name = request.form.get("skill", "")
user_input = request.form.get("input", "").strip()
if not name or not user_input:
return jsonify({"error": "缺少参数"}), 400
agent = AGENTS.get(name)
if not agent:
return jsonify({"error": f"未知 skill: {name}"}), 400
try:
report, steps = agent(user_input)
except Exception as e:
return jsonify({"error": str(e), "traceback": traceback.format_exc()}), 500
timestamp = time.strftime("%Y%m%d_%H%M%S")
(HISTORY_DIR / f"{name}_{timestamp}.md").write_text(
f"# {SKILL_INFO[name]['title']} 报告\n\n## 输入\n{user_input}\n\n## 结果\n\n{report}\n\n---\n*生成: {time.strftime('%Y-%m-%d %H:%M:%S')}*",
encoding="utf-8"
)
return jsonify({"report": report, "steps": steps})
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--port", type=int, default=int(os.environ.get("PORT", 5000)))
p.add_argument("--host", default="0.0.0.0")
p.add_argument("--debug", action="store_true", default=False)
args = p.parse_args()
print(f"GitLink Skills Web Service: http://{args.host}:{args.port}")
app.run(host=args.host, port=args.port, debug=args.debug)

104
gitlink-web/deploy.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Deploy GitLink Skills Web Service to Alibaba Cloud."""
import os
import sys
import subprocess
HOST = "121.41.212.97"
PORT = 22
USER = "root"
PASSWORD = "<YOUR_PASSWORD>"
API_KEY = "<YOUR_API_KEY>"
LOCAL_DIR = os.path.dirname(os.path.abspath(__file__))
REMOTE_DIR = "/opt/gitlink-web"
try:
import paramiko
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "paramiko", "-q"])
import paramiko
def run_ssh(ssh, cmd):
_, stdout, stderr = ssh.exec_command(cmd)
exit_code = stdout.channel.recv_exit_status()
out = stdout.read().decode().strip()
err = stderr.read().decode().strip()
return out, err, exit_code
def main():
print(f"[1/8] Connecting to {HOST}...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, PORT, USER, PASSWORD, timeout=10)
print(" OK - Connected")
print("[2/8] Installing system dependencies...")
run_ssh(ssh, "apt-get update -qq && apt-get install -y -qq python3-pip npm nodejs 2>&1 | tail -3")
print("[3/8] Installing gitlink-cli...")
run_ssh(ssh, "npm install -g gitlink-cli 2>&1 | tail -3")
v, _, _ = run_ssh(ssh, "gitlink-cli version 2>&1 || echo 'not found'")
print(f" gitlink-cli: {v}")
print(f"[4/8] Creating {REMOTE_DIR}...")
run_ssh(ssh, f"mkdir -p {REMOTE_DIR}/templates {REMOTE_DIR}/skills {REMOTE_DIR}/reports")
print("[5/8] Uploading files...")
sftp = ssh.open_sftp()
for root, dirs, files in os.walk(LOCAL_DIR):
for fname in files:
if fname.endswith(('.py', '.txt', '.html')) or fname in ('requirements.txt', 'README.md'):
local_path = os.path.join(root, fname)
rel_path = os.path.relpath(local_path, LOCAL_DIR)
remote_path = f"{REMOTE_DIR}/{rel_path}"
try:
sftp.put(local_path, remote_path)
print(f" -> {rel_path}")
except Exception as e:
print(f" FAIL {rel_path}: {e}")
sftp.close()
print("[6/8] Installing Python dependencies...")
run_ssh(ssh, f"cd {REMOTE_DIR} && pip3 install -r requirements.txt -q 2>&1 | tail -3")
print("[7/8] Creating systemd service...")
service = '\n'.join([
'[Unit]',
'Description=GitLink Skills Web Service',
'After=network.target',
'',
'[Service]',
'Type=simple',
'User=root',
f'WorkingDirectory={REMOTE_DIR}',
f'Environment="API_KEY={API_KEY}"',
'Environment="API_MODEL=deepseek-chat"',
f'ExecStart=/usr/bin/python3 {REMOTE_DIR}/app.py --port=80',
'Restart=always',
'RestartSec=5',
'',
'[Install]',
'WantedBy=multi-user.target',
])
escaped_service = service.replace('"', '\\"').replace("'", "\\'")
run_ssh(ssh, f"cat > /etc/systemd/system/gitlink-web.service << 'EOF'\n{service}\nEOF")
print("[8/8] Starting service...")
run_ssh(ssh, "systemctl daemon-reload")
run_ssh(ssh, "systemctl enable gitlink-web")
_, err, code = run_ssh(ssh, "systemctl start gitlink-web")
if err and "Unit" not in err:
print(f" WARN: {err[:200]}")
out, _, _ = run_ssh(ssh, "systemctl status gitlink-web --no-pager -l | head -15")
print(f"\n {out.replace(chr(10), chr(10)+' ')}")
print(f"\nService URLs:")
print(f" http://{HOST}/")
print(f" http://{HOST}/research")
print(f" http://{HOST}/contributor")
ssh.close()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,3 @@
flask>=3.0
requests>=2.31
gunicorn>=21.2

View File

@ -0,0 +1,179 @@
# gitlink-ci-healthCI 健康巡检)
**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。**
**CRITICAL — 本 Skill 为只读操作。CI 激活/关闭需通过 GitLink Web 界面操作CLI 不提供对应命令。**
**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`GitHub CLI操作 GitLink 资源。**
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。
---
## 功能概述
面向维护者的 CI/CD 健康度巡检工具:
1. **授权检查** — 确认仓库 CI 是否已激活
2. **构建历史** — 获取近期构建列表
3. **成功率统计** — 计算构建成功率和平均耗时
4. **故障分析** — 识别频繁失败的构建及其原因
5. **健康报告** — 生成 CI 健康度评分和改进建议
---
## 工作流CI 健康巡检
### Step 1检查 CI 授权状态
**方法 1推荐**:通过 `repo +info` 查看 `open_devops` 字段:
```bash
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
```
- `"open_devops": true` → CI 已激活
- `"open_devops": false` → CI 未激活
**方法 2**:直接调用 `ci +builds`CI 未激活时返回:
```json
{"status": -1, "message": "接口数据异常"}
```
> ⚠️ `ci +authorize` 命令在当前 CLI 版本v0.1.18)中**不存在**。可用 CI 命令仅:`+builds`、`+logs`、`+restart`、`+stop`。
若 CI 未激活,报告中说明"CI 未启用",建议通过 GitLink Web 界面(仓库设置 → DevOps开启随后不再继续后续步骤。
### Step 2获取构建历史
```bash
gitlink-cli ci +builds --owner <owner> --repo <repo> --format json
```
提取每次构建的:
- `status` — 构建状态success/failed/running/pending
- `created_at` / `finished_at` — 时间信息
- `duration` — 耗时(如有)
- `branch` — 触发分支
如构建数量 >30取最近 30 次分析。
### Step 3构建日志失败构建
对状态为 failed 的构建获取日志:
```bash
gitlink-cli ci +logs --owner <owner> --repo <repo> --build <build_id> --format json
```
> ⚠️ **控制调用量**:仅对最近 5 次失败构建获取日志,避免过多 API 调用。日志可能过大,提取关键错误行(最后 20 行)。
### Step 4统计分析
#### 4.1 成功率计算
| 指标 | 计算方式 |
|------|----------|
| 整体成功率 | 成功构建数 / 总构建数 × 100% |
| 近 10 次成功率 | 最近 10 次中成功占比 |
| 平均修复时间 | 从失败到下次成功的平均间隔 |
#### 4.2 健康度评分(满分 20
| 维度 | 权重 | 评分标准 |
|------|------|----------|
| CI 激活 | 4 | 已激活=4未激活=0 |
| 构建成功率 | 5 | ≥90%=5≥80%=4≥70%=3≥50%=2<50%=1 |
| 近期稳定性 | 5 | 近10次全部成功=58-9次=46-7次=34-5次=2<4次=1 |
| 构建频率 | 3 | 每天有构建=32-3天=2每周=1更少=0 |
| 修复速度 | 3 | 失败后1次内修复=32-3次=2>3次=1 |
### Step 5生成 CI 健康报告
---
## 输出模板
```markdown
# 🔧 CI 健康巡检报告:{{仓库名}}
> 巡检时间:{{当前时间}}
> 仓库:{{full_name}}
> CI 状态:{{ci_status_display}}
---
## 一、健康度总览
| 指标 | 数值 | 评分 |
|------|------|------|
| CI 激活状态 | {{activated_status}} | {{activate_score}}/4 |
| 整体成功率 | {{success_rate}}%{{success_count}}/{{total_count}} | {{success_score}}/5 |
| 近期稳定性 | 近 10 次 {{recent_success}} 次成功 | {{stability_score}}/5 |
| 构建频率 | {{build_frequency_desc}} | {{frequency_score}}/3 |
| 修复速度 | {{repair_speed_desc}} | {{repair_score}}/3 |
| **总分** | | **{{total_score}}/20** |
## 二、构建趋势
```
最近 20 次构建:
✅✅❌✅✅✅❌✅✅✅✅✅❌✅✅✅✅✅✅
(✅=成功 ❌=失败)
```
| 时间段 | 总构建 | 成功 | 失败 | 成功率 |
|--------|--------|------|------|--------|
| 最近 7 天 | {{w1_total}} | {{w1_success}} | {{w1_fail}} | {{w1_rate}}% |
| 7-14 天 | {{w2_total}} | {{w2_success}} | {{w2_fail}} | {{w2_rate}}% |
| 14-30 天 | {{w3_total}} | {{w3_success}} | {{w3_fail}} | {{w3_rate}}% |
## 三、故障分析
> 如无失败构建,输出:**🎉 分析期内无失败构建CI 运行健康。**
| 构建 ID | 分支 | 失败时间 | 错误摘要 |
|---------|------|----------|----------|
| {{id}} | {{branch}} | {{time}} | {{error_summary}} |
### 故障模式分类
| 故障类型 | 次数 | 占比 |
|----------|------|------|
| 编译错误 | {{compile_count}} | {{compile_pct}}% |
| 测试失败 | {{test_fail_count}} | {{test_fail_pct}}% |
| 超时 | {{timeout_count}} | {{timeout_pct}}% |
| 环境问题 | {{env_count}} | {{env_pct}}% |
| 其他 | {{other_count}} | {{other_pct}}% |
## 四、改进建议
<!-- 根据分析结果,从以下列表中选择匹配的建议输出 -->
- **立即激活 CI**(当 CI 未激活时):前往 GitLink Web 界面 → 仓库设置 → DevOps 开启 CI/CD 服务CLI 暂不支持 `ci +activate`
- **提升成功率**(当 success_rate < 80% 时):优先修复高频失败原因
- **增加构建频率**(当构建频率评分 < 2 时):建议每次 push 触发 CI
- **缩短修复时间**(当修复速度评分 < 2 时):建立 CI 失败告警
```
---
## 异常场景处理
| 场景 | 处理方式 |
|------|----------|
| CI 未激活 | 报告 CI 状态为"未激活",建议通过 Web 界面开启,不再继续后续步骤 |
| 无构建记录 | 标注"仓库暂无 CI 构建记录" |
| `ci +logs` 返回空 | 标注"日志不可用" |
| 构建总数 < 5 | 样本量不足,标注"数据有限,统计不具代表性" |
---
## 注意事项
- ✅ **所有命令使用 `--format json`**,确保可解析
- ✅ **CI 激活/关闭需通过 GitLink Web 界面**CLI 不提供 `+activate`/`+deactivate` 命令
- ✅ **Owner/repo 优先从 `git remote` 自动解析**
- ⚠️ **`ci +logs` 输出可能很大**,仅提取关键错误行
- ⚠️ **构建历史无分页参数**,实际返回条数取决于 API
- ⚠️ **CI 数据仅反映 GitLink 平台活动**,不包括第三方 CI 服务
- ⚠️ **`repo +info` 的 `open_devops` 字段**是判断 CI 是否激活的最可靠方式

View File

@ -0,0 +1,247 @@
# gitlink-contributor-insight贡献者活跃度分析
**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。**
**CRITICAL — 本 Skill 为只读操作,不会修改任何仓库。无需用户额外确认即可执行。**
**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`GitHub CLI操作 GitLink 资源。**
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。
---
## ⚠️ 命令可用性声明
gitlink-cli 的命令集在持续演进中。以下命令**当前版本可能不可用**,执行前先验证:
| 命令 | 状态 | 替代方案 |
|------|------|----------|
| `repo +contributors` | ❌ 不可用 | 从 `pr +list` 提取 `author_login` + `repo +info` 获取 `contributor_users_count` |
| `user +heatmap` | ❌ 不可用 | 从 PR 时间戳手动推算活跃天数 |
| `user +stats` | ❌ 不可用 | 从 `pr +list` 统计 PR 数Issue 数通过 `issue +list` 获取 |
| `user +trends` | ❌ 不可用 | 从 PR 时间分布手动判断趋势(上升/平稳/下降) |
| `repo +info` | ✅ 可用 | — |
| `pr +list` | ✅ 可用 | — |
| `user +info` | ✅ 可用 | — |
| `issue +list` | ✅ 可用 | — |
> **核心原则**:优先使用可用的 Shortcut 命令。当所需命令不可用时,从 `pr +list` 和 `user +info` 中提取等效数据,并在报告中标注"数据来源PR 列表(命令 X 不可用)"。
---
## 功能概述
面向开源社区管理者和维护者的贡献者分析工具:
1. **项目概览** — 获取仓库贡献者规模和基本信息
2. **贡献数据分析** — 通过 `pr +list` 提取每位贡献者的 PR 数量和时间分布
3. **用户画像** — 通过 `user +info` 了解贡献者背景
4. **趋势判断** — 从 PR 时间序列推断贡献趋势
5. **洞察报告** — 生成贡献者活跃度排名和团队健康度评估
---
## 工作流:贡献者分析全流程
### Step 1获取仓库基本信息
```bash
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
```
提取:`contributor_users_count`、`full_name`、`description`、`default_branch`、`fork_info`(如为 Fork 项目)。
### Step 2获取贡献者列表通过 PR 数据)
由于 `repo +contributors` 不可用,改用两步获取贡献者:
```bash
# 2a. 获取所有 PR含已合并和已关闭
gitlink-cli pr +list --owner <owner> --repo <repo> --format json
# 2b. 如果 Issue 数据也需要
gitlink-cli issue +list --owner <owner> --repo <repo> --format json
```
从 `pr +list` 返回数据中:
- 提取所有唯一的 `author_login` 作为实际代码贡献者
- 统计每位作者的 PR 数(`pull_request_status`: 0=open, 1=merged, 2=closed
- 记录每个 PR 的 `pr_full_time` 用于时间分析
- 记录每个 PR 的 `journals_count`(评论/审核活动数)
从 `issue +list` 返回数据中:
- 提取所有唯一的 `author_login` 作为 Issue 参与者
- 统计每位作者的 Issue 数
> 如果 PR 数量较多(>50按 `author_login` 聚合后取 PR 数前 10 的贡献者分析,报告中注明"基于 Top 10 分析"。
### Step 3逐位贡献者深度分析
对每位贡献者执行:
```bash
# 用户基本信息
gitlink-cli user +info --login <username> --format json
```
从 `user +info` 提取:`login`、`name`、`created_time`(注册时间)、`user_projects_count`、`user_org_count`、`user_identity`。
**如果 `user +heatmap/+stats/+trends` 可用**(未来版本),补充执行。当前版本用以下替代方案:
| 维度 | 替代数据源 | 分析要点 |
|------|----------|----------|
| 贡献频率 | PR 时间戳列表 | 统计活跃天数、相邻 PR 间隔、判断"持续贡献者"还是"间歇参与者" |
| 贡献产出 | `pr +list` 聚合 | PR 数、Issue 数,区分"代码贡献者"和"问题反馈者" |
| 活跃趋势 | PR 按日/周聚合 | 贡献量上升/稳定/下降,识别"上升期贡献者"和"逐渐淡出者" |
> ⚠️ **控制 API 调用**:贡献者 >15 人时,仅分析 PR 数最高的前 10 位。每人 1 次 `user +info` 调用(共 ≤10 次PR 数据已在 Step 2 全量获取。
### Step 4贡献者分级与分类
#### 4.1 活跃度分级
| 级别 | 判定标准 |
|------|----------|
| 🔥 **核心贡献者** | 最近 30 天有贡献 + 总贡献 PR ≥ 5或总贡献 PR > 10 |
| 🌟 **活跃贡献者** | 最近 60 天有贡献 + 总贡献 ≥ 3 |
| 🌱 **新兴贡献者** | 最近 90 天首次出现 + 贡献频率上升 |
| 💤 **休眠贡献者** | 最近 90 天无贡献 + 历史有贡献 |
> **年轻项目特殊处理**:项目历史 < 30 天时,放宽标准——所有活跃贡献者均可标记为核心贡献者,报告中注明"项目处于早期阶段,分级标准已放宽"。
#### 4.2 贡献类型分类
| 类型 | 判定 |
|------|------|
| **代码贡献者** | PR 数量 > Issue 数量 |
| **问题反馈者** | Issue 数量 > PR 数量 |
| **全能贡献者** | PR 和 Issue 数量均衡(差异 ≤ 1 |
### Step 5生成贡献者洞察报告
按下方输出模板生成报告,并根据数据可用性灵活调整章节。
---
## 输出模板
```markdown
# 👥 贡献者洞察报告:{{仓库名}}
> 分析时间:{{当前时间}}
> 仓库:{{full_name}}
> 总贡献者:{{contributor_users_count}} 人,本次分析:{{analyzed_count}} 人
---
## 一、团队概览
| 指标 | 数值 |
|------|------|
| 总贡献者 | {{contributor_users_count}} |
| 核心贡献者 | {{core_count}} |
| 活跃贡献者 | {{active_count}} |
| 新兴贡献者 | {{new_count}} |
| 休眠贡献者 | {{dormant_count}} |
| 近 30 天活跃率 | {{active_30d_rate}}% |
---
## 二、贡献者活跃度排行榜
| 排名 | 贡献者 | 级别 | 类型 | 活跃天数 | 总PR | 总Issue | 趋势 |
|------|--------|------|------|---------|------|---------|------|
| 1 | {{login}} | 🔥 | 代码 | {{d}} 天 | {{pr_count}} | {{issue_count}} | ↑ |
| ... | ... | ... | ... | ... | ... | ... | ... |
---
## 三、重点贡献者分析
> 仅展示核心/活跃贡献者。
### 🔥 {{login}}(核心贡献者)
| 维度 | 数据 | 说明 |
|------|------|------|
| 活跃天数 | {{d}} 天 | {{评价}} |
| 总 PR 数 | {{pr_count}} | |
| 总 Issue 数 | {{issue_count}} | |
| 贡献趋势 | {{trend_direction}} | {{trend_comment}} |
**PR 贡献明细**:(可选,数据充足时展示)
| PR# | 标题 | 日期 | 类型 |
|-----|------|------|------|
| ... | ... | ... | feat/fix/refactor |
---
## 四、团队健康度评估
### 健康度指标
| 指标 | 状态 | 说明 |
|------|------|------|
| 核心贡献者占比 | {{core_ratio}}% | {{core_comment}} |
| 新老比例 | {{new_old_ratio}} | {{new_old_comment}} |
| 贡献频率稳定性 | {{stability}} | {{stability_comment}} |
| 知识分散度 | {{bus_factor}} | {{bus_factor_comment}} |
### 风险提示
- ⚠️ **核心贡献者不足**(当 core_count < 3 时):仅 {{core_count}} 位核心贡献者存在单点依赖风险Bus Factor = {{core_count}})。
- ⚠️ **贡献者流失**(当 dormant_rate > 50% 时):超过一半的贡献者已不活跃,需要关注社区留存。
- ⚠️ **缺少新鲜血液**(当 new_count == 0 时):近期无新兴贡献者,建议通过 Good First Issue 等方式吸引新人。
- **项目处于早期阶段**(当项目历史 < 30 天时):贡献者分级标准已放宽,以上风险置信度有限。
- ✅ **团队健康**(当以上情况均不满足时):贡献者结构合理,团队运转良好。
---
## 五、社区建设建议
1. **激励核心贡献者**{{核心贡献者维护建议}}
2. **激活休眠贡献者**{{休眠贡献者召回建议}}
3. **吸引新贡献者**{{新贡献者吸引建议}}
4. **平衡贡献类型**{{贡献类型平衡建议}}
---
## 📋 数据来源与局限性
| 数据维度 | 来源 | 可靠性 |
|----------|------|--------|
| 贡献者数量 | `repo +info` | ✅ 可靠 |
| PR 贡献数据 | `pr +list` 全量 | ✅ 可靠 |
| Issue 数据 | `issue +list` | ✅ 可靠 |
| 用户信息 | `user +info` | ✅ 可靠 |
| 贡献热力图 | 不可用(命令未实现) | ❌ 缺失 |
| 统计信息 | 不可用(命令未实现) | ❌ 缺失 |
| 趋势数据 | 不可用(命令未实现) | ❌ 缺失 |
> **局限性**:本报告仅反映 GitLink 平台活动不包括其他平台GitHub、GitLab 等)的数据。
```
---
## 异常场景处理
| 场景 | 处理方式 |
|------|----------|
| `repo +contributors` 不可用(当前版本常态) | 从 `pr +list` 的 `author_login` 提取贡献者列表 |
| `user +heatmap` / `+stats` / `+trends` 不可用 | 从 PR 时间戳推算活跃天数PR 聚合得产出量,时间分布得趋势 |
| `pr +list` 返回空 | 标注"仓库暂无 PR 数据",仅展示 `repo +info` 基本信息 |
| `user +info` 返回空 | 标注"用户信息不可用",仅展示 PR 统计 |
| 贡献者 > 15 人 | 仅分析 PR 数最高的前 10 位,报告中注明"基于 Top 10 分析" |
| 项目历史 < 30 天 | 放宽分级标准,报告中注明"项目处于早期阶段" |
| `issue +list` 返回空 | Issue 数列为 0贡献类型统一标注"代码贡献者" |
---
## 注意事项
- ✅ **所有命令使用 `--format json`**,确保可解析
- ✅ **本 Skill 为纯只读分析**,不会修改任何仓库
- ✅ **Owner/repo 优先从 `git remote` 自动解析**,无 git 上下文时询问用户
- ⚠️ **核心数据来源为 `pr +list`**:当前版本 gitlink-cli 中 `user +heatmap/+stats/+trends` 不可用,分析主要依赖 PR 列表数据
- ⚠️ **`repo +contributors` 不可用**:贡献者列表从 PR 作者提取,可能与实际 `contributor_users_count` 有差异(后者包含未提 PR 的参与者)
- ⚠️ **数据仅反映 GitLink 平台活动**:不包括 GitHub 或其他平台的数据
- **参照样例**[`EXAMPLES.md`](EXAMPLES.md) 包含手动执行和 Agent 调用两种场景的完整样例,[`examples/jiangtx-gitlink-cli.md`](examples/jiangtx-gitlink-cli.md) 包含原始命令输出数据

View File

@ -0,0 +1,216 @@
# gitlink-issue-triageIssue 智能分拣)
**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。**
**CRITICAL — 本 Skill 为只读操作,不会修改任何 Issue。无需用户额外确认即可执行。**
**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`GitHub CLI操作 GitLink 资源。**
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。
---
## 功能概述
对仓库的开放 Issue 进行全量扫描和智能分类,输出结构化的分拣报告:
1. **类型分类** — 判断每个 Issue 是 Bug、功能请求、文档问题还是使用咨询
2. **紧急度评估** — 根据关键词和优先级字段标注紧急程度
3. **复杂度预估** — 根据描述详尽程度评估修复难度
4. **行动建议** — 给出具体处理建议(立即修复/需讨论/可关闭/适合作入门任务)
---
## 工作流Issue 全量分拣
### Step 1获取项目概览
```bash
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
```
提取 `issues_count` 了解 Issue 池总量,`default_branch` 确认主分支。
### Step 2获取全部开放 Issue
```bash
gitlink-cli issue +list --owner <owner> --repo <repo> --state open --format json
```
> ⚠️ **已知问题**`--state open` 过滤不准确,返回列表可能包含已关闭的 Issue。需在客户端按 `status_id` 二次过滤:保留 `status_id` = 1新增或 2正在解决排除 3已解决、5关闭。`status_id` = 0 纳入分析但标注"状态异常"。
如果返回数量 >20追加分页参数获取全部
```bash
gitlink-cli issue +list --owner <owner> --repo <repo> --state open --format json --page 2
```
### Step 3逐条深入分析
对过滤后的每条 Issue获取详情
```bash
gitlink-cli issue +view --owner <owner> --repo <repo> --number <project_issues_index> --format json
```
分析以下维度:
| 维度 | 关注字段 | 分析要点 |
|------|----------|----------|
| 类型 | `subject`, `description` | 标题和描述中的关键词 |
| 紧急度 | `priority`, `subject` | 优先级字段 + 标题紧急信号 |
| 复杂度 | `description` 长度 | 描述的详细程度、是否有复现步骤 |
| 活跃度 | `comment_journals_count`, `updated_at` | 讨论热度和最后活跃时间 |
| 分配状态 | `assigners` | 是否已有人负责 |
### Step 4分类规则
#### 4.1 类型分类type
| 类型 | 匹配规则 |
|------|----------|
| **bug** | 标题/描述含 `bug`、`错误`、`失败`、`崩溃`、`异常`、`修复`、`fix`、`修复`、`报错`、`不工作`、`问题`(上下文为故障时) |
| **feature** | 标题/描述含 `feature`、`新增`、`添加`、`希望`、`建议`、`需要`、`支持`、`实现`,且非故障描述 |
| **docs** | 标题/描述含 `文档`、`doc`、`README`、`说明`、`教程`、`注释` |
| **question** | 标题/描述含 `如何`、`怎么`、`是否`、`能不能`、`请问`、`为什么`,且以问号结尾或明显为咨询语气 |
| **refactor** | 标题/描述含 `重构`、`refactor`、`优化结构`、`代码清理`、`技术债` |
| **ci** | 标题/描述含 `CI`、`CD`、`构建`、`部署`、`pipeline`、`自动化`、`测试环境` |
| **meta** | 维护者创建的元讨论帖、反馈收集帖、公告,无具体技术任务指向 |
| **other** | 不匹配以上任何类型时的兜底分类 |
#### 4.2 紧急度评估urgency
| 级别 | 判定条件 |
|------|----------|
| **urgent** | 标题含 `紧急`、`urgent`、`hotfix`、`生产`、`线上`、`崩溃`;或 `priority.name` = "紧急" |
| **high** | `priority.name` = "高";或标题含 `严重`、`阻塞`、`关键` |
| **normal** | 默认级别;`priority.name` = "正常" 或无优先级 |
| **low** | `priority.name` = "低";或标题含 `优化`、`nice to have`、`小建议` |
#### 4.3 复杂度预估complexity
| 级别 | 判定条件 |
|------|----------|
| **easy** | 描述简洁明确,有清晰复现步骤或单一功能点;`description` < 300 字且范围明确 |
| **medium** | 涉及多个文件/模块,需要一定背景了解;`description` 300~800 字,或虽有描述但需推断 |
| **hard** | 涉及架构变更、新子系统、跨模块重构;`description` > 800 字或非常模糊 |
> **特殊情况**`description` 仅含图片附件链接而无可读文字 → 视为"描述缺失",复杂度标记为 hard因无法评估建议标记为 discuss。
#### 4.4 行动建议action
| 建议 | 判定条件 |
|------|----------|
| **fix-now** | bug + urgent/high |
| **investigate** | bug + normal/low需先确认复现 |
| **implement** | feature + 描述清晰 + 范围明确 |
| **discuss** | 描述模糊、需求不清、或 question 类型 |
| **close-candidate** | 超过 90 天无更新、无评论、无分配 |
| **good-first-issue** | complexity=easy + 无人分配 + 范围明确 |
### Step 5生成分拣报告
将所有分析结果组织输出。
---
## 输出模板
```markdown
# 📊 {{仓库名}} Issue 分拣报告
> 分析时间:{{当前时间}}
> Issue 总数:{{total}},开放:{{open_count}},本次分析:{{analyzed_count}} 条
---
## 总览
| 指标 | 数量 |
|------|------|
| Bug | {{bug_count}} |
| 功能请求 | {{feature_count}} |
| 文档 | {{docs_count}} |
| 咨询 | {{question_count}} |
| 元讨论 | {{meta_count}} |
| 其他 | {{other_count}} |
| **需立即处理** | {{urgent_count}} |
| **适合入门** | {{good_first_issue_count}} |
---
## 🔴 需立即处理
> 如本段为空,输出:*当前无紧急 Issue状态健康。*
| # | 标题 | 类型 | 紧急度 | 复杂度 | 建议 | 备注 |
|---|------|------|--------|--------|------|------|
| {{number}} | {{subject}} | bug | urgent | medium | fix-now | |
| ... | ... | ... | ... | ... | ... | ... |
## 🟡 建议近期处理
> 如本段为空,输出:*当前无高优先级 Issue。*
| # | 标题 | 类型 | 紧急度 | 复杂度 | 建议 | 备注 |
|---|------|------|--------|--------|------|------|
| ... | ... | bug/feature | high/normal | easy/medium | investigate/implement | |
## 🟢 可延迟 / 需讨论
> 如本段为空,输出:*所有 Issue 均已明确,无需额外讨论。*
| # | 标题 | 类型 | 紧急度 | 复杂度 | 建议 | 备注 |
|---|------|------|--------|--------|------|------|
| ... | ... | question/feature | normal/low | medium/hard | discuss | |
## ⭐ 适合入门Good First Issue
> 如本段为空,输出:*暂无完全符合条件的入门 Issue。建议在后续工作中拆分出简单子任务。*
| # | 标题 | 类型 | 复杂度 | 推荐理由 |
|---|------|------|--------|----------|
| {{number}} | {{subject}} | bug/docs | easy | 范围明确,单文件修改 |
| ... | ... | ... | ... | ... |
## ⚠️ 候选关闭90+ 天无活动)
> 如本段为空,输出:*无长期不活跃的 Issue。*
| # | 标题 | 最后更新 | 建议 |
|---|------|----------|------|
| {{number}} | {{subject}} | {{updated_at}} | 评论询问是否仍需要,如无回应可关闭 |
---
## 📋 维护建议
1. **立即行动**{{urgent_count}} 个紧急 Issue 需要优先处理
2. **本周目标**:建议处理 {{suggested_this_week}} 个 Issuesuggested_this_week = 建议近期处理段中的 Issue 数量,即 bug+normal/high + feature+清晰描述 的总数)
3. **社区引导**{{good_first_issue_count}} 个 Issue 适合标记为 good first issue吸引新贡献者
4. **清理计划**{{close_candidate_count}} 个 Issue 长期无活动,建议批量确认后关闭
5. {{#if no_tags}}本仓库未使用 Issue 标签系统建议建立标签体系bug/feature/docs/question/meta/help-wanted/good-first-issue以提升管理效率{{/if}}
6. {{#if status_anomalies}}本批次有 {{status_anomaly_count}} 个 Issue 状态异常status_id=0建议在平台上手动确认{{/if}}
```
---
## 异常场景处理
| 场景 | 处理方式 |
|------|----------|
| 无开放 Issue | 输出 `repo +info` 概览后,恭喜维护者"Issue 池已清空" |
| Issue 数量 >50 | 优先分析最近 30 天更新的 Issue其余标记为"待分批处理" |
| 全部 Issue 无标签/无优先级 | 分类完全依赖标题和描述关键词分析,并在报告末尾建议建立标签体系 |
| `description` 为空或仅含图片/附件链接 | 标注"描述缺失",类型仅根据标题判断,复杂度标为 hard建议标记为 discuss |
| `status_id` = 0未知 | 纳入分析但标注"状态异常" |
---
## 注意事项
- ✅ **所有命令使用 `--format json`**,确保可解析
- ✅ **`issue +view` 使用 `--number`(网页编号)**,非数据库 ID
- ✅ **本 Skill 为纯只读分析**,不会修改任何 Issue
- ✅ **Owner/repo 优先从 `git remote` 自动解析**,无 git 上下文时询问用户
- ⚠️ **`issue +list --state open` 过滤不准确**,必须客户端按 `status_id` 二次过滤
- ⚠️ **分类规则是启发式的**AI 应根据实际内容做判断,不要机械匹配关键词
- ⚠️ **Issue 数量多时分批处理**,超过 50 条建议先按更新时间排序,优先分析最近活跃的

View File

@ -0,0 +1,246 @@
# gitlink-research-tracker科研热点追踪
**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。**
**CRITICAL — 本 Skill 为只读操作,不会修改任何仓库。无需用户额外确认即可执行。**
**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`GitHub CLI操作 GitLink 资源。**
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。
---
## 功能概述
面向科研场景的技术调研工具,帮助研究者快速了解 GitLink 平台上的技术格局:
1. **多关键词搜索** — 将研究主题拆解为多个关键词,全面覆盖相关项目
2. **项目深度评估** — 从活跃度、社区规模、代码产出等维度评估项目健康度
3. **横向对比** — 对比同类项目的核心指标,识别领先者和潜力项目
4. **趋势洞察** — 基于更新时间、贡献者增长、版本发布频率等推断技术趋势
5. **调研报告** — 生成结构化的技术调研简报
---
## 工作流:技术调研全流程
### Step 1理解研究主题拆解搜索关键词
根据用户的研究主题,拆解 3~5 个搜索关键词:
| 研究主题示例 | 拆解关键词 |
|-------------|-----------|
| "AI Agent 工具" | `agent`, `AI`, `LLM`, `智能体`, `copilot` |
| "DevOps 流水线" | `devops`, `pipeline`, `CI/CD`, `自动化部署`, `容器` |
| "开源合规" | `license`, `compliance`, `合规`, `sbom`, `供应链安全` |
| "微服务框架" | `microservice`, `微服务`, `rpc`, `服务网格`, `cloud native` |
> **原则**:关键词应覆盖中英文、缩写全称、技术术语和行业叫法。每个关键词独立搜索。
### Step 2多关键词搜索
对每个关键词执行搜索:
```bash
gitlink-cli search +repos -k <关键词> --format json
```
对每个搜索结果,提取:
| SKILL 中用到的概念 | 实际字段来源 | 说明 |
|-------------------|-------------|------|
| owner/repo 标识 | `author.login` + `/` + `identifier` | 搜索结果**没有** `full_name`,需手动拼接。`identifier` 是仓库的唯一标识符 |
| 项目描述 | `description` | 直接可用 |
| 关注度 | `praises_count` | 搜索结果中叫 `praises_count`**不是** `stars`。`watchers_count` 仅在 `repo +info` 中返回 |
| Fork 数 | `forked_count` | 搜索结果中叫 `forked_count`**不是** `forks_count` |
| 编程语言 | `language.name` | `language` 是嵌套对象 `{id, name}`,需取 `.name`。可能为 `null` |
| 更新时间 | `last_update_time`Unix 时间戳)或 `full_last_update_time`ISO 8601 字符串) | 搜索结果中**没有** `updated_at` |
| 是否镜像 | `mirror` | 仅在 `repo +info` 返回。GitLink 上大量仓库是 GitHub 镜像,需特别标注 |
**去重规则**:用 `author.login/identifier` 作为唯一标识。同一仓库出现在多个关键词结果中时,只保留一次,标注匹配了哪些关键词。
**数量控制**:每个关键词保留前 8 个结果。合并去重后总数控制在 20 个以内。超出时优先保留匹配多关键词的和 `last_update_time` 最近的。
### Step 3重点项目深度评估
对合并去重后的每个项目,获取详细指标:
```bash
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
```
从返回数据中提取核心评估维度:
| 维度 | 字段 | 评估标准 |
|------|------|----------|
| **社区活跃度** | `contributor_users_count` | >20 大社区5~20 中等,<5 小团队 |
| **关注度** | `watchers_count`, `praises_count` | 反映项目知名度 |
| **研发节奏** | `version_releases_count` | >20 快速迭代5~20 正常,<5 慢速 |
| **代码规模** | `size` | 粗略判断项目复杂度 |
| **开放性** | `forked_count` | fork 数反映二次开发热度 |
| **PR 活跃度** | `pull_requests_count` | 反映代码贡献频率 |
可选补充(如有需要):
```bash
# 查看最近 Issue 活跃度
gitlink-cli issue +list --owner <owner> --repo <repo> --state open --format json
# 查看最近 Release 情况
gitlink-cli release +list --owner <owner> --repo <repo> --format json
```
> ⚠️ **控制分析数量**:深度评估仅对最有价值的 5~8 个项目执行优先匹配多关键词、watchers 多、updated_at 最近的项目),避免过多 API 调用。
### Step 4横向对比与趋势分析
#### 4.1 项目分类与镜像识别
在评分之前,先通过 `repo +info` 的 `mirror` 字段区分项目类型:
| 类型 | 判定 | 处理 |
|------|------|------|
| **镜像仓库** | `mirror: true` | 标注 `[镜像]`。GitLink 上的 `contributor_users_count`/`watchers_count` 等指标均为 0不代表真实社区活跃度。评分仅作参考 |
| **原创仓库** | `mirror: false` 且 `forked_from_project_id: null` | 正常评分 |
| **Fork 仓库** | `forked_from_project_id` 非 null | 标注 `[Fork]`,评分反映的是 Fork 后的独立开发情况 |
#### 4.2 项目成熟度评分
对每个深度评估的项目,按以下标准打分(满分 25
| 维度 | 权重 | 评分标准 |
|------|------|----------|
| 社区规模 | 5 | contributor_users_count: >20=5, >10=4, >5=3, >2=2, ≤2=1 |
| 关注度 | 5 | repo +info 的 watchers_count: >30=5, >15=4, >8=3, >3=2, ≤3=1 |
| 研发节奏 | 5 | version_releases_count: >10=5, >5=4, >1=3, 0=2。**镜像仓库此项固定给 1**(镜像通常不通过 GitLink 发版)。注意 GitLink 平台 Release 功能使用率低,即使原创仓库 release=0 也建议给 2 而非 1 |
| 开发活跃 | 5 | 最近 30 天有更新=5, 60 天=4, 90 天=3, 180 天=2, >180 天=1。基于 `repo +info` 的更新时间或搜索结果中的 `last_update_time` |
| 开放性 | 5 | forked_count: >30=5, >15=4, >8=3, >3=2, ≤3=1 |
> **镜像修正**:镜像仓库的社区规模、关注度、开放性三项在 GitLink 上均为 0应标注"数据为 GitLink 平台内数据不代表项目在原始平台GitHub的真实影响力",不参与排名比较。
#### 4.3 技术趋势推断
- **增长信号**:近期频繁 Release + contributor 增长 + fork 增长 → 技术热点上升期
- **成熟信号**:大量 watcher + 稳定 Release 节奏 + 大社区 → 技术趋于成熟
- **衰退信号**:超过 180 天无更新 + 少量 contributor + 无新 Release → 可能已不活跃
- **新兴信号**:小社区 + 快速迭代 + 最新更新时间近 → 可能是新兴项目
### Step 5生成技术调研报告
---
## 输出模板
```markdown
# 🔬 技术调研报告:{{研究主题}}
> 调研时间:{{当前时间}}
> 搜索关键词:{{keyword_list}}
> 搜索命中:{{total_hits}} 个仓库,去重后 {{unique_count}} 个,深度分析 {{deep_analysis_count}} 个
---
## 一、技术格局概览
| 指标 | 数值 |
|------|------|
| 相关项目总数 | {{unique_count}} |
| 主要编程语言 | {{top_languages}} |
| 平均社区规模 | {{avg_contributors}} 人 |
| 近 30 天活跃项目 | {{active_30d_count}}{{active_30d_pct}}% |
| 高成熟度项目≥20分 | {{high_maturity_count}} |
---
## 二、项目成熟度排行榜
| 排名 | 项目 | 类型 | 评分 | 语言 | Watch | 贡献者 | Release | Fork | 关键词匹配 |
|------|------|------|------|------|-------|--------|---------|------|------------|
| 1 | {{full_name}} {{#if mirror}}[镜像]{{/if}} | {{原创/镜像/Fork}} | {{score}}/25 | {{language}} | {{watchers}} | {{contributors}} | {{releases}} | {{forks}} | {{matched_keywords}} |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
---
## 三、重点项
> 仅展示评分 ≥15 或匹配 3+ 关键词的高潜力项目。
### 🥇 {{项目名}}{{score}}/25
| 维度 | 评分 | 说明 |
|------|------|------|
| 社区规模 | {{community_score}}/5 | {{contributor_users_count}} 位贡献者 |
| 关注度 | {{popularity_score}}/5 | {{watchers_count}} watch, {{praises_count}} star |
| 研发节奏 | {{release_score}}/5 | {{version_releases_count}} 个版本发布 |
| 开发活跃 | {{activity_score}}/5 | 最后更新于 {{last_update}} |
| 开放性 | {{openness_score}}/5 | {{forked_count}} 次 fork |
**亮点**{{一句话总结项目最大优势}}
**关注点**{{一句话指出需要关注的风险或不足}}
---
### 🥈 {{项目名}}{{score}}/25
(同上格式)
---
## 四、技术趋势洞察
1. **热点方向**{{当前最热的技术方向,基于项目分布推断}}
2. **新兴项目**{{列出 1~3 个"新兴信号"明显的项目}}
3. **成熟生态**{{列出 1~2 个"成熟信号"明显的项目,适合作为技术选型参考}}
4. **风险提示**{{列出 1~2 个"衰退信号"项目或值得关注的生态空白}}
---
## 五、调研建议
### 技术选型推荐
| 场景 | 推荐项目 | 理由 |
|------|----------|------|
| 生产环境使用 | {{最成熟的项目}} | 社区大、更新稳定、文档完善 |
| 学习入门 | {{最简单的项目}} | 代码量小、贡献门槛低 |
| 前沿探索 | {{最新兴的项目}} | 技术新颖、迭代快速 |
### 研究选题建议
- {{基于当前生态,建议 2~3 个可深入研究的选题}}
- {{指出 1~2 个生态空白,可能是创新机会}}
---
## 六、数据来源
所有数据通过 `gitlink-cli` 从 GitLink 平台实时获取,每个项目均已通过 `repo +info` 验证。
```
---
## 异常场景处理
| 场景 | 处理方式 |
|------|----------|
| 关键词无搜索结果 | 尝试近义词或更宽泛的关键词重试,仍无结果则标注"该方向暂无相关项目" |
| 搜索返回大量结果(>50 | `search +repos` 无分页参数,实际返回约 20 条/关键词。合并后按 `praises_count` 降序取前 20 |
| 某项目 `repo +info` 返回 404 | 该项目可能为私有或已删除,从列表中移除 |
| `repo +info` 网络超时/TLS 错误 | 等待 5 秒后重试一次。仍失败则标注"网络请求失败",跳过该项目继续分析其余 |
| 大量搜索结果来自镜像仓库 | 优先分析 `mirror: false` 的原创项目。镜像项目保留但标注,评分仅作参考 |
| 所有项目评分均 <15 | 说明该领域尚未形成成熟生态,调整报告语气为"早期探索阶段" |
| 用户未提供具体关键词 | 引导用户明确研究主题,提供几个示例关键词供选择 |
| `language` 字段为 `null` | 标注为"未知" |
---
## 注意事项
- ✅ **所有命令使用 `--format json`**,确保可解析
- ✅ **本 Skill 为纯只读分析**,不会修改任何仓库
- ✅ **搜索关键词建议中英文各覆盖**,提高命中率
- ✅ **深度评估控制在 5~8 个项目**,避免调用过多 API
- ⚠️ **`search +repos` 和 `repo +info` 字段名不同**:搜索结果用 `praises_count`/`forked_count`/`author.login+identifier``repo +info` 才有 `watchers_count`/`full_name`/`mirror`。详见 Step 2 字段映射表
- ⚠️ **`repo +info` 并发请求可能触发 TLS 超时**,失败时等 5 秒重试一次,不要放弃
- ⚠️ **GitLink 平台镜像仓库比例高**,镜像仓库的社区数据为 0不代表项目真实影响力。在报告中标注 `[镜像]` 并单独说明
- ⚠️ **GitLink Release 功能使用率低**,大部分项目 `version_releases_count`=0。评分时 Release 维度降低权重预期0 个 Release 给 2 分(而非 1 分)
- ⚠️ **搜索结果无分页参数**,每次返回约 20 条。关键词超过 5 个时需手动截断合并结果
- ⚠️ **本 Skill 场景适配 GitLink 平台**GitLink 以国内开发者和企业项目为主,搜索结果可能偏向中文技术生态,且镜像项目较多

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitLink Skills</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-50 min-h-screen">
<nav class="bg-white shadow-sm border-b">
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
<a href="/" class="text-xl font-bold text-blue-600">🔧 GitLink Skills</a>
<div class="text-sm text-gray-500">科技赋能 · 开源创新</div>
</div>
</nav>
<main class="max-w-6xl mx-auto px-4 py-8">
{% block content %}{% endblock %}
</main>
<footer class="text-center text-gray-400 text-sm py-8">
GitLink Skills Web Service &mdash; 基于 Flask + DeepSeek API + gitlink-cli
</footer>
</body>
</html>

View File

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block content %}
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800 mb-4">GitLink Skills 智能分析服务</h1>
<p class="text-lg text-gray-500 max-w-2xl mx-auto">
基于 DeepSeek API 和 gitlink-cli通过 AI Agent 自动搜索、分析 GitLink 平台数据
</p>
</div>
<!-- 工具区 -->
<h2 class="text-2xl font-bold text-gray-800 mb-4">🔧 工具</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-12">
{% for key, skill in skills.items() %}
{% if not key.startswith("lab-") %}
<a href="/skills/{{ url_map.get(key, key) }}" class="block p-5 bg-white rounded-xl shadow-sm hover:shadow-md transition border hover:border-blue-200">
<h3 class="text-lg font-semibold text-gray-800 mb-1">{{ skill.title }}</h3>
<div class="mt-2 text-blue-600 text-sm font-medium">开始使用 →</div>
</a>
{% endif %}
{% endfor %}
</div>
<!-- 科研实验室: 流程图布局 -->
<h2 class="text-2xl font-bold text-green-700 mb-4">🔬 科研实验室</h2>
<p class="text-gray-500 text-sm mb-6">多技能串联合成的科研辅助工作流,覆盖完整科研项目生命周期</p>
<div class="flex flex-col md:flex-row items-center justify-center gap-0 max-w-5xl mx-auto">
{% set flow = [
("lab-hotspot", "🔍 热点追踪", "搜项目 → 深挖 → 提取Fork关系 → 生成知识图谱"),
("lab-insight", "📊 项目洞悉", "仓库信息+贡献者+PR/Issue → 全息分析报告"),
("lab-compliance", "✅ 合规检查", "检查License/CI/Release/文档 → 合规评分与改进建议"),
("lab-match", "🤝 协作匹配", "分析Issue+社区健康度 → 评估新手友好度与入门方向"),
("lab-track", "📈 进度跟踪", "批量巡检多仓库 → 健康/警告/危险三级告警"),
] %}
{% for key, title, desc in flow %}
<div class="relative group w-36">
<a href="/skills/{{ key }}" class="block">
<div class="bg-green-50 border-2 border-green-300 rounded-xl p-4 text-center hover:bg-green-100 hover:border-green-500 hover:shadow-lg transition-all duration-200">
<div class="text-sm font-bold text-green-800">{{ title }}</div>
</div>
</a>
<div class="absolute z-10 invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-opacity duration-200 bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-800 text-white text-xs rounded-lg shadow-lg w-48 pointer-events-none">
{{ desc }}
<div class="absolute top-full left-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-800"></div>
</div>
</div>
{% if not loop.last %}
<div class="flex items-center justify-center w-8 h-8">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</div>
{% endif %}
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,168 @@
{% extends "base.html" %}
{% block content %}
<div class="max-w-4xl mx-auto">
<a href="/skills/" class="text-blue-600 hover:underline text-sm mb-4 inline-block">&larr; 返回首页</a>
<h1 class="text-3xl font-bold text-gray-800 mb-2">{{ info.title }}</h1>
{% if skill_name.startswith("lab-") and info.intro %}
<div class="bg-green-50 border border-green-200 rounded-xl p-4 mb-6 text-sm text-green-800">
{{ info.intro }}
</div>
{% endif %}
<!-- 功能介绍 + 预期输出 -->
<div class="space-y-2 mb-6">
<details class="bg-white border border-gray-200 rounded-xl overflow-hidden">
<summary class="px-4 py-3 cursor-pointer font-medium text-gray-700 hover:bg-gray-50 transition flex items-center gap-2">
<span class="text-blue-500">📖</span> 功能介绍
</summary>
<div class="px-4 py-3 text-sm text-gray-600 border-t border-gray-100">
{% if info.intro %}{{ info.intro }}{% endif %}
</div>
</details>
<details class="bg-white border border-gray-200 rounded-xl overflow-hidden">
<summary class="px-4 py-3 cursor-pointer font-medium text-gray-700 hover:bg-gray-50 transition flex items-center gap-2">
<span class="text-blue-500">📋</span> 预期输出格式与内容
</summary>
<div class="px-4 py-3 text-sm text-gray-600 border-t border-gray-100">
{% if info.output_desc %}{{ info.output_desc|replace('\n', '<br>')|safe }}{% else %}
<p>生成中文分析报告,包含:总览、详细分析、排行榜、建议等。</p>
{% endif %}
</div>
</details>
</div>
<!-- 输入表单 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-8">
<label class="block font-medium text-gray-700 mb-2">{{ info.prompt_label }}</label>
<input id="user-input" type="text"
class="w-full border rounded-lg px-4 py-3 text-gray-700 focus:ring-2 focus:ring-blue-300 focus:border-blue-400 outline-none"
placeholder="{{ info.prompt_placeholder }}">
<p id="hint-text" class="text-sm text-gray-400 mt-2"></p>
<button onclick="runSkill()" id="run-btn"
class="mt-4 bg-blue-600 hover:bg-blue-700 text-white font-medium px-6 py-3 rounded-lg transition disabled:opacity-50">
🚀 开始分析
</button>
</div>
<!-- 进度 -->
<div id="progress" class="hidden mb-6">
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center gap-3 mb-4">
<div class="animate-spin w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full"></div>
<span class="text-gray-600 font-medium">AI 正在分析中...</span>
</div>
<div id="log" class="text-sm text-gray-500 space-y-1 font-mono max-h-48 overflow-y-auto"></div>
</div>
</div>
<!-- 报告输出 -->
<div id="report" class="hidden">
<div class="bg-white rounded-xl shadow-sm p-6 overflow-auto">
<div id="report-content" class="markdown-body"></div>
</div>
<div class="mt-4 flex gap-3">
<button onclick="downloadTxt()" class="text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg transition font-medium">
📥 下载 TXT
</button>
</div>
</div>
<!-- 错误 -->
<div id="error" class="hidden bg-red-50 border border-red-200 rounded-xl p-6 text-red-700"></div>
</div>
<script>
const skillName = "{{ skill_name }}";
const hints = {
"research-tracker": "例如微服务框架、AI Agent、DevOps 流水线、大模型、数据可视化",
"contributor-insight": "例如ci4s/ci4sManagement-cloud, jiangtx/gitlink-cli",
"issue-triage": "例如Gitlink/gitlink-cli, ci4s/ci4sManagement-cloud",
"ci-health": "例如jiangtx/gitlink-cli, Gitlink/gitlink-cli",
"lab-hotspot": "例如大模型、AI Agent、微服务、DevOps、数据可视化",
"lab-insight": "例如ci4s/ci4sManagement-cloud, Gitlink/gitlink-cli",
"lab-compliance": "例如ci4s/ci4sManagement-cloud, Gitlink/gitlink-cli",
"lab-match": "例如ci4s/ci4sManagement-cloud, Gitlink/gitlink-cli",
"lab-track": "例如ci4s/ci4sManagement-cloud, Gitlink/gitlink-cli",
};
document.getElementById('hint-text').textContent = '💡 ' + (hints[skillName] || '');
function runSkill() {
const input = document.getElementById('user-input').value.trim();
if (!input) { alert('请输入参数'); return; }
document.getElementById('progress').classList.remove('hidden');
document.getElementById('report').classList.add('hidden');
document.getElementById('error').classList.add('hidden');
document.getElementById('run-btn').disabled = true;
document.getElementById('log').innerHTML = '<div>⏳ 提交中...</div>';
fetch('/api/run', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `skill=${encodeURIComponent(skillName)}&input=${encodeURIComponent(input)}`
})
.then(r => r.json())
.then(data => {
if (data.error) {
document.getElementById('error').classList.remove('hidden');
document.getElementById('error').textContent = '❌ ' + data.error;
return;
}
document.getElementById('log').innerHTML = '<div class="text-green-600">✅ 分析完成</div>';
// Render Markdown
const reportDiv = document.getElementById('report-content');
reportDiv.innerHTML = marked.parse(data.report || '');
document.getElementById('report').classList.remove('hidden');
// Render Mermaid diagrams (find code.language-mermaid)
document.querySelectorAll('code.language-mermaid').forEach((code) => {
const pre = code.parentElement;
const div = document.createElement('div');
div.className = 'mermaid';
div.textContent = code.textContent;
pre.parentElement.replaceChild(div, pre);
});
if (typeof mermaid !== 'undefined') {
mermaid.run({ nodes: document.querySelectorAll('.mermaid') });
}
})
.catch(err => {
document.getElementById('error').classList.remove('hidden');
document.getElementById('error').textContent = '❌ 请求失败: ' + err.message;
})
.finally(() => {
document.getElementById('progress').classList.add('hidden');
document.getElementById('run-btn').disabled = false;
});
}
function downloadTxt() {
const text = document.getElementById('report-content').textContent;
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'report-' + new Date().toISOString().slice(0,10) + '.txt';
a.click();
URL.revokeObjectURL(a.href);
}
</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: false, theme: 'default' });</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown.min.css">
<style>
#report-content.markdown-body { padding: 0; }
#report-content.markdown-body table { display: table; width: 100%; }
#report-content.markdown-body pre { background: #f6f8fa; border-radius: 6px; position: relative; }
#report-content.markdown-body code { font-size: 13px; }
#report-content.markdown-body img { max-width: 100%; }
/* Mermaid SVG should be visible */
#report-content.markdown-body .mermaid svg { max-width: 100%; height: auto; }
</style>
{% endblock %}

View File

@ -0,0 +1,17 @@
[
{
"list": [
{
"contribution_perc": "100.00%",
"contributions": 1,
"email": "jtx0909@qq.com",
"id": "148911",
"image_url": "system/lets/letter_avatars/2/J/67_157_94/120.png",
"login": "jiangtx",
"name": "jiangtx",
"type": "User"
}
],
"total_count": 1
}
]

View File

@ -41,6 +41,9 @@ func DeleteToken() error {
// File-based fallback
func credentialPath() string {
if dir := os.Getenv("GITLINK_CONFIG_DIR"); dir != "" {
return filepath.Join(dir, "credentials")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "gitlink-cli", "credentials")
}

View File

@ -12,7 +12,7 @@ import (
func tempHome(t *testing.T) string {
t.Helper()
dir := t.TempDir()
t.Setenv("HOME", dir)
t.Setenv("GITLINK_CONFIG_DIR", filepath.Join(dir, ".config", "gitlink-cli"))
return dir
}
@ -57,18 +57,18 @@ func TestStoreLoadDeleteTokenFile(t *testing.T) {
func TestCredentialPath(t *testing.T) {
tempHome(t)
got := credentialPath()
expected := filepath.Join(os.Getenv("HOME"), ".config", "gitlink-cli", "credentials")
expected := filepath.Join(os.Getenv("GITLINK_CONFIG_DIR"), "credentials")
if got != expected {
t.Fatalf("credentialPath = %q, want %q", got, expected)
}
}
func TestStoreTokenFileCreatesDir(t *testing.T) {
home := tempHome(t)
_ = tempHome(t)
_ = deleteTokenFile()
// Config dir shouldn't exist yet
credDir := filepath.Join(home, ".config", "gitlink-cli")
credDir := os.Getenv("GITLINK_CONFIG_DIR")
os.RemoveAll(credDir)
if err := storeTokenFile("new-token"); err != nil {
@ -114,14 +114,14 @@ func TestStoreLoadTokenFileEmpty(t *testing.T) {
func TestStoreTokenFallback(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
home := tempHome(t)
_ = tempHome(t)
_ = deleteTokenFile()
if err := StoreToken("keychain-fallback-token"); err != nil {
t.Fatalf("StoreToken error: %v", err)
}
data, err := os.ReadFile(filepath.Join(home, ".config", "gitlink-cli", "credentials"))
data, err := os.ReadFile(filepath.Join(os.Getenv("GITLINK_CONFIG_DIR"), "credentials"))
if err != nil {
t.Fatalf("read error: %v", err)
}
@ -132,10 +132,10 @@ func TestStoreTokenFallback(t *testing.T) {
func TestDeleteTokenFallback(t *testing.T) {
keyring.MockInitWithError(errors.New("keychain unavailable"))
home := tempHome(t)
_ = tempHome(t)
_ = deleteTokenFile()
p := filepath.Join(home, ".config", "gitlink-cli", "credentials")
p := filepath.Join(os.Getenv("GITLINK_CONFIG_DIR"), "credentials")
os.MkdirAll(filepath.Dir(p), 0700)
os.WriteFile(p, []byte("delete-me"), 0600)

View File

@ -1,6 +1,7 @@
package auth
import (
"fmt"
"net/http"
"os"
"strings"
@ -18,10 +19,8 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
token, _ = LoadToken()
}
if token != "" {
if strings.HasPrefix(token, "cookie:") {
if cookiePart, ok := strings.CutPrefix(token, "cookie:"); ok {
// Cookie-based auth: token stored as "cookie:<name>=<value>"
cookiePart := strings.TrimPrefix(token, "cookie:")
// Append to existing cookies
existing := req.Header.Get("Cookie")
if existing != "" {
req.Header.Set("Cookie", existing+"; "+cookiePart)
@ -47,6 +46,102 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
return base.RoundTrip(req)
}
// IsCookieAuth returns true if cookie-based auth is available.
// It checks the effective token (env > storage). If the env var is set
// but is not a cookie, it still checks the stored token as a fallback,
// since wiki endpoints can use the stored cookie directly.
func IsCookieAuth() bool {
token := os.Getenv("GITLINK_TOKEN")
if token != "" {
if strings.HasPrefix(token, "cookie:") {
return true
}
// Env var is set but not a cookie; check storage as fallback
stored, _ := LoadToken()
return strings.HasPrefix(stored, "cookie:")
}
token, _ = LoadToken()
return strings.HasPrefix(token, "cookie:")
}
// CookieTransport is an HTTP transport that uses the stored cookie token,
// bypassing any non-cookie GITLINK_TOKEN environment variable.
// Required for endpoints (like Wiki) that only accept session cookie auth.
type CookieTransport struct {
Base http.RoundTripper
}
func (t *CookieTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Use cookie token from env var if it's a cookie, otherwise from storage.
// This ensures wiki API calls always use cookie auth, even when GITLINK_TOKEN
// is set to a non-cookie access token.
token := os.Getenv("GITLINK_TOKEN")
if !strings.HasPrefix(token, "cookie:") {
token, _ = LoadToken()
}
if token != "" {
if cookiePart, ok := strings.CutPrefix(token, "cookie:"); ok {
existing := req.Header.Get("Cookie")
if existing != "" {
req.Header.Set("Cookie", existing+"; "+cookiePart)
} else {
req.Header.Set("Cookie", cookiePart)
}
}
}
if req.Body != nil && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
base := t.Base
if base == nil {
base = http.DefaultTransport
}
return base.RoundTrip(req)
}
// NewCookieHTTPClient creates an HTTP client that always uses the stored
// cookie token, ignoring the GITLINK_TOKEN environment variable.
func NewCookieHTTPClient() *http.Client {
return &http.Client{
Transport: &CookieTransport{},
}
}
// AuthDiagnostic returns a human-readable summary of the current auth state
// for debugging purposes.
func AuthDiagnostic() string {
envToken := os.Getenv("GITLINK_TOKEN")
storedToken, storeErr := LoadToken()
parts := []string{}
if envToken != "" {
prefix := envToken
if len(prefix) > 20 {
prefix = prefix[:20] + "..."
}
parts = append(parts, fmt.Sprintf("GITLINK_TOKEN env 已设置 (前缀: %q)", prefix))
} else {
parts = append(parts, "GITLINK_TOKEN env 未设置")
}
if storeErr != nil {
parts = append(parts, fmt.Sprintf("存储读取失败: %v", storeErr))
} else if storedToken != "" {
prefix := storedToken
if len(prefix) > 30 {
prefix = prefix[:30] + "..."
}
isCookie := strings.HasPrefix(storedToken, "cookie:")
parts = append(parts, fmt.Sprintf("存储 token (前缀: %q, cookie=%v)", prefix, isCookie))
} else {
parts = append(parts, "存储 token 为空")
}
return strings.Join(parts, "; ")
}
func NewHTTPClient() *http.Client {
return &http.Client{
Transport: &Transport{},

View File

@ -0,0 +1,295 @@
package capability
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/gitlink-org/gitlink-cli/internal/client"
)
// Status represents the availability status of a backend API domain.
type Status int
const (
StatusUnknown Status = iota // not probed yet
StatusAvailable // backend API responds with JSON
StatusUnavailable // backend API returns HTML or 404
StatusError // probe itself failed (network error, etc.)
)
func (s Status) String() string {
switch s {
case StatusAvailable:
return "available"
case StatusUnavailable:
return "unavailable"
case StatusError:
return "error"
default:
return "unknown"
}
}
// Emoji returns a single-character status indicator for help text.
func (s Status) Emoji() string {
switch s {
case StatusAvailable:
return "✓"
case StatusUnavailable:
return "⚠"
case StatusError:
return "✗"
default:
return "?"
}
}
// DomainStatus records the probe result for a single domain.
type DomainStatus struct {
Domain string `json:"domain"`
Status Status `json:"status"`
Message string `json:"message,omitempty"`
LastChecked time.Time `json:"last_checked"`
}
// CanaryProbe defines a lightweight endpoint used to test domain availability.
type CanaryProbe struct {
Method string // HTTP method (usually GET)
Path string // API path; use {owner} and {repo} as placeholders
NeedsRepo bool // whether the probe requires owner/repo context
}
// canaryEndpoints maps each domain to its probe endpoint.
var canaryEndpoints = map[string]CanaryProbe{
"label": {Method: "GET", Path: "/v1/{owner}/{repo}/issue_tags", NeedsRepo: true},
"notification": {Method: "GET", Path: "/notifications?page=1&limit=1", NeedsRepo: false},
"pm": {Method: "GET", Path: "/pm/dashboards", NeedsRepo: false},
"wiki": {Method: "GET", Path: "/{owner}/{repo}/wiki_pages", NeedsRepo: true},
"pipeline": {Method: "GET", Path: "/pm/pipelines", NeedsRepo: false},
"webhook": {Method: "GET", Path: "/v1/{owner}/{repo}/webhooks", NeedsRepo: true},
"member": {Method: "GET", Path: "/{owner}/{repo}/collaborators", NeedsRepo: true},
"milestone": {Method: "GET", Path: "/v1/{owner}/{repo}/milestones", NeedsRepo: true},
"export": {Method: "GET", Path: "/{owner}/{repo}/contributors", NeedsRepo: true},
"search": {Method: "GET", Path: "/repos/search?q=test&limit=1", NeedsRepo: false},
"workflow": {Method: "GET", Path: "/v1/{owner}/{repo}", NeedsRepo: true},
}
// Registry holds capability probe results with thread-safe access.
type Registry struct {
mu sync.RWMutex
statuses map[string]*DomainStatus
cachePath string
}
// NewRegistry creates a Registry and attempts to load cached results.
func NewRegistry() *Registry {
r := &Registry{
statuses: make(map[string]*DomainStatus),
cachePath: cacheFilePath(),
}
r.load()
return r
}
// Get returns the cached status for a domain.
func (r *Registry) Get(domain string) Status {
r.mu.RLock()
defer r.mu.RUnlock()
if ds, ok := r.statuses[domain]; ok {
return ds.Status
}
return StatusUnknown
}
// GetAll returns a copy of all domain statuses.
func (r *Registry) GetAll() map[string]*DomainStatus {
r.mu.RLock()
defer r.mu.RUnlock()
result := make(map[string]*DomainStatus, len(r.statuses))
for k, v := range r.statuses {
copy := *v
result[k] = &copy
}
return result
}
// ProbeAll probes all registered domains concurrently.
// owner and repo are used for endpoints that require repository context.
// If owner/repo are empty, repo-dependent probes are skipped.
func (r *Registry) ProbeAll(cli *client.Client, owner, repo string) map[string]*DomainStatus {
results := make(map[string]*DomainStatus)
var mu sync.Mutex
var wg sync.WaitGroup
for domain, canary := range canaryEndpoints {
if canary.NeedsRepo && (owner == "" || repo == "") {
// Skip repo-dependent probes when no repo context available
continue
}
wg.Add(1)
go func(domain string, canary CanaryProbe) {
defer wg.Done()
ds := r.probeOne(cli, domain, canary, owner, repo)
mu.Lock()
results[domain] = ds
mu.Unlock()
}(domain, canary)
}
wg.Wait()
// Merge results into registry
r.mu.Lock()
for k, v := range results {
r.statuses[k] = v
}
r.mu.Unlock()
r.save()
return results
}
// probeOne probes a single canary endpoint.
func (r *Registry) probeOne(cli *client.Client, domain string, canary CanaryProbe, owner, repo string) *DomainStatus {
path := canary.Path
if canary.NeedsRepo {
path = strings.Replace(path, "{owner}", owner, 1)
path = strings.Replace(path, "{repo}", repo, 1)
}
ds := &DomainStatus{
Domain: domain,
LastChecked: time.Now(),
}
env, err := cli.Do(canary.Method, path, nil, nil)
if err != nil {
apiErr, ok := err.(*client.APIError)
if !ok {
ds.Status = StatusError
ds.Message = fmt.Sprintf("网络错误: %v", err)
return ds
}
switch {
case apiErr.Code == "HTML_RESPONSE":
// Backend returned HTML instead of JSON — endpoint doesn't exist
ds.Status = StatusUnavailable
ds.Message = "后端 API 尚未实现该端点"
case apiErr.StatusCode == 404:
// 404 means the endpoint path doesn't exist on the backend
ds.Status = StatusUnavailable
ds.Message = "API 端点不存在404"
case apiErr.StatusCode == 401 || apiErr.StatusCode == 403:
// Auth/permission errors mean the endpoint EXISTS but probe lacks credentials.
// The user may have valid credentials — mark as available.
ds.Status = StatusAvailable
ds.Message = "端点存在(探测权限受限,用户可能有完整权限)"
default:
// Other HTTP errors (422, 500, etc.) — endpoint exists but something went wrong
ds.Status = StatusAvailable
ds.Message = fmt.Sprintf("端点响应: HTTP %d", apiErr.StatusCode)
}
return ds
}
if env != nil && env.OK {
ds.Status = StatusAvailable
ds.Message = "API 正常响应"
} else {
ds.Status = StatusUnavailable
if env != nil && env.Error != nil {
ds.Message = env.Error.Message
}
}
return ds
}
// Refresh re-probes all domains and returns the updated statuses.
func (r *Registry) Refresh(cli *client.Client, owner, repo string) map[string]*DomainStatus {
return r.ProbeAll(cli, owner, repo)
}
// IsStale returns true if the cache is older than 24 hours or doesn't exist.
func (r *Registry) IsStale() bool {
r.mu.RLock()
defer r.mu.RUnlock()
for _, ds := range r.statuses {
if time.Since(ds.LastChecked) > 24*time.Hour {
return true
}
}
return len(r.statuses) == 0
}
// Summary returns a human-readable multi-line summary of all domain statuses.
func (r *Registry) Summary() string {
r.mu.RLock()
defer r.mu.RUnlock()
var sb strings.Builder
sb.WriteString("API 后端能力探测结果:\n")
sb.WriteString(strings.Repeat("-", 50) + "\n")
// Order domains for consistent output
domains := []string{
"label", "notification", "pm", "wiki", "pipeline",
"webhook", "member", "milestone", "export", "search", "workflow",
}
for _, domain := range domains {
ds, ok := r.statuses[domain]
if !ok {
sb.WriteString(fmt.Sprintf(" ? %-15s 未探测\n", domain))
continue
}
icon := ds.Status.Emoji()
statusText := ds.Status.String()
detail := ""
if ds.Message != "" {
detail = " — " + ds.Message
}
sb.WriteString(fmt.Sprintf(" %s %-15s %s%s\n", icon, domain, statusText, detail))
}
return sb.String()
}
// cacheFilePath returns the path to the capability cache file.
func cacheFilePath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "gitlink-cli", "capabilities.json")
}
// save writes the current registry state to the cache file.
func (r *Registry) save() {
r.mu.RLock()
data, err := json.MarshalIndent(r.statuses, "", " ")
r.mu.RUnlock()
if err != nil {
return
}
dir := filepath.Dir(r.cachePath)
os.MkdirAll(dir, 0700)
os.WriteFile(r.cachePath, data, 0600)
}
// load reads cached capability data from disk.
func (r *Registry) load() {
data, err := os.ReadFile(r.cachePath)
if err != nil {
return
}
var statuses map[string]*DomainStatus
if err := json.Unmarshal(data, &statuses); err != nil {
return
}
r.mu.Lock()
r.statuses = statuses
r.mu.Unlock()
}

View File

@ -0,0 +1,402 @@
package capability
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/gitlink-org/gitlink-cli/internal/client"
)
// newTestRegistry creates a Registry with a temp cache file to avoid
// interference from real CLI cache files.
func newTestRegistry(t *testing.T) *Registry {
t.Helper()
r := &Registry{
statuses: make(map[string]*DomainStatus),
cachePath: filepath.Join(t.TempDir(), "capabilities.json"),
}
return r
}
func TestStatusString(t *testing.T) {
tests := []struct {
status Status
want string
}{
{StatusUnknown, "unknown"},
{StatusAvailable, "available"},
{StatusUnavailable, "unavailable"},
{StatusError, "error"},
}
for _, tt := range tests {
if got := tt.status.String(); got != tt.want {
t.Errorf("Status(%d).String() = %q, want %q", tt.status, got, tt.want)
}
}
}
func TestStatusEmoji(t *testing.T) {
tests := []struct {
status Status
want string
}{
{StatusUnknown, "?"},
{StatusAvailable, "✓"},
{StatusUnavailable, "⚠"},
{StatusError, "✗"},
}
for _, tt := range tests {
if got := tt.status.Emoji(); got != tt.want {
t.Errorf("Status(%d).Emoji() = %q, want %q", tt.status, got, tt.want)
}
}
}
func TestProbeOneAvailable(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true,"data":[]}`))
}))
defer server.Close()
r := newTestRegistry(t)
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
canary := CanaryProbe{Method: "GET", Path: "/api/test", NeedsRepo: false}
ds := r.probeOne(cli, "test", canary, "", "")
if ds.Status != StatusAvailable {
t.Errorf("expected StatusAvailable, got %s", ds.Status)
}
if ds.Message != "API 正常响应" {
t.Errorf("Message = %q, want 'API 正常响应'", ds.Message)
}
}
func TestProbeOneHTMLResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<!DOCTYPE html><html><body>Login</body></html>`))
}))
defer server.Close()
r := newTestRegistry(t)
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
canary := CanaryProbe{Method: "GET", Path: "/api/test", NeedsRepo: false}
ds := r.probeOne(cli, "test", canary, "", "")
if ds.Status != StatusUnavailable {
t.Errorf("expected StatusUnavailable, got %s", ds.Status)
}
if !strings.Contains(ds.Message, "尚未实现") {
t.Errorf("Message should mention '未实现', got %q", ds.Message)
}
}
func TestProbeOne404(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("not found"))
}))
defer server.Close()
r := newTestRegistry(t)
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
canary := CanaryProbe{Method: "GET", Path: "/api/test", NeedsRepo: false}
ds := r.probeOne(cli, "test", canary, "", "")
if ds.Status != StatusUnavailable {
t.Errorf("expected StatusUnavailable, got %s", ds.Status)
}
if !strings.Contains(ds.Message, "404") {
t.Errorf("Message should mention 404, got %q", ds.Message)
}
}
func TestProbeOne401(t *testing.T) {
// 401 means endpoint exists but auth is needed — should be Available
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"status":401,"message":"请登录后再操作"}`))
}))
defer server.Close()
r := newTestRegistry(t)
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
canary := CanaryProbe{Method: "GET", Path: "/api/test", NeedsRepo: false}
ds := r.probeOne(cli, "test", canary, "", "")
if ds.Status != StatusAvailable {
t.Errorf("expected StatusAvailable for 401 (endpoint exists), got %s", ds.Status)
}
}
func TestProbeOne403(t *testing.T) {
// 403 means endpoint exists but permission denied — should be Available
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"status":403,"message":"您没有权限进行该操作"}`))
}))
defer server.Close()
r := newTestRegistry(t)
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
canary := CanaryProbe{Method: "GET", Path: "/api/test", NeedsRepo: false}
ds := r.probeOne(cli, "test", canary, "", "")
if ds.Status != StatusAvailable {
t.Errorf("expected StatusAvailable for 403 (endpoint exists), got %s", ds.Status)
}
}
func TestProbeOneWithRepoPlaceholders(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/myowner/myrepo/issue_tags.json" {
t.Errorf("path = %s, want /api/v1/myowner/myrepo/issue_tags.json", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true,"data":[]}`))
}))
defer server.Close()
r := newTestRegistry(t)
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
canary := CanaryProbe{Method: "GET", Path: "/api/v1/{owner}/{repo}/issue_tags", NeedsRepo: true}
ds := r.probeOne(cli, "label", canary, "myowner", "myrepo")
if ds.Status != StatusAvailable {
t.Errorf("expected StatusAvailable, got %s", ds.Status)
}
}
func TestProbeAllSkipsRepoProbesWhenNoContext(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true}`))
}))
defer server.Close()
r := newTestRegistry(t)
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
// Probe with empty owner/repo — repo-dependent probes should be skipped
results := r.ProbeAll(cli, "", "")
// Repo-less endpoints (notification, pm, wiki, pipeline, search) should be probed
for _, domain := range []string{"notification", "pm", "pipeline", "search"} {
if _, ok := results[domain]; !ok {
t.Errorf("domain %q should be probed (no repo needed), but was skipped", domain)
}
}
// Repo-dependent endpoints should be skipped
for _, domain := range []string{"label", "webhook", "member", "milestone", "export", "wiki", "workflow"} {
if _, ok := results[domain]; ok {
t.Errorf("domain %q needs repo context, should be skipped, but was probed", domain)
}
}
}
func TestProbeAllWithRepoContext(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true,"data":[]}`))
}))
defer server.Close()
r := newTestRegistry(t)
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
results := r.ProbeAll(cli, "owner", "repo")
// All domains should be probed when repo context is available
allDomains := []string{
"label", "notification", "pm", "wiki", "pipeline",
"webhook", "member", "milestone", "export", "search", "workflow",
}
for _, domain := range allDomains {
if _, ok := results[domain]; !ok {
t.Errorf("domain %q should be probed, but was skipped", domain)
}
}
}
func TestGetSet(t *testing.T) {
r := newTestRegistry(t)
// Initial state: unknown
if r.Get("label") != StatusUnknown {
t.Error("expected StatusUnknown before any probe")
}
// Manually set a status via ProbeAll
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true}`))
}))
defer server.Close()
cli := &client.Client{HTTP: server.Client(), BaseURL: server.URL}
r.ProbeAll(cli, "", "")
// After probe, notification should be known (repo-less endpoint was probed)
if r.Get("notification") == StatusUnknown {
t.Error("notification should have been probed")
}
}
func TestIsStale(t *testing.T) {
r := newTestRegistry(t)
if !r.IsStale() {
t.Error("empty registry should be stale")
}
// Add a fresh entry
r.mu.Lock()
r.statuses["test"] = &DomainStatus{
Domain: "test",
Status: StatusAvailable,
LastChecked: time.Now(),
}
r.mu.Unlock()
if r.IsStale() {
t.Error("registry with fresh entry should not be stale")
}
// Add a stale entry
r.mu.Lock()
r.statuses["stale"] = &DomainStatus{
Domain: "stale",
Status: StatusAvailable,
LastChecked: time.Now().Add(-48 * time.Hour),
}
r.mu.Unlock()
if !r.IsStale() {
t.Error("registry with stale entry should be stale")
}
}
func TestGetAll(t *testing.T) {
r := newTestRegistry(t)
r.mu.Lock()
r.statuses["test"] = &DomainStatus{Domain: "test", Status: StatusAvailable}
r.mu.Unlock()
all := r.GetAll()
if len(all) != 1 {
t.Fatalf("expected 1 entry, got %d", len(all))
}
if all["test"].Status != StatusAvailable {
t.Error("GetAll should return a copy with correct data")
}
}
func TestCacheSaveLoad(t *testing.T) {
dir := t.TempDir()
r := newTestRegistry(t)
r.cachePath = filepath.Join(dir, "capabilities.json")
// Add some data
r.mu.Lock()
r.statuses["test"] = &DomainStatus{
Domain: "test",
Status: StatusAvailable,
Message: "working",
LastChecked: time.Now(),
}
r.mu.Unlock()
// Save
r.save()
if _, err := os.Stat(r.cachePath); os.IsNotExist(err) {
t.Fatal("cache file was not created")
}
// Load into new registry (no load from disk — just read the saved file)
r2 := &Registry{
statuses: make(map[string]*DomainStatus),
cachePath: r.cachePath,
}
r2.load()
if r2.Get("test") != StatusAvailable {
t.Errorf("loaded status = %s, want available", r2.Get("test"))
}
}
func TestCacheFileRoundTrip(t *testing.T) {
dir := t.TempDir()
cachePath := filepath.Join(dir, "capabilities.json")
// Create and populate registry
r := newTestRegistry(t)
r.cachePath = cachePath
r.mu.Lock()
r.statuses["search"] = &DomainStatus{
Domain: "search",
Status: StatusUnavailable,
Message: "API 端点不存在404",
LastChecked: time.Now(),
}
r.mu.Unlock()
r.save()
// Verify JSON structure
data, err := os.ReadFile(cachePath)
if err != nil {
t.Fatal(err)
}
var decoded map[string]*DomainStatus
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if decoded["search"].Status != StatusUnavailable {
t.Errorf("decoded status = %d, want %d", decoded["search"].Status, StatusUnavailable)
}
}
func TestSummary(t *testing.T) {
r := newTestRegistry(t)
r.mu.Lock()
// Use domains from the hardcoded list in Summary()
r.statuses["search"] = &DomainStatus{Domain: "search", Status: StatusAvailable, Message: "API 正常响应"}
r.statuses["wiki"] = &DomainStatus{Domain: "wiki", Status: StatusUnavailable, Message: "后端 API 尚未实现该端点"}
r.mu.Unlock()
summary := r.Summary()
if !strings.Contains(summary, "search") {
t.Error("Summary should contain domain name 'search'")
}
if !strings.Contains(summary, "wiki") {
t.Error("Summary should contain domain name 'wiki'")
}
if !strings.Contains(summary, "available") {
t.Error("Summary should contain status text")
}
}
func TestCanaryEndpointsHaveValidDomains(t *testing.T) {
// Verify that all canary endpoints map to known domains
for domain, canary := range canaryEndpoints {
if canary.Method == "" {
t.Errorf("domain %q: Method is empty", domain)
}
if canary.Path == "" {
t.Errorf("domain %q: Path is empty", domain)
}
if canary.NeedsRepo && !strings.Contains(canary.Path, "{owner}") && !strings.Contains(canary.Path, "{repo}") {
t.Errorf("domain %q: NeedsRepo=true but path has no owner/repo placeholder", domain)
}
}
}

View File

@ -8,9 +8,11 @@ import (
"net/http"
"net/url"
"strings"
"time"
"github.com/gitlink-org/gitlink-cli/internal/auth"
"github.com/gitlink-org/gitlink-cli/internal/config"
"github.com/gitlink-org/gitlink-cli/internal/demo"
"github.com/gitlink-org/gitlink-cli/internal/output"
)
@ -35,8 +37,14 @@ func New() (*Client, error) {
if err != nil {
return nil, err
}
httpClient := auth.NewHTTPClient()
if demo.Enabled() {
// Offline mock mode (--demo / GITLINK_DEMO=1): short-circuit every
// outbound call through the in-process fixture transport.
httpClient.Transport = demo.NewTransport()
}
return &Client{
HTTP: auth.NewHTTPClient(),
HTTP: httpClient,
BaseURL: cfg.BaseURL,
}, nil
}
@ -79,10 +87,31 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
return nil, err
}
if c.Debug {
fmt.Printf("→ %s %s\n", method, fullURL)
// Set Content-Type for JSON bodies so Rails parses params correctly.
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.Debug {
fmt.Printf("[DEBUG] → %s %s\n", method, fullURL)
authShown := false
for k, v := range req.Header {
if strings.EqualFold(k, "Authorization") {
fmt.Printf(" [DEBUG] %s: Bearer ***\n", k)
authShown = true
} else {
fmt.Printf(" [DEBUG] %s: %s\n", k, strings.Join(v, ","))
}
}
// Authorization is injected by the auth transport at RoundTrip time,
// so it is not yet on req.Header here. Print a redacted hint anyway so
// the debug trace documents the auth header for demos / verify.sh.
if !authShown {
fmt.Printf(" [DEBUG] Authorization: Bearer *** (transport-injected)\n")
}
}
debugStart := time.Now()
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
@ -95,7 +124,13 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
}
if c.Debug {
fmt.Printf("← %d %s\n", resp.StatusCode, string(respData[:min(len(respData), 200)]))
elapsed := time.Since(debugStart)
fmt.Printf("[DEBUG] ← %d %s (%dms, %dB)\n", resp.StatusCode, http.StatusText(resp.StatusCode), elapsed.Milliseconds(), len(respData))
bodyPreview := respData
if len(bodyPreview) > 200 {
bodyPreview = bodyPreview[:200]
}
fmt.Printf(" [DEBUG] body: %s\n", string(bodyPreview))
}
// Check HTTP-level errors
@ -107,10 +142,21 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
}
}
// Detect HTML responses (GitLink returns login pages when auth is missing)
if detectHTMLResponse(respData) {
msg := "服务器返回了 HTML 页面而非 JSON 数据"
suggestion := suggestHTMLFix()
return output.ErrorEnvelope(resp.StatusCode, msg, suggestion),
&APIError{
StatusCode: resp.StatusCode,
Code: "HTML_RESPONSE",
Message: msg + "\n" + suggestion,
}
}
// Parse JSON
var raw map[string]interface{}
if err := json.Unmarshal(respData, &raw); err != nil {
// Not JSON, return as-is
return output.SuccessEnvelope(string(respData), nil), nil
}
@ -164,6 +210,11 @@ func shouldAppendJSONSuffix(path string) bool {
if strings.HasSuffix(path, ".json") {
return false
}
// Wiki API uses a centralized controller (/api/wiki/*) that does not
// support the .json format suffix used by other endpoints.
if strings.Contains(path, "/wiki/") {
return false
}
parts := strings.Split(strings.Trim(path, "/"), "/")
for i, part := range parts {
if part == "raw" && i >= 2 && i+2 < len(parts) {
@ -201,6 +252,41 @@ func (c *Client) Delete(path string, query url.Values) (*output.Envelope, error)
return c.Do("DELETE", path, nil, query)
}
// detectHTMLResponse detects whether the response body is an HTML page instead of JSON.
// It first strips any XML declaration (<?xml ...?>) before checking for HTML prefixes.
func detectHTMLResponse(data []byte) bool {
trimmed := bytes.TrimSpace(data)
if len(trimmed) == 0 {
return false
}
// Skip leading XML declaration (e.g., <?xml version="1.0"?>)
if bytes.HasPrefix(trimmed, []byte("<?")) {
if idx := bytes.Index(trimmed, []byte("?>")); idx != -1 {
trimmed = bytes.TrimSpace(trimmed[idx+2:])
}
}
if len(trimmed) == 0 {
return false
}
// Check for HTML document prefixes
prefixes := []string{"<!DOCTYPE", "<html", "<HTML", "<!doctype"}
for _, p := range prefixes {
if bytes.HasPrefix(trimmed, []byte(p)) {
return true
}
}
return false
}
func suggestHTMLFix() string {
return "API 返回了 HTML 页面而非 JSON 数据。" +
"可能原因:\n" +
" 1. 未登录或 Token 已过期 → 运行 gitlink-cli auth login\n" +
" 2. Token 权限不足 → 在 GitLink 平台重新生成 Token\n" +
" 3. API 端点不存在 → 检查路径是否正确\n" +
" 4. 使用 Shortcut 命令替代 Raw API → 运行 gitlink-cli --help 查看可用命令"
}
func suggestFix(code int) string {
switch code {
case 401:

View File

@ -7,6 +7,7 @@ import (
"net/url"
"os"
"path/filepath"
"strings"
"testing"
)
@ -525,3 +526,110 @@ func TestShouldAppendJSONSuffixSkipsExistingJSONPath(t *testing.T) {
t.Fatal("existing .json path should not get another suffix")
}
}
func TestDetectHTMLResponse(t *testing.T) {
tests := []struct {
name string
body string
wantHTML bool
}{
{"正常 JSON", `{"key":"value"}`, false},
{"DOCTYPE 开头", `<!DOCTYPE html><html>...</html>`, true},
{"html 小写开头", `<html><head>...</head></html>`, true},
{"HTML 大写开头", `<HTML><HEAD>...</HEAD></HTML>`, true},
{"doctype 小写开头", `<!doctype html><html lang="en">`, true},
{"空响应体", "", false},
{"纯文本", `just some text`, false},
{"空白后 HTML", ` <!DOCTYPE html>`, true},
{"JSON 数组", `[1,2,3]`, false},
{"HTML 片段(无前缀)", `<body>content</body>`, false},
{"XML 声明后跟 HTML", `<?xml version="1.0"?><!DOCTYPE html>`, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := detectHTMLResponse([]byte(tt.body)); got != tt.wantHTML {
t.Errorf("detectHTMLResponse(%q) = %v, want %v", tt.body, got, tt.wantHTML)
}
})
}
}
func TestClientDoHTMLResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<!DOCTYPE html><html><head><title>Sign in</title></head><body>Please log in</body></html>`))
}))
defer server.Close()
c := &Client{HTTP: server.Client(), BaseURL: server.URL}
env, err := c.Do("GET", "/api/test", nil, nil)
if err == nil {
t.Fatal("expected error for HTML response")
}
if env == nil {
t.Fatal("expected envelope for HTML response")
}
if env.OK {
t.Fatal("expected OK=false for HTML response")
}
apiErr, ok := err.(*APIError)
if !ok {
t.Fatalf("expected *APIError, got %T", err)
}
if apiErr.Code != "HTML_RESPONSE" {
t.Fatalf("Code = %v, want HTML_RESPONSE", apiErr.Code)
}
}
func TestSuggestHTMLFix(t *testing.T) {
msg := suggestHTMLFix()
if msg == "" {
t.Fatal("suggestHTMLFix should return a non-empty message")
}
if !strings.Contains(msg, "gitlink-cli auth login") {
t.Fatal("suggestHTMLFix should mention auth login")
}
}
func TestClientDebugTraceFormat(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":0,"data":{"x":1}}`))
}))
defer server.Close()
// Capture stdout around the debug trace.
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
c := &Client{HTTP: server.Client(), BaseURL: server.URL, Debug: true}
_, err := c.Do("GET", "/api/probe", nil, nil)
w.Close()
os.Stdout = old
if err != nil {
t.Fatalf("Do: %v", err)
}
var buf strings.Builder
io_Copy(&buf, r)
out := buf.String()
for _, want := range []string{"[DEBUG]", "Bearer ***", "ms", "body"} {
if !strings.Contains(out, want) {
t.Errorf("debug output missing %q:\n%s", want, out)
}
}
}
// io_Copy is a thin wrapper kept here so the test file does not need to pull
// in "io" at the top only for a single call.
func io_Copy(dst *strings.Builder, src interface{ Read([]byte) (int, error) }) {
b := make([]byte, 4096)
for {
n, err := src.Read(b)
if n > 0 {
dst.Write(b[:n])
}
if err != nil {
return
}
}
}

107
internal/demo/demo.go Normal file
View File

@ -0,0 +1,107 @@
// Package demo provides an offline mock transport so the CLI can run every
// command with no network and no token — the backbone of the projector-friendly
// `--demo` / `GITLINK_DEMO=1` mode.
//
// Transport implements http.RoundTripper. For each request it:
// 1. derives a fixture key from method + last path segment (e.g. GET:issues);
// 2. returns the matching embedded fixture if present;
// 3. otherwise returns a generic success envelope shaped after GitLink's
// {"status":0,...} convention, so shortcuts keep parsing cleanly.
package demo
import (
"bytes"
"io"
"net/http"
"os"
"strings"
)
// DemoFlag is bound to the global --demo flag from cmd/root.go. It is read by
// Enabled, so the flag wiring lives in the cmd layer while this package stays
// free of cmd imports.
var DemoFlag bool
// Enabled reports whether demo mode is active (via --demo or GITLINK_DEMO=1).
func Enabled() bool {
return DemoFlag || os.Getenv("GITLINK_DEMO") == "1"
}
// Transport returns canned JSON for known endpoints and a generic success
// envelope for the rest.
type Transport struct {
fixtures map[string][]byte
}
// NewTransport returns a Transport seeded with the embedded fixtures.
func NewTransport() *Transport {
return &Transport{fixtures: defaultFixtures()}
}
// RoundTrip implements http.RoundTripper.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
key := buildFixtureKey(req)
if data, ok := t.fixtures[key]; ok {
return mockResponse(req, 200, data), nil
}
if data, ok := fuzzyMatch(req); ok {
return mockResponse(req, 200, data), nil
}
return mockResponse(req, 200, genericResponse(req.Method)), nil
}
// buildFixtureKey reduces a request to METHOD:lastSegment so the fixture table
// is owner/repo agnostic (GET /{owner}/{repo}/issues.json → "GET:issues").
func buildFixtureKey(req *http.Request) string {
path := strings.TrimSuffix(req.URL.Path, ".json")
path = strings.Trim(path, "/")
segs := strings.Split(path, "/")
last := segs[len(segs)-1]
return strings.ToUpper(req.Method) + ":" + last
}
// fuzzyMatch recognises a few path shapes that lastSegment alone cannot
// disambiguate (e.g. /users/me vs /users/:login).
func fuzzyMatch(req *http.Request) ([]byte, bool) {
path := strings.TrimSuffix(req.URL.Path, ".json")
switch {
case strings.HasSuffix(path, "/users/me"):
return fixtureUsersMe, true
case strings.HasSuffix(path, "/headmaps"):
return fixtureHeadmaps, true
case strings.Contains(path, "/wiki/"):
return fixtureWikiPages, true
}
return nil, false
}
// genericResponse synthesises a minimal success body for unmatched requests.
func genericResponse(method string) []byte {
switch method {
case http.MethodGet:
return []byte(`{"status":0,"total_count":0,"data":[]}`)
case http.MethodPost, http.MethodPut:
return []byte(`{"status":0,"id":1,"message":"success (demo)"}`)
case http.MethodDelete:
return []byte(`{"status":0,"message":"deleted (demo)"}`)
default:
return []byte(`{"status":0,"message":"ok (demo)"}`)
}
}
// mockResponse builds an http.Response carrying data as JSON.
func mockResponse(req *http.Request, status int, data []byte) *http.Response {
return &http.Response{
Status: http.StatusText(status),
StatusCode: status,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{
"Content-Type": []string{"application/json"},
},
Body: io.NopCloser(bytes.NewReader(data)),
ContentLength: int64(len(data)),
Request: req,
}
}

191
internal/demo/demo_test.go Normal file
View File

@ -0,0 +1,191 @@
package demo
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestEnabledFlagAndEnv(t *testing.T) {
oldFlag := DemoFlag
t.Setenv("GITLINK_DEMO", "")
defer func() { DemoFlag = oldFlag }()
DemoFlag = false
if Enabled() {
t.Error("disabled when flag false and env empty")
}
DemoFlag = true
if !Enabled() {
t.Error("flag true should enable")
}
DemoFlag = false
t.Setenv("GITLINK_DEMO", "1")
if !Enabled() {
t.Error("env=1 should enable")
}
}
func TestBuildFixtureKey(t *testing.T) {
cases := []struct {
method, path, want string
}{
{"GET", "/api/v1/jiangtx/demo/issues.json", "GET:issues"},
{"POST", "/api/v1/jiangtx/demo/issues.json", "POST:issues"},
{"GET", "/api/v1/jiangtx/demo/languages.json", "GET:languages"},
{"GET", "/api/v1/users/me.json", "GET:me"},
{"GET", "/api/v1/jiangtx/demo", "GET:demo"},
}
for _, c := range cases {
req := httptest.NewRequest(c.method, c.path, nil)
if got := buildFixtureKey(req); got != c.want {
t.Errorf("buildFixtureKey(%s %s) = %q, want %q", c.method, c.path, got, c.want)
}
}
}
func TestTransportReturnsFixtureForKnownEndpoint(t *testing.T) {
tr := NewTransport()
req := httptest.NewRequest("GET", "/api/v1/o/r/issues.json", nil)
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip error: %v", err)
}
if resp.StatusCode != 200 {
t.Errorf("status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), `"status":0`) {
t.Errorf("expected status:0 in fixture body, got: %s", string(body))
}
}
func TestTransportGenericFallback(t *testing.T) {
tr := NewTransport()
// /unknown-segment has no fixture → genericResponse.
req := httptest.NewRequest("GET", "/api/v1/o/r/unknownseg.json", nil)
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip error: %v", err)
}
body, _ := io.ReadAll(resp.Body)
var m map[string]interface{}
if err := json.Unmarshal(body, &m); err != nil {
t.Fatalf("generic body not JSON: %v (body=%s)", err, string(body))
}
if m["status"] != float64(0) {
t.Errorf("generic GET status = %v, want 0", m["status"])
}
}
func TestTransportGenericPost(t *testing.T) {
tr := NewTransport()
req := httptest.NewRequest("POST", "/api/v1/o/r/unknownxyz.json", strings.NewReader("{}"))
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip error: %v", err)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), `"id":1`) {
t.Errorf("generic POST missing id:1: %s", string(body))
}
}
func TestTransportGenericDelete(t *testing.T) {
tr := NewTransport()
req := httptest.NewRequest("DELETE", "/api/v1/o/r/unknownxyz.json", nil)
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip error: %v", err)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "deleted") {
t.Errorf("generic DELETE missing 'deleted': %s", string(body))
}
}
func TestGenericResponseDefault(t *testing.T) {
if len(genericResponse(http.MethodPatch)) == 0 {
t.Error("PATCH generic should be non-empty")
}
}
func TestTransportFuzzyUsersMe(t *testing.T) {
tr := NewTransport()
req := httptest.NewRequest("GET", "/api/v1/users/me.json", nil)
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip error: %v", err)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), `"login":"jiangtx"`) {
t.Errorf("fuzzy /users/me missing login: %s", string(body))
}
}
func TestTransportFuzzyHeadmaps(t *testing.T) {
tr := NewTransport()
req := httptest.NewRequest("GET", "/api/v1/users/jiangtx/headmaps.json", nil)
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip error: %v", err)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "contributions") {
t.Errorf("fuzzy headmaps missing contributions: %s", string(body))
}
}
func TestTransportFuzzyWiki(t *testing.T) {
tr := NewTransport()
req := httptest.NewRequest("GET", "/api/v1/wiki/o/r/pages", nil)
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip error: %v", err)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "pages") {
t.Errorf("fuzzy wiki missing pages: %s", string(body))
}
}
func TestTransportContentLength(t *testing.T) {
tr := NewTransport()
req := httptest.NewRequest("GET", "/api/v1/o/r/issues.json", nil)
resp, _ := tr.RoundTrip(req)
if resp.ContentLength <= 0 {
t.Errorf("ContentLength = %d, want > 0", resp.ContentLength)
}
if resp.Header.Get("Content-Type") != "application/json" {
t.Errorf("Content-Type = %q, want application/json", resp.Header.Get("Content-Type"))
}
}
func TestDefaultFixturesSeeded(t *testing.T) {
m := defaultFixtures()
if len(m) == 0 {
t.Fatal("defaultFixtures returned empty map")
}
for _, key := range []string{"GET:issues", "POST:issues", "GET:webhooks", "GET:labels"} {
if _, ok := m[key]; !ok {
t.Errorf("missing fixture key %q", key)
}
}
}
func TestTransportAsRoundTripper(t *testing.T) {
// Transport must be usable as http.Client.Transport (integration smoke test).
tr := NewTransport()
client := &http.Client{Transport: tr}
resp, err := client.Get("http://demo.invalid/api/v1/o/r/issues.json")
if err != nil {
t.Fatalf("client.Get: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("status = %d", resp.StatusCode)
}
}

View File

@ -0,0 +1 @@
{"status":0,"total_count":2,"branches":[{"name":"master","protected":true},{"name":"dev","protected":false}]}

View File

@ -0,0 +1 @@
{"status":0,"members":[{"id":101325,"login":"member-a","role":"Developer"},{"id":126177,"login":"member-b","role":"Developer"}]}

View File

@ -0,0 +1 @@
{"status":0,"total_count":1,"commits":[{"sha":"abc1234","message":"demo commit","author":{"login":"jiangtx","name":"演示"}}]}

View File

@ -0,0 +1 @@
{"status":0,"contributors":[{"login":"jiangtx","contributions":120,"email":"demo@example.com"}]}

View File

@ -0,0 +1 @@
{"status":0,"total_count":2,"count":2,"issues":[{"id":1,"project_issues_index":1,"subject":"示例 Issue A","description":"演示","status":1,"priority_id":2},{"id":2,"project_issues_index":2,"subject":"示例 Issue B","description":"演示","status":1}]}

View File

@ -0,0 +1 @@
{"status":0,"labels":[{"id":1,"name":"bug","color":"#fc2929"},{"id":2,"name":"enhancement","color":"#84b6eb"}]}

View File

@ -0,0 +1 @@
{"status":0,"Go":85.5,"Shell":10.0,"Other":4.5}

View File

@ -0,0 +1 @@
{"status":0,"total_count":1,"versions":[{"id":1,"name":"Sprint 6","description":"演示里程碑","effective_date":"2026-07-15","status":"open"}]}

View File

@ -0,0 +1 @@
{"status":0,"total_count":2,"notifications":[{"id":1,"subject":"通知 1","status":1},{"id":2,"subject":"通知 2","status":1}]}

View File

@ -0,0 +1 @@
{"status":0,"total_count":1,"pipelines":[{"id":1,"name":"build","file":"build.yml","status":"success","run_number":42}]}

View File

@ -0,0 +1 @@
{"status":0,"total_count":1,"projects":[{"id":1,"name":"演示看板","identifier":"demo-board"}]}

View File

@ -0,0 +1 @@
{"status":0,"total_count":1,"pulls":[{"id":1,"pull_request_id":1,"title":"示例 PR","status":0,"base":"master","head":"dev"}]}

View File

@ -0,0 +1 @@
{"status":0,"total_count":1,"releases":[{"id":1,"tag_name":"v2.0","name":"v2.0","body":"演示发布"}]}

View File

@ -0,0 +1 @@
{"status":0,"total_count":1,"webhooks":[{"id":51348,"url":"https://ci.example.com/hook","http_method":"POST","events":["push","create"]}]}

View File

@ -0,0 +1 @@
{"status":0,"id":99,"project_issues_index":99,"subject":"演示 Issue (demo)","description":"通过 demo 模式创建"}

View File

@ -0,0 +1 @@
{"status":0,"id":3,"name":"P0","color":"#FF0000","description":"演示标签"}

Some files were not shown because too many files have changed in this diff Show More