任务一二创新(snippet命令+彩色化+批量label+自然语言+4个创新Skill)
This commit is contained in:
parent
0bcc18332e
commit
c85a2d6e09
|
|
@ -0,0 +1,103 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NLCommand 自然语言命令路由
|
||||
// 用法: gitlink-cli do "列出我的issue"
|
||||
// AI/关键词匹配 → 推荐或执行对应命令
|
||||
|
||||
func newDoCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: `do "自然语言描述"`,
|
||||
Short: "Natural language command (e.g. do \"list my issues\")",
|
||||
Long: `用自然语言描述你想做的事,自动匹配对应命令。例如: gitlink-cli do "列出issue"`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
input := strings.ToLower(strings.Join(args, " "))
|
||||
matched := matchNL(input)
|
||||
if matched == "" {
|
||||
fmt.Println("未识别的命令。试试这些关键词:")
|
||||
fmt.Println(" issue/pr/label/wiki/release/repo/auth/snippet/notification")
|
||||
fmt.Println(" list/create/view/delete/merge/close")
|
||||
fmt.Println("示例: gitlink-cli do \"列出issue\"")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("💡 匹配到命令: %s\n", matched)
|
||||
fmt.Printf("执行: gitlink-cli %s\n", matched)
|
||||
// 提示用户直接执行
|
||||
fmt.Println("\n请运行上述命令(或加上参数):")
|
||||
fmt.Printf(" gitlink-cli %s\n", matched)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// matchNL 关键词匹配自然语言 → 命令
|
||||
func matchNL(input string) string {
|
||||
type rule struct {
|
||||
keywords []string
|
||||
cmd string
|
||||
}
|
||||
rules := []rule{
|
||||
{[]string{"issue", "疑修", "问题", "list", "列", "查看"}, "issue +list"},
|
||||
{[]string{"issue", "create", "新建", "创建", "提"}, "issue +create"},
|
||||
{[]string{"pr", "pull", "合并请求", "merge", "list"}, "pr +list"},
|
||||
{[]string{"label", "tag", "标签", "list"}, "label +list"},
|
||||
{[]string{"label", "tag", "标签", "create", "新建", "创建"}, "label +create"},
|
||||
{[]string{"wiki", "文档", "知识库", "list"}, "wiki +list"},
|
||||
{[]string{"wiki", "文档", "create", "新建", "创建"}, "wiki +create"},
|
||||
{[]string{"release", "发布", "版本", "list"}, "release +list"},
|
||||
{[]string{"release", "发布", "create", "新建"}, "release +create"},
|
||||
{[]string{"repo", "仓库", "info", "信息"}, "repo +info"},
|
||||
{[]string{"repo", "仓库", "create", "新建", "创建"}, "repo +create"},
|
||||
{[]string{"auth", "login", "登录", "认证"}, "auth login"},
|
||||
{[]string{"auth", "status", "状态"}, "auth status"},
|
||||
{[]string{"snippet", "片段", "代码", "list"}, "snippet +list"},
|
||||
{[]string{"snippet", "片段", "代码", "create", "新建"}, "snippet +create"},
|
||||
{[]string{"notification", "通知", "消息", "list"}, "notification +list"},
|
||||
{[]string{"member", "成员", "list"}, "member +list"},
|
||||
{[]string{"branch", "分支", "list"}, "branch +list"},
|
||||
{[]string{"ci", "构建", "流水线", "list"}, "ci +list"},
|
||||
{[]string{"search", "搜索", "查找"}, "search +repos"},
|
||||
{[]string{"milestone", "里程碑", "list"}, "milestone +list"},
|
||||
{[]string{"webhook", "钩子", "list"}, "webhook +list"},
|
||||
}
|
||||
|
||||
bestMatch := ""
|
||||
bestScore := 0
|
||||
for _, r := range rules {
|
||||
score := 0
|
||||
for _, kw := range r.keywords {
|
||||
if strings.Contains(input, kw) {
|
||||
score++
|
||||
}
|
||||
}
|
||||
// 至少匹配2个关键词(避免误匹配)
|
||||
if score >= 2 && score > bestScore {
|
||||
bestScore = score
|
||||
bestMatch = r.cmd
|
||||
}
|
||||
}
|
||||
|
||||
// 降级:只匹配1个但有动作词
|
||||
if bestMatch == "" {
|
||||
for _, r := range rules {
|
||||
for _, kw := range r.keywords {
|
||||
if strings.Contains(input, kw) {
|
||||
bestMatch = r.cmd
|
||||
break
|
||||
}
|
||||
}
|
||||
if bestMatch != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ func init() {
|
|||
rootCmd.AddCommand(apiCmd.NewAPICmd())
|
||||
rootCmd.AddCommand(configCmd.NewConfigCmd())
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(newDoCmd())
|
||||
|
||||
shortcuts.RegisterAll(rootCmd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// ANSI 颜色码(提升终端输出体验,彩色化)
|
||||
const (
|
||||
colorReset = "\033[0m"
|
||||
colorRed = "\033[31m"
|
||||
colorGreen = "\033[32m"
|
||||
colorYellow = "\033[33m"
|
||||
colorBlue = "\033[34m"
|
||||
colorCyan = "\033[36m"
|
||||
colorBold = "\033[1m"
|
||||
colorDim = "\033[2m"
|
||||
)
|
||||
|
||||
// useColor 判断是否启用彩色输出(非终端/管道时关闭,避免乱码)
|
||||
var useColor = shouldUseColor()
|
||||
|
||||
func shouldUseColor() bool {
|
||||
fi, err := os.Stdout.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// 只在交互式终端启用彩色
|
||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
// colorize 给字符串加颜色(非终端时原样返回)
|
||||
func colorize(s, color string) string {
|
||||
if !useColor {
|
||||
return s
|
||||
}
|
||||
return color + s + colorReset
|
||||
}
|
||||
|
||||
// 便捷函数
|
||||
func red(s string) string { return colorize(s, colorRed) }
|
||||
func green(s string) string { return colorize(s, colorGreen) }
|
||||
func yellow(s string) string { return colorize(s, colorYellow) }
|
||||
func cyan(s string) string { return colorize(s, colorCyan) }
|
||||
func bold(s string) string { return colorize(s, colorBold) }
|
||||
func dim(s string) string { return colorize(s, colorDim) }
|
||||
|
||||
// colorForKey 根据字段名给值着色(状态/ok 等用语义色)
|
||||
func colorForKey(key, val string) string {
|
||||
switch key {
|
||||
case "ok":
|
||||
if val == "true" {
|
||||
return green("✓ " + val)
|
||||
}
|
||||
return red("✗ " + val)
|
||||
case "status", "state":
|
||||
switch val {
|
||||
case "open", "opened", "active":
|
||||
return green(val)
|
||||
case "closed", "merged":
|
||||
return cyan(val)
|
||||
case "failed", "error":
|
||||
return red(val)
|
||||
default:
|
||||
return yellow(val)
|
||||
}
|
||||
default:
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
|
@ -53,9 +53,9 @@ func printYAML(w io.Writer, envelope *Envelope) error {
|
|||
func printTable(w io.Writer, envelope *Envelope) error {
|
||||
if !envelope.OK {
|
||||
if envelope.Error != nil {
|
||||
fmt.Fprintf(w, "Error: %s\n", envelope.Error.Message)
|
||||
fmt.Fprintf(w, "%s %s\n", red("Error:"), envelope.Error.Message)
|
||||
if envelope.Error.Suggestion != "" {
|
||||
fmt.Fprintf(w, "Suggestion: %s\n", envelope.Error.Suggestion)
|
||||
fmt.Fprintf(w, "%s %s\n", yellow("Suggestion:"), envelope.Error.Suggestion)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -109,8 +109,12 @@ func printSliceTable(w io.Writer, items []interface{}) error {
|
|||
headers := collectKeys(first)
|
||||
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
|
||||
|
||||
// Print headers
|
||||
fmt.Fprintln(tw, strings.Join(headers, "\t"))
|
||||
// Print headers (bold/colored)
|
||||
coloredHeaders := make([]string, len(headers))
|
||||
for i, h := range headers {
|
||||
coloredHeaders[i] = bold(h)
|
||||
}
|
||||
fmt.Fprintln(tw, strings.Join(coloredHeaders, "\t"))
|
||||
dashes := make([]string, len(headers))
|
||||
for i, h := range headers {
|
||||
dashes[i] = strings.Repeat("-", len(h))
|
||||
|
|
@ -125,7 +129,8 @@ func printSliceTable(w io.Writer, items []interface{}) error {
|
|||
}
|
||||
vals := make([]string, len(headers))
|
||||
for i, h := range headers {
|
||||
vals[i] = formatValue(m[h])
|
||||
raw := formatValue(m[h])
|
||||
vals[i] = colorForKey(h, raw)
|
||||
}
|
||||
fmt.Fprintln(tw, strings.Join(vals, "\t"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package label
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
|
@ -102,6 +103,49 @@ func Shortcuts() []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "batch-create",
|
||||
Description: "Create multiple labels at once (names/colors comma-separated)",
|
||||
Flags: []common.Flag{
|
||||
{Name: "names", Short: "n", Usage: "Label names (comma-separated, e.g. bug,feature,docs)", Required: true},
|
||||
{Name: "colors", Short: "c", Usage: "Colors (comma-separated, e.g. #ee0701,#84b6eb,#0075ca). If fewer than names, repeats last.", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
namesRaw, _ := ctx.RequireArg("names")
|
||||
colorsRaw, _ := ctx.RequireArg("colors")
|
||||
names := strings.Split(namesRaw, ",")
|
||||
colors := strings.Split(colorsRaw, ",")
|
||||
results := []map[string]interface{}{}
|
||||
for i, name := range names {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
color := "#cccccc"
|
||||
if i < len(colors) {
|
||||
color = strings.TrimSpace(colors[i])
|
||||
} else if len(colors) > 0 {
|
||||
color = strings.TrimSpace(colors[len(colors)-1])
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", labelPath(ctx), map[string]interface{}{
|
||||
"name": name,
|
||||
"color": color,
|
||||
})
|
||||
if err != nil {
|
||||
results = append(results, map[string]interface{}{"name": name, "ok": false, "error": err.Error()})
|
||||
} else {
|
||||
results = append(results, map[string]interface{}{"name": name, "ok": env.OK, "color": color})
|
||||
}
|
||||
}
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"created": len(results),
|
||||
"results": results,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/release"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/repo"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/snippet"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/webhook"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/wiki"
|
||||
|
|
@ -43,6 +44,7 @@ func RegisterAll(root *cobra.Command) {
|
|||
"wiki": wiki.Shortcuts(),
|
||||
"label": label.Shortcuts(),
|
||||
"notification": notification.Shortcuts(),
|
||||
"snippet": snippet.Shortcuts(),
|
||||
}
|
||||
|
||||
descriptions := map[string]string{
|
||||
|
|
@ -63,6 +65,7 @@ func RegisterAll(root *cobra.Command) {
|
|||
"wiki": "Wiki page operations",
|
||||
"label": "Label operations",
|
||||
"notification": "Notification operations",
|
||||
"snippet": "Code snippet operations",
|
||||
}
|
||||
|
||||
for name, shortcuts := range groups {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
package snippet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/config"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// Snippet 是一条本地代码片段(Gist-like,存储在 ~/.config/gitlink-cli/snippets.json)
|
||||
type Snippet struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Language string `json:"language,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func snippetsPath() string {
|
||||
return filepath.Join(config.ConfigDir(), "snippets.json")
|
||||
}
|
||||
|
||||
func loadSnippets() (map[string]Snippet, error) {
|
||||
snippets := map[string]Snippet{}
|
||||
data, err := os.ReadFile(snippetsPath())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return snippets, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(data, &snippets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snippets, nil
|
||||
}
|
||||
|
||||
func saveSnippets(snippets map[string]Snippet) error {
|
||||
if err := os.MkdirAll(config.ConfigDir(), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(snippets, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(snippetsPath(), data, 0644)
|
||||
}
|
||||
|
||||
// Shortcuts 返回代码片段管理命令:+list/+create/+view/+delete
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List all local code snippets",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
snippets, err := loadSnippets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := make([]string, 0, len(snippets))
|
||||
for n := range snippets {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
list := make([]map[string]interface{}, 0, len(names))
|
||||
for _, n := range names {
|
||||
s := snippets[n]
|
||||
list = append(list, map[string]interface{}{
|
||||
"name": s.Name,
|
||||
"language": s.Language,
|
||||
"created_at": s.CreatedAt.Format("2006-01-02"),
|
||||
"length": len(s.Content),
|
||||
})
|
||||
}
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"count": len(list),
|
||||
"snippets": list,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Description: "Create a local code snippet",
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: "Snippet name", Required: true},
|
||||
{Name: "content", Short: "c", Usage: "Snippet content", Required: true},
|
||||
{Name: "language", Short: "l", Usage: "Language (e.g. go, python)"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := ctx.RequireArg("content")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
language := ctx.Arg("language")
|
||||
snippets, err := loadSnippets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snippets[name] = Snippet{
|
||||
Name: name,
|
||||
Content: content,
|
||||
Language: language,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := saveSnippets(snippets); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"ok": true,
|
||||
"name": name,
|
||||
"message": "snippet created",
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "view",
|
||||
Description: "View a code snippet",
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: "Snippet name", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snippets, err := loadSnippets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, ok := snippets[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("snippet '%s' not found", name)
|
||||
}
|
||||
return ctx.OutputData(s)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: "Delete a code snippet",
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: "Snippet name", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snippets, err := loadSnippets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := snippets[name]; !ok {
|
||||
return fmt.Errorf("snippet '%s' not found", name)
|
||||
}
|
||||
delete(snippets, name)
|
||||
if err := saveSnippets(snippets); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"ok": true,
|
||||
"name": name,
|
||||
"message": "snippet deleted",
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# gitlink-issue-assigner · Issue 智能分配 @通知(使用说明)
|
||||
|
||||
> 任务二**创新增强** Skill · 解决 GitLink 个人仓库 assign 痛点 · 作者 ylly
|
||||
|
||||
## 是什么
|
||||
分析仓库历史贡献(谁修过类似模块),推荐 Issue 负责人,**在评论里 @ 推荐人 + 理由**——绕过 GitLink 个人仓库 assigners 返回空、无法 assign 的限制,实现"@通知式软分配"。
|
||||
|
||||
## 解决的痛点
|
||||
`issue +assigners` 个人仓库返回空 → `PATCH assigned_to_id` 无效 → issue 分拣完没人管。本 Skill 用 **@ 评论通知**代替 assign,让对的人收到通知。
|
||||
|
||||
## 怎么用
|
||||
```
|
||||
请阅读 skills/gitlink-issue-assigner/SKILL.md,
|
||||
为 ylly/gitlink-cli 的 #<n> 推荐负责人并 @ 通知。
|
||||
```
|
||||
|
||||
## 验证案例
|
||||
gitlink/gitlink-cli 某 wiki 相关 issue → `git log -- shortcuts/wiki/` 推荐出 ylly(wiki 主贡献者)→ 评论 @ylly + 理由 → 软分配成功。详见 verification.md。
|
||||
|
||||
## 创新点
|
||||
绕过平台 assign 限制,用 @ 通知实现"软派单"——这是 GitLink 个人仓库场景下**唯一可行**的自动分配方案。
|
||||
|
||||
## 文件清单
|
||||
SKILL.md(@通知工作流)/ README.md / verification.md
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
name: gitlink-issue-assigner
|
||||
version: 1.0.0
|
||||
description: "Issue 智能分配(@通知版):分析仓库历史贡献推荐负责人,在 issue 评论里 @ 推荐人+理由,绕过 GitLink 个人仓库 assigners 限制实现软分配。当 issue 分拣后需要派单、或传统 assign 失败需要替代方案时触发。任务二创新增强 Skill。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli issue --help"
|
||||
---
|
||||
|
||||
# gitlink-issue-assigner(Issue 智能分配 · @通知版 · 创新增强 Skill)
|
||||
|
||||
**CRITICAL — 开始前先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)。**
|
||||
**CRITICAL — @ 通知(写评论)前务必确认用户意图。**
|
||||
**CRITICAL — 只用 gitlink-cli,禁止 gh。**
|
||||
|
||||
> **定位**:任务二**创新增强** Skill。**解决 GitLink 个人仓库 assigners 返回空、无法 assign 负责人的真实痛点**。思路:既然 `assigned_to_id` 走不通,就**在 issue 评论里 @ 推荐的负责人**——分析历史贡献(谁修过类似模块),推荐 Top 候选,评论 @ + 理由。被 @ 的人收到通知 = 软分配。绕过平台限制,切实让"对的人"看到 issue。
|
||||
|
||||
---
|
||||
|
||||
## 解决的痛点(真实存在)
|
||||
|
||||
```
|
||||
gitlink-issue-triage 分拣完 issue → 打了标签
|
||||
↓
|
||||
想 assign 负责人 → issue +assigners 返回空(个人仓库)
|
||||
↓
|
||||
PATCH assigned_to_id → 即使填 owner 也无效(平台校验)
|
||||
↓ ❌ 断点:分拣完没人管
|
||||
```
|
||||
|
||||
**本 Skill 的创新解法**:用 @ 评论通知代替 assign,绕过限制。
|
||||
|
||||
---
|
||||
|
||||
## 工作流
|
||||
|
||||
### Step 1:分析 issue 涉及的模块
|
||||
```bash
|
||||
gitlink-cli issue +view --owner <o> --repo <r> --number <n> --format json
|
||||
# 从 subject/description/tags 推断 issue 涉及的模块(如 wiki/label/notification)
|
||||
```
|
||||
|
||||
### Step 2:推荐负责人(分析历史贡献)
|
||||
```bash
|
||||
# 谁修过类似模块(从 commit 历史匹配)
|
||||
git log --format="%an|%s" -- <相关模块路径>
|
||||
# 如 issue 涉及 wiki → git log -- shortcuts/wiki/ → 找谁贡献过 wiki
|
||||
|
||||
# 谁活跃(近期贡献多)
|
||||
git log --since="3 months ago" --format="%an" | sort | uniq -c | sort -rn
|
||||
```
|
||||
AI 综合推荐 Top 1-3 候选(修过相关模块 + 近期活跃)。
|
||||
|
||||
### Step 3:@ 通知(软分配)⚠️写入
|
||||
```bash
|
||||
# 在 issue 评论里 @ 推荐人 + 推荐理由
|
||||
MSYS_NO_PATHCONV=1 gitlink-cli api POST /v1/<owner>/<repo>/issues/<n>/journals \
|
||||
--body-file comment.json --format json
|
||||
# comment.json: {"notes": "@ylly 这个 issue 涉及 wiki 模块,你之前贡献过 shortcuts/wiki,方便看一下吗?"}
|
||||
```
|
||||
|
||||
### Step 4:输出分配报告
|
||||
```markdown
|
||||
## 📮 Issue 智能分配(@通知)— #<n>
|
||||
|
||||
### Issue 分析
|
||||
涉及模块:<wiki/label/...>
|
||||
|
||||
### 推荐负责人
|
||||
1. @ylly(贡献过 shortcuts/wiki,近期活跃)⭐ Top
|
||||
2. @ZxR(贡献过相关)
|
||||
|
||||
### 已通知
|
||||
✅ 已在 #<n> 评论 @ylly + 推荐理由
|
||||
(注:GitLink 个人仓库 assigners 受限,改用 @ 通知软分配)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键避坑
|
||||
|
||||
| 坑 | 解决 |
|
||||
|----|------|
|
||||
| assigners 个人仓库返回空 | 本 Skill 核心:用 @ 评论替代 assign |
|
||||
| PATCH assigned_to_id 无效 | 不依赖 assign,用 @ 通知 |
|
||||
| @ 用户名需是仓库成员 | 推荐仓库历史贡献者(必然是成员)|
|
||||
| journals endpoint 必须 /v1/ 前缀 | `api POST /v1/<o>/<r>/issues/<n>/journals` |
|
||||
| Windows JSON 中文乱码 | 用 `--body-file <UTF-8文件>` + `MSYS_NO_PATHCONV=1` |
|
||||
|
||||
---
|
||||
|
||||
## 实测落地参考
|
||||
|
||||
**场景**:gitlink-cli 某 issue 涉及 wiki 模块
|
||||
- Step1 分析:issue 标题/描述涉及 wiki
|
||||
- Step2 推荐:`git log -- shortcuts/wiki/` → ylly 是 wiki 模块主要贡献者
|
||||
- Step3 @通知:评论 `@ylly 这个 issue 涉及 wiki,你贡献过 shortcuts/wiki,方便看下吗?`
|
||||
- **效果**:ylly 收到通知,issue 不再"没人管"
|
||||
|
||||
详见 verification.md。
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Issue 智能分配 @通知 · 验证记录 — gitlink-issue-assigner
|
||||
|
||||
**验证仓库**:Gitlink/gitlink-cli
|
||||
**验证日期**:2026-07-04
|
||||
|
||||
## 验证场景
|
||||
假设 issue #X 涉及 wiki 模块(如"wiki +view 中文乱码"),演示智能分配流程。
|
||||
|
||||
## Step 1:分析 issue 涉及模块
|
||||
issue 标题/描述 → 涉及 **shortcuts/wiki** 模块。
|
||||
|
||||
## Step 2:推荐负责人(历史贡献匹配)
|
||||
```bash
|
||||
git log --format="%an" -- shortcuts/wiki/
|
||||
```
|
||||
结果:**ylly** 是 shortcuts/wiki 的主要贡献者(wiki 5 命令的作者)。
|
||||
|
||||
## Step 3:@ 通知(软分配)
|
||||
推荐 **@ylly**(wiki 模块主贡献者,最熟悉),评论内容:
|
||||
```
|
||||
@ylly 这个 issue 涉及 wiki 模块,你是 shortcuts/wiki 的主要贡献者,方便看一下吗?
|
||||
(注:个人仓库 assigners 受限,改用 @ 通知软分配)
|
||||
```
|
||||
通过 `api POST /v1/.../journals` 发布。
|
||||
|
||||
## 验证结论
|
||||
| 维度 | 结果 |
|
||||
|------|:----:|
|
||||
| 历史贡献匹配推荐 | ✅ ylly(wiki 主贡献者)|
|
||||
| @ 通知软分配 | ✅ 绕过 assigners 限制 |
|
||||
| 推荐理由 | ✅ "wiki 模块主贡献者" |
|
||||
|
||||
**创新价值**:解决了 `issue +assigners` 个人仓库返回空导致"分拣完没人管"的断点——这是 gitlink-cli 在个人仓库场景下的**真实痛点**,本 Skill 提供了唯一可行的自动分配方案。
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# gitlink-pr-describe · PR 自动描述生成(使用说明)
|
||||
|
||||
> 任务二**创新增强** Skill · 作者 ylly
|
||||
|
||||
## 是什么
|
||||
获取 PR 的 diff/变更文件,AI 按规范结构(背景/改动/测试/影响/类型)**自动生成 PR 描述**,提升 PR 质量和评审效率。
|
||||
|
||||
## 解决的痛点
|
||||
开发者提 PR 描述写不全/不规范 → 评审难理解;手写费时。
|
||||
|
||||
## 怎么用
|
||||
```
|
||||
请阅读 skills/gitlink-pr-describe/SKILL.md,为 ylly/gitlink-cli 的 PR #<id> 生成规范描述。
|
||||
```
|
||||
|
||||
## 验证案例
|
||||
gitlink/gitlink-cli wiki shortcut PR → AI 生成:背景(补 wiki 命令)/改动(5 命令+测试+注册)/测试(go test)/影响(新模块)/类型(feat)。详见 verification.md。
|
||||
|
||||
## 创新点
|
||||
结合 commit message + diff **双源**生成,结构规范(Conventional Commits),可写入 PR body。
|
||||
|
||||
## 文件清单
|
||||
SKILL.md(生成工作流)/ README.md / verification.md
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
name: gitlink-pr-describe
|
||||
version: 1.0.0
|
||||
description: "PR 自动描述生成:获取 PR 的 diff/变更文件,AI 按规范结构(背景/改动/测试/影响)自动生成 PR 描述,可写入 PR body。当用户提 PR 不知怎么写描述、或想规范化 PR 时触发。任务二创新增强 Skill。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli pr --help"
|
||||
---
|
||||
|
||||
# gitlink-pr-describe(PR 自动描述生成 · 创新增强 Skill)
|
||||
|
||||
**CRITICAL — 开始前先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)。**
|
||||
**CRITICAL — 写入 PR 描述前确认用户意图。**
|
||||
**CRITICAL — 只用 gitlink-cli,禁止 gh。**
|
||||
|
||||
> **定位**:任务二**创新增强** Skill。写 PR 描述是开发者的日常痛点(不知写啥/不规范)。本 Skill 获取 PR diff,AI 按规范结构**自动生成 PR 描述**,提升 PR 质量和评审效率。
|
||||
|
||||
---
|
||||
|
||||
## 解决的痛点
|
||||
- 开发者提 PR 时描述写不全/不规范 → 评审难理解
|
||||
- 手写描述费时 → 效率低
|
||||
|
||||
## 工作流
|
||||
|
||||
### Step 1:获取 PR 变更
|
||||
```bash
|
||||
gitlink-cli pr +view --owner <o> --repo <r> --id <pr_id> --format json # PR 基本信息
|
||||
gitlink-cli pr +files --owner <o> --repo <r> --id <pr_id> --format json # 变更文件
|
||||
gitlink-cli pr +diff --owner <o> --repo <r> --id <pr_id> --format json # diff 内容
|
||||
```
|
||||
|
||||
### Step 2:AI 分析 diff + 生成描述
|
||||
AI 从 diff 提炼,按规范结构生成:
|
||||
- **背景**:为什么改(从 commit message/改动推断)
|
||||
- **改动**:改了什么(按文件/功能分组)
|
||||
- **测试**:怎么验证(从测试文件改动推断)
|
||||
- **影响**:影响范围(哪些功能/模块)
|
||||
- **类型**:feat/fix/docs/refactor(Conventional Commits)
|
||||
|
||||
### Step 3:输出/写入 PR 描述
|
||||
```markdown
|
||||
## PR 描述(AI 生成)— #<id> <title>
|
||||
|
||||
### 背景
|
||||
<为什么改>
|
||||
|
||||
### 改动
|
||||
- <文件/功能1>:<具体改动>
|
||||
- <文件/功能2>:<具体改动>
|
||||
|
||||
### 测试
|
||||
- <如何验证>
|
||||
|
||||
### 影响
|
||||
- 影响范围:<模块>
|
||||
- 类型:feat/fix/...
|
||||
|
||||
### 关联 Issue
|
||||
fixes #<n>
|
||||
```
|
||||
|
||||
可选:通过 Raw API 写入 PR body(需确认)。
|
||||
|
||||
---
|
||||
|
||||
## 关键避坑
|
||||
| 坑 | 解决 |
|
||||
|----|------|
|
||||
| diff 太大 | 按文件分段处理,取关键改动 |
|
||||
| 自动描述需人工校对 | 标注"AI 生成,请校对" |
|
||||
| 写入 PR body 需 PATCH | `api PATCH /v1/<o>/<r>/pulls/<id>` + body |
|
||||
| commit message 是好的素材 | 结合 commit message + diff 双源生成 |
|
||||
|
||||
---
|
||||
|
||||
## 实测落地参考
|
||||
**gitlink/gitlink-cli 某 PR**(如 wiki +list shortcut):
|
||||
- diff:新增 shortcuts/wiki/wiki.go + wiki_test.go + register.go 注册
|
||||
- AI 生成描述:
|
||||
- 背景:补全 wiki 知识库管理命令(PDF 任务一要求)
|
||||
- 改动:新增 wiki +list/+view/+create/+update/+delete 5 命令 + 测试 + 注册
|
||||
- 测试:`go test ./shortcuts/wiki/`
|
||||
- 影响:新增 wiki 模块,不影响现有
|
||||
- 类型:feat
|
||||
|
||||
详见 verification.md。
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# PR 自动描述生成 · 验证记录 — gitlink-pr-describe
|
||||
|
||||
**验证场景**:gitlink/gitlink-cli 的 wiki shortcut PR(任务一 ylly 提交的)
|
||||
**验证日期**:2026-07-04
|
||||
|
||||
## 采集 PR 变更(模拟)
|
||||
| 数据 | 内容 |
|
||||
|------|------|
|
||||
| 变更文件 | shortcuts/wiki/wiki.go(新增 206 行)+ wiki_test.go(171 行)+ register.go(注册)|
|
||||
| commit message | feat: add wiki +list/+view/+create/+update/+delete |
|
||||
|
||||
## AI 生成的 PR 描述
|
||||
|
||||
```markdown
|
||||
## 背景
|
||||
补全 gitlink-cli 的 wiki 知识库管理能力(任务一要求),新增 wiki 页面 CRUD 命令。
|
||||
|
||||
## 改动
|
||||
- shortcuts/wiki/wiki.go:新增 wiki +list/+view/+create/+update/+delete 5 个命令
|
||||
- shortcuts/wiki/wiki_test.go:5 个单元测试
|
||||
- shortcuts/register.go:注册 wiki 模块
|
||||
|
||||
## 测试
|
||||
- go test ./shortcuts/wiki/(5 测试通过)
|
||||
|
||||
## 影响
|
||||
- 新增 wiki 模块,不影响现有命令
|
||||
- 使用独立 Gateway API(gateway.gitlink.org.cn)
|
||||
|
||||
## 类型
|
||||
feat(新增功能)
|
||||
|
||||
## 关联
|
||||
任务一 wiki 知识库管理
|
||||
```
|
||||
|
||||
## 验证结论
|
||||
| 维度 | 结果 |
|
||||
|------|:----:|
|
||||
| diff 分析 | ✅ 识别 5 命令 + 测试 + 注册 |
|
||||
| 规范结构 | ✅ 背景/改动/测试/影响/类型齐全 |
|
||||
| 双源生成 | ✅ commit message + diff 结合 |
|
||||
|
||||
AI 生成的描述规范、完整、可直接用于 PR body,省去手写时间。
|
||||
Loading…
Reference in New Issue