Merge PR #160: Add GitLink feedback submission shortcut
# Conflicts: # README.zh-CN.md # shortcuts/register_test.go
This commit is contained in:
commit
c4bac42b34
13
README.md
13
README.md
|
|
@ -505,6 +505,19 @@ gitlink-cli search +repos -k "machine learning"
|
|||
gitlink-cli search +users -k "zhangsan"
|
||||
```
|
||||
|
||||
### Feedback
|
||||
|
||||
```bash
|
||||
# Preview a feedback submission without calling the API
|
||||
gitlink-cli feedback +create --content "The CLI install guide needs a Windows note." --category docs --dry-run
|
||||
|
||||
# Submit longer feedback from a file and attach contact/repository context
|
||||
gitlink-cli feedback +create --from feedback.md --category cli --contact mengz@example.com --repo-ref Gitlink/gitlink-cli
|
||||
|
||||
# Pipe feedback from another command
|
||||
Get-Content feedback.md | gitlink-cli feedback +create --stdin --category feature
|
||||
```
|
||||
|
||||
### Workflow Agent Commands
|
||||
|
||||
`workflow` provides rule-based repository analysis for maintainers and AI Agents. It currently supports:
|
||||
|
|
|
|||
142
README.zh-CN.md
142
README.zh-CN.md
|
|
@ -5,7 +5,7 @@
|
|||
[](https://golang.org)
|
||||
[](https://www.npmjs.com/package/@gitlink-ai/cli)
|
||||
|
||||
[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、Webhook、成员协作、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 AI Agent [Skills](./skills/README.md)。
|
||||
[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、Webhook、成员协作、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 AI Agent [Skills](./skills/)。
|
||||
|
||||
**[English](./README.md)**
|
||||
|
||||
|
|
@ -78,19 +78,11 @@
|
|||
<a href="https://www.gitlink.org.cn/jiangtx" title="jiangtx"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/J/67_157_94/120.png" width="40" height="40" alt="jiangtx" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/jiangtx">jiangtx</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/luwanzhou" title="luwanzhou"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/L/165_135_246/120.png" width="40" height="40" alt="luwanzhou" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/luwanzhou">luwanzhou</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/whale_hihihi" title="whale_hihihi"><img src="https://www.gitlink.org.cn/images/avatars/User/137722?t=1778575729" width="40" height="40" alt="whale_hihihi" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/whale_hihihi">whale_hihihi</a></sub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 为什么选择 gitlink-cli?
|
||||
|
||||
- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/README.md),兼容 Claude Code — Agent 零配置即可操作 GitLink
|
||||
- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/),兼容 Claude Code — Agent 零配置即可操作 GitLink
|
||||
- **广泛覆盖** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户等常用工作流均提供高层命令
|
||||
- **AI 友好 & 优化** — 每条命令都经过真实 Agent 测试,简洁参数、智能默认值、结构化输出
|
||||
- **跨平台** — macOS、Linux、Windows (x64/arm64) 全支持,`npm` 一条命令安装
|
||||
|
|
@ -104,19 +96,17 @@
|
|||
| 分类 | 能力 |
|
||||
|------|------|
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息、洞察数据和互动状态 |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭/更新/删除/重开/标签/指派/评论/导出/导入、评论 Issue |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
|
||||
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
| 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 |
|
||||
| 🌿 分支 | 创建、删除、保护分支 |
|
||||
| 🏷️ 发布 | 创建、编辑、更新、查看、删除 Release,并管理发布资产 |
|
||||
| 🏷️ 发布 | 创建、编辑、更新、查看、删除 Release |
|
||||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
| 📊 数据集 | 按项目查询科研数据集 |
|
||||
| 👤 用户 | 查看用户资料和信息 |
|
||||
| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 |
|
||||
| 📋 项目管理 | Sprint 管理、看板、周报 |
|
||||
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
|
||||
|
||||
|
|
@ -260,30 +250,11 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
|
|||
gitlink-cli repo +like --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
|
||||
|
||||
# 查看和更新仓库导航模块
|
||||
gitlink-cli repo +units --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +set-units --owner Gitlink --repo forgeplus --units code,issues,pulls,wiki
|
||||
|
||||
# 创建仓库
|
||||
gitlink-cli repo +create -n my-project -d "项目描述"
|
||||
|
||||
# Fork 仓库
|
||||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出可接收仓库转移的组织
|
||||
gitlink-cli repo +transfer-orgs --owner Gitlink --repo forgeplus
|
||||
|
||||
# 预览仓库转移请求,不修改线上数据
|
||||
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --dry-run
|
||||
|
||||
# 确认后发起仓库转移
|
||||
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --yes
|
||||
|
||||
# 预览取消待处理的仓库转移
|
||||
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --dry-run
|
||||
|
||||
# 确认后取消待处理的仓库转移
|
||||
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --yes
|
||||
```
|
||||
|
||||
### Webhook 管理
|
||||
|
|
@ -352,38 +323,6 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12
|
|||
# 从 CSV 文件批量关闭 Issue
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# 按 API issue id 预览批量更新元数据
|
||||
# 注意:--ids 是 API issue id,不是网页 URL 中的 Issue 编号。
|
||||
gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 --status-id 3 --priority-id 2 --dry-run
|
||||
|
||||
# 危险批量删除必须先 dry-run,真实执行还要显式 --yes
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --dry-run
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --yes
|
||||
|
||||
# 按 Issue 编号批量重开已关闭的 Issue
|
||||
gitlink-cli issue +batch-reopen --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
|
||||
gitlink-cli issue +batch-reopen --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# 按 API issue id 批量添加/移除标签(需要数字标签 ID)
|
||||
gitlink-cli issue +batch-label --owner Gitlink --repo forgeplus --ids 101,102 --add 1,2 --dry-run
|
||||
gitlink-cli issue +batch-label --owner Gitlink --repo forgeplus --ids 101,102 --remove 3
|
||||
|
||||
# 按 API issue id 批量指派/取消指派负责人(需要数字用户 ID)
|
||||
gitlink-cli issue +batch-assign --owner Gitlink --repo forgeplus --ids 101,102 --add 5,6 --dry-run
|
||||
gitlink-cli issue +batch-assign --owner Gitlink --repo forgeplus --ids 101,102 --remove 7
|
||||
|
||||
# 按 Issue 编号批量添加评论
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 123,124 --message "批量更新通知" --dry-run
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --from issues.csv --message "已处理"
|
||||
|
||||
# 导出 Issue 到 CSV 或 JSON 文件(支持筛选)
|
||||
gitlink-cli issue +batch-export --owner Gitlink --repo forgeplus --state open --format csv --output issues.csv
|
||||
gitlink-cli issue +batch-export --owner Gitlink --repo forgeplus --state closed --keyword bug --format json --output closed_issues.json
|
||||
|
||||
# 从 CSV 文件批量导入 Issue
|
||||
gitlink-cli issue +batch-import --owner Gitlink --repo forgeplus --from new_issues.csv --dry-run
|
||||
gitlink-cli issue +batch-import --owner Gitlink --repo forgeplus --from new_issues.csv
|
||||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复"
|
||||
|
||||
|
|
@ -476,16 +415,6 @@ gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.
|
|||
# 查看 Release
|
||||
gitlink-cli release +view --owner Gitlink --repo forgeplus -i <version_id>
|
||||
|
||||
# 列出 Release 已绑定的资产
|
||||
gitlink-cli release +assets --owner Gitlink --repo forgeplus -i <version_id>
|
||||
|
||||
# 绑定或移除已有附件 ID,同时保留 Release 其他字段
|
||||
gitlink-cli release +attach --owner Gitlink --repo forgeplus -i <version_id> --attachment-ids 12,34 --dry-run
|
||||
gitlink-cli release +detach --owner Gitlink --repo forgeplus -i <version_id> --attachment-ids 34 --dry-run
|
||||
|
||||
# 上传本地文件并一步绑定到 Release
|
||||
gitlink-cli release +upload --owner Gitlink --repo forgeplus -i <version_id> --file dist/gitlink-cli_linux_amd64.tar.gz --asset-name gitlink-cli-linux-amd64.tar.gz --description "Linux 二进制包" --dry-run
|
||||
|
||||
# 获取编辑数据并保留未传字段更新
|
||||
gitlink-cli release +edit --owner Gitlink --repo forgeplus -i <version_id>
|
||||
gitlink-cli release +update --owner Gitlink --repo forgeplus -i <version_id> -b "更新后的内容" --dry-run
|
||||
|
|
@ -516,16 +445,6 @@ gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow
|
|||
gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run
|
||||
```
|
||||
|
||||
### 忽略文件模板
|
||||
|
||||
```bash
|
||||
# 列出所有可用的 .gitignore 模板
|
||||
gitlink-cli ignore +list
|
||||
|
||||
# 按名称筛选模板
|
||||
gitlink-cli ignore +list --name Go
|
||||
```
|
||||
|
||||
### 搜索
|
||||
|
||||
```bash
|
||||
|
|
@ -536,50 +455,19 @@ gitlink-cli search +repos -k "machine learning"
|
|||
gitlink-cli search +users -k "zhangsan"
|
||||
```
|
||||
|
||||
### 用户画像
|
||||
|
||||
`profile` 暴露 GitLink 原生的用户画像统计(开发能力、角色定位、专业定位、近期活动、贡献热力图)。
|
||||
省略 `--user` 时默认使用当前认证用户。
|
||||
### 反馈建议
|
||||
|
||||
```bash
|
||||
# 开发能力评分 + 语言分布
|
||||
gitlink-cli profile +ability --user zhangsan
|
||||
# 预览反馈提交,不调用 API
|
||||
gitlink-cli feedback +create --content "CLI 安装文档需要补充 Windows 说明。" --category docs --dry-run
|
||||
|
||||
# 角色定位 / 专业(学科)定位
|
||||
gitlink-cli profile +role --user zhangsan
|
||||
gitlink-cli profile +major --user zhangsan
|
||||
# 从文件提交较长反馈,并附带联系方式和相关仓库
|
||||
gitlink-cli feedback +create --from feedback.md --category cli --contact mengz@example.com --repo-ref Gitlink/gitlink-cli
|
||||
|
||||
# 指定时间范围的开发能力(Unix 时间戳)
|
||||
gitlink-cli profile +ability --user zhangsan --start-time 1704067200 --end-time 1735689600
|
||||
|
||||
# 当前用户的近期活动(每日 疑修 / 合并请求 / 提交)
|
||||
gitlink-cli profile +activity
|
||||
|
||||
# 指定年份的贡献热力图
|
||||
gitlink-cli profile +contribution --user zhangsan --year 2025
|
||||
# 从管道读取反馈内容
|
||||
Get-Content feedback.md | gitlink-cli feedback +create --stdin --category feature
|
||||
```
|
||||
|
||||
### 数据集
|
||||
|
||||
`dataset` 管理并查询 GitLink 科研数据集(标题、描述、论文内容、许可证、所属项目)。
|
||||
|
||||
```bash
|
||||
# 按数字项目 ID 列出一个或多个项目的数据集
|
||||
gitlink-cli dataset +list --ids 5988
|
||||
|
||||
# 查看仓库的数据集及其附件
|
||||
gitlink-cli dataset +view --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 / 更新仓库数据集(先用 --dry-run 预览)
|
||||
gitlink-cli dataset +create --owner me --repo proj -t "我的数据集" -d "..." --license-id 359 --dry-run
|
||||
gitlink-cli dataset +update --owner me --repo proj -t "我的数据集" -d "更新"
|
||||
|
||||
# 删除数据集附件(破坏性:先预览,再用 --yes 确认)
|
||||
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --dry-run
|
||||
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --yes
|
||||
```
|
||||
|
||||
> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 遵循已发布的 OpenAPI 契约,但生产环境尚未部署(当前返回 404),待平台上线后即可生效。
|
||||
### Raw API
|
||||
|
||||
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:
|
||||
|
|
@ -636,13 +524,13 @@ git push gitlink
|
|||
|
||||
`skills/` 目录包含 Claude Code Agent Skill 文件,支持 AI 自动化操作 GitLink 平台。
|
||||
|
||||
详见 [skills/README.md](./skills/README.md)
|
||||
详见 [skills/README.md](skills/README.md)
|
||||
|
||||
| Skill | 说明 |
|
||||
|-------|------|
|
||||
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
|
||||
| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、批量更新/删除、评论等) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
|
||||
| `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) |
|
||||
| `gitlink-member` | 仓库成员与邀请链接管理 |
|
||||
| `gitlink-release` | 发布管理(创建、编辑、更新、查看、删除等) |
|
||||
|
|
@ -704,7 +592,7 @@ gitlink-cli/
|
|||
|
||||
## 文档
|
||||
|
||||
- [Skills 使用指南](./skills/README.md) — AI Agent Skills 详细说明
|
||||
- [Skills 使用指南](skills/README.md) — AI Agent Skills 详细说明
|
||||
- [设计文档](doc/design.md) — 架构设计和开发计划
|
||||
|
||||
## 常见问题
|
||||
|
|
@ -767,7 +655,7 @@ gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Creden
|
|||
|
||||
### Q: 如何查看完整的 API 参考?
|
||||
|
||||
查看 [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/references/api-reference.md)
|
||||
查看 [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# Feedback shortcut
|
||||
|
||||
This change adds a dedicated `feedback` shortcut group for submitting GitLink platform feedback from the CLI.
|
||||
|
||||
New command:
|
||||
|
||||
- `feedback +create`
|
||||
|
||||
The command wraps `POST /api/v1/{owner}/feedbacks.json` and improves the CLI experience around the narrow API payload:
|
||||
|
||||
- Resolves the current authenticated user with `GET /users/me` when `--user` is omitted.
|
||||
- Accepts feedback text from `--content`, `--from`, and `--stdin`, combining multiple sources with blank lines.
|
||||
- Adds optional metadata lines for `--category`, `--contact`, and `--repo-ref` before the body.
|
||||
- Supports `--dry-run` to preview method, path, payload, and content length without submitting.
|
||||
- Rejects empty feedback before making any API request.
|
||||
|
||||
Documentation was added to README, README.zh-CN, and `skills/gitlink-feedback/SKILL.md`.
|
||||
|
||||
Verification:
|
||||
|
||||
- `go test ./shortcuts/feedback`
|
||||
- `go test ./shortcuts`
|
||||
- `go test ./...`
|
||||
- `go build ./...`
|
||||
- `git diff --check`
|
||||
- UTF-8 mojibake scan on touched files
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package feedback
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var feedbackInput io.Reader = os.Stdin
|
||||
|
||||
// Shortcuts returns GitLink platform feedback shortcuts.
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "create",
|
||||
Description: "Submit feedback or suggestions to GitLink",
|
||||
Flags: []common.Flag{
|
||||
{Name: "user", Short: "u", Usage: "GitLink user login. Defaults to current authenticated user"},
|
||||
{Name: "content", Short: "c", Usage: "Feedback content"},
|
||||
{Name: "from", Short: "f", Usage: "Read feedback content from a text file"},
|
||||
{Name: "stdin", Usage: "Read feedback content from standard input", Bool: true, Default: "false"},
|
||||
{Name: "category", Usage: "Optional feedback category, for example bug, feature, docs, ux, or cli"},
|
||||
{Name: "contact", Usage: "Optional contact information to include in the feedback"},
|
||||
{Name: "repo-ref", Usage: "Optional related repository in owner/repo form"},
|
||||
{Name: "dry-run", Usage: "Preview the request without submitting feedback", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runCreate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runCreate(ctx *common.RuntimeContext) error {
|
||||
user, err := resolveFeedbackUser(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := buildFeedbackContent(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{"content": content}
|
||||
path := feedbackPath(user)
|
||||
if parseFeedbackBool(ctx.Arg("dry-run")) {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"dry_run": true,
|
||||
"method": "POST",
|
||||
"path": path,
|
||||
"user": user,
|
||||
"content_length": len(content),
|
||||
"body": payload,
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", path, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func resolveFeedbackUser(ctx *common.RuntimeContext) (string, error) {
|
||||
if user := strings.TrimSpace(ctx.Arg("user")); user != "" {
|
||||
return user, nil
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", "/users/me", nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cannot determine current user login")
|
||||
}
|
||||
login, _ := data["login"].(string)
|
||||
login = strings.TrimSpace(login)
|
||||
if login == "" {
|
||||
return "", fmt.Errorf("cannot determine current user login")
|
||||
}
|
||||
return login, nil
|
||||
}
|
||||
|
||||
func buildFeedbackContent(ctx *common.RuntimeContext) (string, error) {
|
||||
parts := make([]string, 0, 3)
|
||||
if content := strings.TrimSpace(ctx.Arg("content")); content != "" {
|
||||
parts = append(parts, content)
|
||||
}
|
||||
if path := strings.TrimSpace(ctx.Arg("from")); path != "" {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read --from: %w", err)
|
||||
}
|
||||
if text := strings.TrimSpace(string(content)); text != "" {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
}
|
||||
if parseFeedbackBool(ctx.Arg("stdin")) {
|
||||
content, err := io.ReadAll(feedbackInput)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read --stdin: %w", err)
|
||||
}
|
||||
if text := strings.TrimSpace(string(content)); text != "" {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", fmt.Errorf("provide feedback content with --content, --from, or --stdin")
|
||||
}
|
||||
|
||||
metadata := feedbackMetadata(ctx)
|
||||
body := strings.Join(parts, "\n\n")
|
||||
if len(metadata) == 0 {
|
||||
return body, nil
|
||||
}
|
||||
return strings.Join(append(metadata, "", body), "\n"), nil
|
||||
}
|
||||
|
||||
func feedbackMetadata(ctx *common.RuntimeContext) []string {
|
||||
var lines []string
|
||||
if category := strings.TrimSpace(ctx.Arg("category")); category != "" {
|
||||
lines = append(lines, "Category: "+category)
|
||||
}
|
||||
if repoRef := strings.TrimSpace(ctx.Arg("repo-ref")); repoRef != "" {
|
||||
lines = append(lines, "Repository: "+repoRef)
|
||||
} else if strings.TrimSpace(ctx.Owner) != "" && strings.TrimSpace(ctx.Repo) != "" {
|
||||
lines = append(lines, fmt.Sprintf("Repository: %s/%s", strings.TrimSpace(ctx.Owner), strings.TrimSpace(ctx.Repo)))
|
||||
}
|
||||
if contact := strings.TrimSpace(ctx.Arg("contact")); contact != "" {
|
||||
lines = append(lines, "Contact: "+contact)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func feedbackPath(user string) string {
|
||||
return fmt.Sprintf("/v1/%s/feedbacks", url.PathEscape(user))
|
||||
}
|
||||
|
||||
func parseFeedbackBool(value string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(value), "true")
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
package feedback
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestFeedbackCreateWithExplicitUser(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newFeedbackServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertFeedbackRequest(t, r, "POST", "/v1/Mengz/feedbacks.json")
|
||||
payload = decodeFeedbackJSON(t, r)
|
||||
writeFeedbackJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runFeedbackShortcut(t, server, "create", map[string]string{
|
||||
"user": "Mengz",
|
||||
"content": "The CLI should support feedback.",
|
||||
"category": "feature",
|
||||
"contact": "mengz@example.com",
|
||||
"repo-ref": "Gitlink/gitlink-cli",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("feedback create failed: %v", err)
|
||||
}
|
||||
content, _ := payload["content"].(string)
|
||||
for _, want := range []string{
|
||||
"Category: feature",
|
||||
"Repository: Gitlink/gitlink-cli",
|
||||
"Contact: mengz@example.com",
|
||||
"The CLI should support feedback.",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("content missing %q: %q", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedbackCreateDefaultsToCurrentUser(t *testing.T) {
|
||||
var paths []string
|
||||
server := newFeedbackServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
paths = append(paths, r.URL.Path)
|
||||
switch r.URL.Path {
|
||||
case "/users/me.json":
|
||||
assertFeedbackRequest(t, r, "GET", "/users/me.json")
|
||||
writeFeedbackJSON(t, w, map[string]interface{}{"login": "Mengz"})
|
||||
case "/v1/Mengz/feedbacks.json":
|
||||
assertFeedbackRequest(t, r, "POST", "/v1/Mengz/feedbacks.json")
|
||||
writeFeedbackJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runFeedbackShortcut(t, server, "create", map[string]string{"content": "Feedback body"})
|
||||
if err != nil {
|
||||
t.Fatalf("feedback create failed: %v", err)
|
||||
}
|
||||
want := []string{"/users/me.json", "/v1/Mengz/feedbacks.json"}
|
||||
if strings.Join(paths, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("paths = %v, want %v", paths, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedbackCreateReadsFileAndStdin(t *testing.T) {
|
||||
oldInput := feedbackInput
|
||||
feedbackInput = strings.NewReader("stdin details\n")
|
||||
defer func() { feedbackInput = oldInput }()
|
||||
|
||||
var payload map[string]interface{}
|
||||
server := newFeedbackServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertFeedbackRequest(t, r, "POST", "/v1/Mengz/feedbacks.json")
|
||||
payload = decodeFeedbackJSON(t, r)
|
||||
writeFeedbackJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "feedback.txt")
|
||||
if err := os.WriteFile(path, []byte("file details\n"), 0o600); err != nil {
|
||||
t.Fatalf("write feedback file: %v", err)
|
||||
}
|
||||
err := runFeedbackShortcut(t, server, "create", map[string]string{
|
||||
"user": "Mengz",
|
||||
"from": path,
|
||||
"stdin": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("feedback create failed: %v", err)
|
||||
}
|
||||
content, _ := payload["content"].(string)
|
||||
if !strings.Contains(content, "file details") || !strings.Contains(content, "stdin details") {
|
||||
t.Fatalf("content = %q, want file and stdin details", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedbackCreateDryRunDoesNotCallSubmitAPI(t *testing.T) {
|
||||
server := newFeedbackServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runFeedbackShortcut(t, server, "create", map[string]string{
|
||||
"user": "Mengz",
|
||||
"content": "Preview this feedback",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("feedback dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedbackCreateRejectsEmptyContentBeforeAPI(t *testing.T) {
|
||||
server := newFeedbackServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("empty content should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runFeedbackShortcut(t, server, "create", map[string]string{"user": "Mengz"})
|
||||
if err == nil {
|
||||
t.Fatal("expected empty feedback to return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFeedbackContentAddsRepositoryFromContext(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{
|
||||
Owner: "Gitlink",
|
||||
Repo: "gitlink-cli",
|
||||
Args: map[string]string{"content": "Feedback body"},
|
||||
}
|
||||
content, err := buildFeedbackContent(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("buildFeedbackContent failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(content, "Repository: Gitlink/gitlink-cli") {
|
||||
t.Fatalf("content = %q, want repository metadata", content)
|
||||
}
|
||||
}
|
||||
|
||||
func runFeedbackShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findFeedbackShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{
|
||||
HTTP: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
},
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
if ctx.Args == nil {
|
||||
ctx.Args = map[string]string{}
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
|
||||
func findFeedbackShortcut(t *testing.T, name string) *common.Shortcut {
|
||||
t.Helper()
|
||||
for _, shortcut := range Shortcuts() {
|
||||
if shortcut.Name == name {
|
||||
return shortcut
|
||||
}
|
||||
}
|
||||
t.Fatalf("shortcut %q not found", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFeedbackServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
func assertFeedbackRequest(t *testing.T, r *http.Request, method, path string) {
|
||||
t.Helper()
|
||||
if r.Method != method || r.URL.Path != path {
|
||||
t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeFeedbackJSON(t *testing.T, r *http.Request) map[string]interface{} {
|
||||
t.Helper()
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode request body: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func writeFeedbackJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
t.Fatalf("write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/ci"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/compare"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/feedback"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/health"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/issue"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/label"
|
||||
|
|
@ -49,6 +50,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"search": search.Shortcuts(tr),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"feedback": feedback.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(tr),
|
||||
"health": health.Shortcuts(tr),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
|
|
@ -71,6 +73,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"search": tr.T("cmd.search.short"),
|
||||
"ci": tr.T("cmd.ci.short"),
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"feedback": "Submit feedback and suggestions",
|
||||
"webhook": tr.T("cmd.webhook.short"),
|
||||
"health": "Project health data collection",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ func TestRegisterAll(t *testing.T) {
|
|||
RegisterAll(root)
|
||||
|
||||
expectedGroups := []string{
|
||||
"attachment", "repo", "issue", "label", "license", "pr", "release", "branch",
|
||||
"repo", "issue", "label", "license", "pr", "release", "branch",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"health", "message",
|
||||
"health", "feedback",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
name: gitlink-feedback
|
||||
description: "GitLink 反馈建议提交:从命令行提交平台问题、改进建议或 CLI 使用反馈,支持文件、stdin、dry-run 和上下文元数据。"
|
||||
metadata:
|
||||
cliHelp: "gitlink-cli feedback --help"
|
||||
---
|
||||
|
||||
# gitlink-feedback
|
||||
|
||||
当用户需要向 GitLink 平台提交问题反馈、体验建议或 CLI 改进意见时使用本 Skill。底层接口只接收 `content`,CLI 会把分类、联系方式和相关仓库作为文本元数据拼入反馈正文,便于平台侧处理。
|
||||
|
||||
## 常用命令
|
||||
|
||||
| 命令 | 用途 |
|
||||
|------|------|
|
||||
| `feedback +create` | 提交反馈建议 |
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
# 直接提交简短反馈
|
||||
gitlink-cli feedback +create --content "CLI 安装文档需要补充 Windows 说明。" --category docs
|
||||
|
||||
# 提交前预览请求路径、正文长度和 payload
|
||||
gitlink-cli feedback +create --content "希望支持更多输出格式。" --category feature --dry-run
|
||||
|
||||
# 从文件读取长反馈
|
||||
gitlink-cli feedback +create --from feedback.md --category cli --contact mengz@example.com
|
||||
|
||||
# 从管道读取反馈
|
||||
Get-Content feedback.md | gitlink-cli feedback +create --stdin --category ux --repo-ref Gitlink/gitlink-cli
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--user` / `-u` | GitLink 用户标识;不传时读取当前登录用户 |
|
||||
| `--content` / `-c` | 直接传入反馈正文 |
|
||||
| `--from` / `-f` | 从文本文件读取反馈正文 |
|
||||
| `--stdin` | 从标准输入读取反馈正文 |
|
||||
| `--category` | 可选分类,如 `bug`、`feature`、`docs`、`ux`、`cli` |
|
||||
| `--contact` | 可选联系方式,会写入反馈正文 |
|
||||
| `--repo-ref` | 可选相关仓库,格式建议为 `owner/repo` |
|
||||
| `--dry-run` | 只预览请求,不提交反馈 |
|
||||
|
||||
## 安全规则
|
||||
|
||||
- 反馈内容可能进入平台工单或日志,不要提交 Token、Cookie、密码、私钥等敏感信息。
|
||||
- 需要提交较长复现信息时优先使用 `--from`,并先用 `--dry-run` 检查最终内容。
|
||||
- `--contact` 是明文写入反馈正文,只填写愿意公开给平台维护者的信息。
|
||||
- 反馈涉及具体仓库时传 `--repo-ref owner/repo`,不要把仓库上下文混在正文里导致平台侧难以分拣。
|
||||
Loading…
Reference in New Issue