diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 30a7864..0862b9e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -15,13 +15,15 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: go.mod + go-version: '1.22' - name: Build run: go build ./... - - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 + - name: Validate i18n and skill metadata + run: | + go run ./internal/i18n/cmd/check + go run ./internal/skillmeta/cmd/check - name: Lint run: make lint @@ -29,8 +31,5 @@ jobs: - name: Test run: make test - - name: Validate research Skills - run: make validate-research-skills - - name: Check formatting run: make fmt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45247a1..e503163 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,8 @@ jobs: - name: Scan i18n key references run: go run ./internal/i18n/cmd/check --scan-code - - name: Validate research Skills - run: python3 scripts/validate-research-skills.py + - name: Validate skill metadata + run: go run ./internal/skillmeta/cmd/check - name: Run Go tests run: go test ./... diff --git a/Makefile b/Makefile index f0a177b..927e2d4 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ BINARY := gitlink-cli VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") LDFLAGS := -s -w -X '$(MODULE)/cmd.Version=$(VERSION)' -.PHONY: build install clean test check vet fmt cover lint validate-research-skills +.PHONY: build install clean test check vet fmt cover lint skills build: go build -ldflags "$(LDFLAGS)" -o $(BINARY) . @@ -35,10 +35,10 @@ cover: lint: golangci-lint run ./... -validate-research-skills: - python3 scripts/validate-research-skills.py +skills: + go run ./internal/skillmeta/cmd/check -check: fmt vet lint test validate-research-skills +check: fmt vet lint skills test @echo "All checks passed." hooks: diff --git a/internal/skillmeta/cmd/check/main.go b/internal/skillmeta/cmd/check/main.go new file mode 100644 index 0000000..9875ea9 --- /dev/null +++ b/internal/skillmeta/cmd/check/main.go @@ -0,0 +1,26 @@ +// Command check validates the SKILL.md frontmatter of the skills/ registry. +// It mirrors the i18n gate: run from the repo root, it exits non-zero and +// prints every problem, so CI and `make check` can keep the registry honest. +package main + +import ( + "fmt" + "os" + + "github.com/gitlink-org/gitlink-cli/internal/skillmeta" +) + +func main() { + problems, err := skillmeta.Validate("skills") + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if len(problems) > 0 { + for _, p := range problems { + fmt.Fprintln(os.Stderr, p.String()) + } + os.Exit(1) + } + fmt.Println("skill metadata is valid") +} diff --git a/internal/skillmeta/frontmatter.go b/internal/skillmeta/frontmatter.go new file mode 100644 index 0000000..1d8c933 --- /dev/null +++ b/internal/skillmeta/frontmatter.go @@ -0,0 +1,60 @@ +// Package skillmeta validates the YAML frontmatter of the skills/ registry so +// that every SKILL.md is discoverable and structurally sound. +package skillmeta + +import ( + "bytes" + "errors" + "fmt" + + "gopkg.in/yaml.v3" +) + +// Frontmatter is the metadata block every SKILL.md carries. +type Frontmatter struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + Description string `yaml:"description"` + Metadata Metadata `yaml:"metadata"` +} + +// Metadata holds the nested metadata fields of a skill. +type Metadata struct { + Requires Requires `yaml:"requires"` + CLIHelp string `yaml:"cliHelp"` +} + +// Requires lists what a skill needs to run. +type Requires struct { + Bins []string `yaml:"bins"` +} + +var errNoFrontmatter = errors.New("no `---` delimited frontmatter block") + +// ExtractFrontmatter returns the YAML between the first pair of `---` fences. +func ExtractFrontmatter(src []byte) ([]byte, error) { + const fence = "---" + trimmed := bytes.TrimLeft(src, " \t\r\n") + if !bytes.HasPrefix(trimmed, []byte(fence)) { + return nil, errNoFrontmatter + } + rest := trimmed[len(fence):] + idx := bytes.Index(rest, []byte("\n"+fence)) + if idx < 0 { + return nil, errNoFrontmatter + } + return rest[:idx], nil +} + +// ParseFrontmatter strictly decodes the frontmatter, rejecting unknown or +// wrongly-nested keys so that structural mistakes surface as errors instead of +// being silently dropped. +func ParseFrontmatter(block []byte) (Frontmatter, error) { + var fm Frontmatter + dec := yaml.NewDecoder(bytes.NewReader(block)) + dec.KnownFields(true) + if err := dec.Decode(&fm); err != nil { + return Frontmatter{}, fmt.Errorf("invalid frontmatter: %w", err) + } + return fm, nil +} diff --git a/internal/skillmeta/testdata/bad/gitlink-flat/SKILL.md b/internal/skillmeta/testdata/bad/gitlink-flat/SKILL.md new file mode 100644 index 0000000..791c9ec --- /dev/null +++ b/internal/skillmeta/testdata/bad/gitlink-flat/SKILL.md @@ -0,0 +1,11 @@ +--- +name: gitlink-flat +version: 1.0.0 +description: "A test skill whose metadata children are flattened to the top instead of nested." +metadata: +requires: +bins: ["gitlink-cli"] +cliHelp: "gitlink-cli x --help" +--- + +# body diff --git a/internal/skillmeta/testdata/bad/gitlink-mismatch/SKILL.md b/internal/skillmeta/testdata/bad/gitlink-mismatch/SKILL.md new file mode 100644 index 0000000..2d6884e --- /dev/null +++ b/internal/skillmeta/testdata/bad/gitlink-mismatch/SKILL.md @@ -0,0 +1,11 @@ +--- +name: gitlink-wrongname +version: 1.0.0 +description: "A test skill whose name field does not match its own directory name." +metadata: + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli x --help" +--- + +# body diff --git a/internal/skillmeta/testdata/bad/gitlink-nobins/SKILL.md b/internal/skillmeta/testdata/bad/gitlink-nobins/SKILL.md new file mode 100644 index 0000000..2708fb6 --- /dev/null +++ b/internal/skillmeta/testdata/bad/gitlink-nobins/SKILL.md @@ -0,0 +1,11 @@ +--- +name: gitlink-nobins +version: 1.0.0 +description: "A test skill whose requires.bins does not include the gitlink-cli binary." +metadata: + requires: + bins: ["other-tool"] + cliHelp: "gitlink-cli x --help" +--- + +# body diff --git a/internal/skillmeta/testdata/bad/gitlink-nohelp/SKILL.md b/internal/skillmeta/testdata/bad/gitlink-nohelp/SKILL.md new file mode 100644 index 0000000..47861b6 --- /dev/null +++ b/internal/skillmeta/testdata/bad/gitlink-nohelp/SKILL.md @@ -0,0 +1,10 @@ +--- +name: gitlink-nohelp +version: 1.0.0 +description: "A test skill whose metadata is missing the cliHelp field for the command." +metadata: + requires: + bins: ["gitlink-cli"] +--- + +# body diff --git a/internal/skillmeta/testdata/bad/gitlink-noversion/SKILL.md b/internal/skillmeta/testdata/bad/gitlink-noversion/SKILL.md new file mode 100644 index 0000000..268119c --- /dev/null +++ b/internal/skillmeta/testdata/bad/gitlink-noversion/SKILL.md @@ -0,0 +1,10 @@ +--- +name: gitlink-noversion +description: "A test skill whose frontmatter is missing the version field entirely." +metadata: + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli x --help" +--- + +# body diff --git a/internal/skillmeta/validate.go b/internal/skillmeta/validate.go new file mode 100644 index 0000000..06ed19a --- /dev/null +++ b/internal/skillmeta/validate.go @@ -0,0 +1,105 @@ +package skillmeta + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "unicode/utf8" +) + +// Problem is a single validation failure against the shared skill schema. +type Problem struct { + Skill string + Field string + Message string +} + +func (p Problem) String() string { + return fmt.Sprintf("%s: %s: %s", p.Skill, p.Field, p.Message) +} + +var ( + semverRe = regexp.MustCompile(`^\d+\.\d+\.\d+$`) + skillDirRe = regexp.MustCompile(`^gitlink-`) +) + +const minDescriptionRunes = 20 + +// Validate checks every /gitlink-*/SKILL.md against the shared schema and +// returns all problems found; it reports the whole registry rather than +// stopping at the first failure. +func Validate(root string) ([]Problem, error) { + entries, err := os.ReadDir(root) + if err != nil { + return nil, err + } + var problems []Problem + for _, entry := range entries { + if !entry.IsDir() || !skillDirRe.MatchString(entry.Name()) { + continue + } + problems = append(problems, validateSkill(root, entry.Name())...) + } + sort.Slice(problems, func(i, j int) bool { + if problems[i].Skill != problems[j].Skill { + return problems[i].Skill < problems[j].Skill + } + return problems[i].Field < problems[j].Field + }) + return problems, nil +} + +func validateSkill(root, name string) []Problem { + var ps []Problem + add := func(field, msg string) { + ps = append(ps, Problem{Skill: name, Field: field, Message: msg}) + } + + src, err := os.ReadFile(filepath.Join(root, name, "SKILL.md")) + if err != nil { + add("SKILL.md", "cannot read: "+err.Error()) + return ps + } + block, err := ExtractFrontmatter(src) + if err != nil { + add("frontmatter", err.Error()) + return ps + } + fm, err := ParseFrontmatter(block) + if err != nil { + add("frontmatter", err.Error()) + return ps + } + + switch { + case fm.Name == "": + add("name", "must not be empty") + case fm.Name != name: + add("name", fmt.Sprintf("must equal the directory name %q", name)) + } + if !semverRe.MatchString(fm.Version) { + add("version", "must be semantic version X.Y.Z") + } + if utf8.RuneCountInString(fm.Description) < minDescriptionRunes { + add("description", fmt.Sprintf("must be at least %d characters; it is the router's only routing signal", minDescriptionRunes)) + } + if !containsString(fm.Metadata.Requires.Bins, "gitlink-cli") { + add("metadata.requires.bins", `must contain "gitlink-cli"`) + } + if strings.TrimSpace(fm.Metadata.CLIHelp) == "" { + add("metadata.cliHelp", "must name the command group, e.g. \"gitlink-cli x --help\"") + } + return ps +} + +func containsString(xs []string, target string) bool { + for _, x := range xs { + if x == target { + return true + } + } + return false +} diff --git a/internal/skillmeta/validate_test.go b/internal/skillmeta/validate_test.go new file mode 100644 index 0000000..c35ee84 --- /dev/null +++ b/internal/skillmeta/validate_test.go @@ -0,0 +1,39 @@ +package skillmeta + +import "testing" + +// TestRepoSkillsValid treats the real skills/ registry as a regression +// baseline: once fixed, every SKILL.md must keep passing the schema. +func TestRepoSkillsValid(t *testing.T) { + problems, err := Validate("../../skills") + if err != nil { + t.Fatalf("validate skills: %v", err) + } + for _, p := range problems { + t.Errorf("unexpected problem in registry: %s", p) + } +} + +// TestValidateCatchesBadSkills pins each rule to a deliberately broken sample. +func TestValidateCatchesBadSkills(t *testing.T) { + problems, err := Validate("testdata/bad") + if err != nil { + t.Fatalf("validate testdata: %v", err) + } + got := make(map[string]bool) + for _, p := range problems { + got[p.Skill+"/"+p.Field] = true + } + want := []string{ + "gitlink-noversion/version", + "gitlink-nobins/metadata.requires.bins", + "gitlink-nohelp/metadata.cliHelp", + "gitlink-flat/frontmatter", + "gitlink-mismatch/name", + } + for _, w := range want { + if !got[w] { + t.Errorf("expected problem %q, got %v", w, problems) + } + } +} diff --git a/skills/gitlink-member/SKILL.md b/skills/gitlink-member/SKILL.md index dfe102a..c671bf5 100644 --- a/skills/gitlink-member/SKILL.md +++ b/skills/gitlink-member/SKILL.md @@ -1,7 +1,10 @@ --- name: gitlink-member -description: "仓库成员管理:列出、添加、批量添加、移除成员,调整成员角色,生成/查看/接受邀请链接,通过邀请码申请加入项目,退出仓库成员关系。" +version: 1.0.0 +description: "仓库成员管理:列出、添加、批量添加、移除成员,调整成员角色,生成、查看和接受项目邀请链接。当用户需要管理 GitLink 仓库成员、成员角色或邀请链接时触发。" metadata: + requires: + bins: ["gitlink-cli"] cliHelp: "gitlink-cli member --help" --- @@ -21,8 +24,6 @@ metadata: | `member +invite-link` | 获取或生成当前邀请链接 | | `member +invite-info` | 查看邀请链接信息 | | `member +accept-invite` | 接受邀请链接 | -| `member +apply` | 通过项目邀请码申请加入项目 | -| `member +quit` | 退出当前仓库成员关系 | ## 示例 @@ -50,20 +51,10 @@ gitlink-cli member +invite-info --owner Gitlink --repo forgeplus --sign - -# 通过邀请码申请加入项目。role 支持 manager、developer、reporter,建议先 dry-run。 -gitlink-cli member +apply --code MPzQgH --role developer --dry-run -gitlink-cli member +apply --code MPzQgH --role developer - -# 退出仓库成员关系。真实执行必须显式 --yes。 -gitlink-cli member +quit --owner Gitlink --repo forgeplus --dry-run -gitlink-cli member +quit --owner Gitlink --repo forgeplus --yes ``` ## 安全规则 - 执行 `member +remove`、`member +role`、`member +add`、`member +batch-add` 前,确认目标仓库和用户 ID。 -- 执行 `member +apply`、`member +quit` 前,确认邀请码、目标仓库和期望角色;优先使用 `--dry-run` 预览。 -- `member +quit` 会让当前用户离开仓库,真实执行必须带 `--yes`。 - 批量添加前优先使用 `--dry-run` 预览。 - 避免在公开日志中暴露邀请链接的完整 `sign`。 diff --git a/skills/gitlink-notification-digest/SKILL.md b/skills/gitlink-notification-digest/SKILL.md new file mode 100644 index 0000000..b317c94 --- /dev/null +++ b/skills/gitlink-notification-digest/SKILL.md @@ -0,0 +1,312 @@ +--- +name: gitlink-notification-digest +version: 2.0.0 +description: "通知摘要:汇总 GitLink 通知并按类型分类,生成通知摘要报告,支持批量标记已读。当用户需要查看通知摘要、整理通知、清理未读通知时触发。" +metadata: + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli api --help" +--- + +# gitlink-notification-digest(通知摘要) + +**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。** +**CRITICAL — 标记已读为写操作,执行前需确认用户意图。** +**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。** + +> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。 +> **执行样例:** 参见 [`EXAMPLES.md`](EXAMPLES.md) + +--- + +## 功能概述 + +帮助用户高效管理 GitLink 通知(GitLink 平台称为「消息」): + +1. **通知列表** — 获取所有未读通知 +2. **自动分类** — 按 `source` 字段分类(Issue/PR/系统等) +3. **优先级判断** — 识别需要立即处理的通知 +4. **批量操作** — 支持标记已读(需确认) +5. **摘要报告** — 生成结构化通知摘要 + +--- + +## ⚠️ 关键注意事项 + +### CLI 路径处理 Bug + +**`gitlink-cli api` 的路径参数不要以 `/` 开头**,否则会被错误解析为本地文件路径。 + +```bash +# ❌ 错误 — 路径以 / 开头会被解析为 D:/Applications/Git/... +gitlink-cli api GET /users/me + +# ✅ 正确 — 去掉前导 / +gitlink-cli api GET "users/{owner}/messages.json" +``` + +### 术语对照 + +GitLink 平台用「**消息**」(messages)而不是「通知」(notifications)。API 端点和字段均使用 `messages`。 + +--- + +## 工作流:通知摘要 + +### Step 1:获取通知列表 + +使用 Raw API 调用 `/api/users/{owner}/messages.json`: + +```bash +# 获取未读通知(status=1 表示未读,2 表示已读) +gitlink-cli api GET "users/{owner}/messages.json" --query "status=1&limit=20" --format json + +# 获取全部通知(含已读) +gitlink-cli api GET "users/{owner}/messages.json" --query "limit=20" --format json + +# 分页获取 +gitlink-cli api GET "users/{owner}/messages.json" --query "status=1&page=2&limit=20" --format json + +# 按类型过滤 +# type=notification 系统消息(仓库动态、PR、Issue 等) +# type=atme @我消息 +gitlink-cli api GET "users/{owner}/messages.json" --query "type=atme&status=1&limit=20" --format json +``` + +**参数说明:** + +| 参数 | 位置 | 说明 | +|------|------|------| +| `{owner}` | Path | 当前用户名(从 `gitlink-cli auth status` 获取) | +| `status` | Query | 1=未读,2=已读,不传=全部 | +| `type` | Query | `notification`=系统消息,`atme`=@我消息,不传=全部 | +| `page` | Query | 页码(默认 1) | +| `limit` | Query | 每页条数(默认 20) | + +**响应结构:** + +```json +{ + "total_count": 28, + "type": "", + "unread_notification": 7, + "unread_atme": 0, + "messages": [ + { + "id": 740214, + "status": 1, + "content": "jiangtx在 jiangtx/gitlink-cli 提交了一个合并请求:label 模块新建", + "notification_url": "https://www.gitlink.org.cn/jiangtx/gitlink-cli/pulls/15347", + "source": "ProjectPullRequest", + "created_at": "2026-06-03 00:27:37", + "time_ago": "10小时前", + "type": "notification", + "sender": { + "id": 113, + "type": "User", + "name": "jiangtx", + "login": "jiangtx", + "image_url": "..." + } + } + ] +} +``` + +**提取字段:** +- `id` — 消息 ID(用于标记已读) +- `content` — HTML 格式的通知内容 +- `source` — 通知来源类型(枚举值,见下方分类表) +- `notification_url` — 跳转链接,可从中解析仓库(提取 URL 中的 `/owner/repo/` 段) +- `created_at` — 通知时间(格式 `YYYY-MM-DD HH:mm:ss`) +- `status` — 1=未读,2=已读 +- `type` — `notification` 或 `atme` +- `sender` — 发送者信息(login, name, image_url) + +### Step 2:分类与优先级 + +#### 2.1 按 `source` 字段分类 + +| 类型 | `source` 枚举值 | 处理建议 | +|------|-----------------|----------| +| 🔴 **@提及** | `IssueAtme`, `PullReuqestAtme`(注意官方 API 拼写如此) | 立即查看回复 | +| 🟡 **Issue 更新** | `IssueAssigned`, `IssueExpire`, `IssueChanged`, `IssueDeleted`, `IssueJournal`, `ProjectIssue` | 当天处理 | +| 🟢 **PR 更新** | `PullRequestAssigned`, `PullRequestChanged`, `PullRequestClosed`, `PullRequestJournal`, `PullRequestMerged`, `ProjectPullRequest` | 跟进代码 | +| 🔵 **系统通知** | `ProjectJoined`, `ProjectLeft`, `ProjectMemberJoined`, `ProjectMemberLeft`, `ProjectForked`, `ProjectPraised`, `ProjectRole`, `ProjectFollowed`, `ProjectDeleted`, `ProjectTransfer`, `ProjectSettingChanged`, `ProjectMilestone`, `ProjectMilestoneCompleted`, `ProjectVersion`, `OrganizationJoined`, `OrganizationLeft`, `OrganizationRole`, `ProjectOpenDevOps` | 知悉即可 | +| ⚪ **其他** | `LoginIpTip` 及未列出的值 | 按需查看 | + +**完整 `source` 枚举参考:** + +
+展开查看全部 source 枚举值 + +| 枚举值 | 含义 | +|--------|------| +| `IssueAssigned` | 有新指派给我的疑修 | +| `IssueExpire` | 我创建或负责的疑修截止日期到达最后一天 | +| `IssueAtme` | 在疑修中@我 | +| `IssueChanged` | 我创建或负责的疑修状态变更 | +| `IssueDeleted` | 我创建或负责的疑修删除 | +| `IssueJournal` | 我创建或负责的疑修有新的评论 | +| `LoginIpTip` | 登录 IP 提示 | +| `OrganizationJoined` | 加入组织 | +| `OrganizationLeft` | 离开组织 | +| `OrganizationRole` | 组织角色变更 | +| `ProjectDeleted` | 项目被删除 | +| `ProjectFollowed` | 有人关注了项目 | +| `ProjectForked` | 项目被 Fork | +| `ProjectIssue` | 项目新 Issue | +| `ProjectJoined` | 加入项目 | +| `ProjectLeft` | 离开项目 | +| `ProjectMemberJoined` | 新成员加入项目 | +| `ProjectMemberLeft` | 成员离开项目 | +| `ProjectMilestoneCompleted` | 里程碑完成 | +| `ProjectMilestone` | 新里程碑 | +| `ProjectOpenDevOps` | DevOps 引擎开通 | +| `ProjectPraised` | 项目被点赞 | +| `ProjectPullRequest` | 项目新 PR | +| `ProjectRole` | 项目角色变更 | +| `ProjectSettingChanged` | 项目设置变更 | +| `ProjectTransfer` | 项目转让 | +| `ProjectVersion` | 新版本发布 | +| `PullRequestAssigned` | 有指派给我的 PR | +| `PullReuqestAtme` | 在 PR 中@我(**官方拼写如此**) | +| `PullRequestChanged` | PR 状态变更 | +| `PullRequestClosed` | PR 被关闭 | +| `PullRequestJournal` | PR 有新评论 | +| `PullRequestMerged` | PR 已合并 | + +
+ +#### 2.2 优先级排序 + +| 优先级 | 判定 | +|--------|------| +| **P0 - 立即** | 含 `Atme` 的消息(IssueAtme, PullReuqestAtme) | +| **P1 - 今天** | 自己管理的仓库有 PR 合并/关闭,或被分配的 Issue/PR 有更新 | +| **P2 - 本周** | 关注的仓库有新 PR、新 Issue | +| **P3 - 可忽略** | 点赞(ProjectPraised)、成员加入/离开、Fork 等系统通知 | + +### Step 3:生成通知摘要 + +按下方输出模板生成报告。 + +### Step 4:标记已读(可选,需确认) + +```bash +# 标记单条已读 +gitlink-cli api POST "users/{owner}/messages/{id}/read" --format json + +# 批量标记已读 — 逐条调用,GitLink 暂无批量已读 API +for id in ; do + gitlink-cli api POST "users/{owner}/messages/$id/read" --format json +done +``` + +> ⚠️ **执行前必须确认用户意图** — 标记已读为写操作。 +> ⚠️ **GitLink 没有批量已读 API**,需要逐条标记。 + +--- + +## 输出模板 + +```markdown +# 🔔 通知摘要 + +> 生成时间:{{当前时间}} +> 未读通知:{{unread_count}} 条 / 总计:{{total_count}} 条 + +--- + +## 一、概要 + +| 类型 | 未读 | 总计 | +|------|------|------| +| @提及 | {{mention_unread}} | {{mention_total}} | +| Issue 更新 | {{issue_unread}} | {{issue_total}} | +| PR 更新 | {{pr_unread}} | {{pr_total}} | +| 系统通知 | {{system_unread}} | {{system_total}} | +| 其他 | {{other_unread}} | {{other_total}} | + +--- + +## 二、需要立即处理(P0) + +> 如无,输出:*🎉 无紧急通知。* + +| # | 类型 | 仓库 | 内容摘要 | 时间 | +|---|------|------|----------|------| +| 1 | 🔴@提及 | {{repo}} | {{summary}} | {{time}} | + +--- + +## 三、今天处理(P1) + +> 如无,输出:*无待处理通知。* + +| # | 类型 | 仓库 | 内容摘要 | 时间 | +|---|------|------|----------|------| + +--- + +## 四、本周关注(P2) + +> 如无,输出:*无需要本周关注的通知。* + +--- + +## 五、可忽略(P3) + +> 如本段被折叠,输出:*{{p3_count}} 条低优先级通知,已折叠。* + +--- + +## 六、通知趋势 + +| 时间段 | 通知数 | +|--------|--------| +| 今日 | {{today_count}} | +| 昨日 | {{yesterday_count}} | +| 本周 | {{week_count}} | +| 上周 | {{last_week_count}} | + +--- + +## 七、近期已读回顾 + +> 列出最近 3-5 条已读但值得回顾的通知(如角色变更、PR 合并等)。 + +--- + +## 操作建议 + +- 建议标记已读:{{suggest_read_count}} 条 P3 通知 +- 需要回复/处理:{{need_action_count}} 条 P0/P1 通知 + +如需标记 P3 通知为已读,我可以逐条执行: +`gitlink-cli api POST "users/{owner}/messages/{id}/read"` +``` + +--- + +## 异常场景处理 + +| 场景 | 处理方式 | +|------|----------| +| 无未读通知 | 输出"🎉 所有通知已处理完毕" | +| 通知数量 > 50 | 分页获取(page 1/2/3),优先分析最近 50 条 | +| API 返回 HTML 而非 JSON | 路径可能以 `/` 开头导致解析错误,去掉前导 `/` 重试 | +| `unread_notification` > messages 数组长度 | 存在多页数据,追加 `--query "page=2"` 获取 | +| 用户名不确定 | 先执行 `gitlink-cli auth status` 获取当前登录用户 | + +--- + +## 注意事项 + +- ✅ **所有命令使用 `--format json`**,确保可解析 +- ✅ **标记已读为写操作**,执行前必须确认用户意图 +- ✅ **本 Skill 默认只读分析**,仅在用户明确要求时标记已读 +- ⚠️ **`gitlink-cli api` 路径不要以 `/` 开头**(CLI Bug) +- ⚠️ **GitLink 用「消息(messages)」而非「通知(notifications)」** +- ⚠️ **`source` 字段 `PullReuqestAtme` 是官方拼写错误**,实际使用注意匹配 +- ⚠️ **通知可能分页**,数量 >20 时需追加 `--query "page=2"` diff --git a/skills/gitlink-scholar-profile/SKILL.md b/skills/gitlink-scholar-profile/SKILL.md index e557afb..0ea2b74 100644 --- a/skills/gitlink-scholar-profile/SKILL.md +++ b/skills/gitlink-scholar-profile/SKILL.md @@ -3,9 +3,9 @@ name: gitlink-scholar-profile version: 1.0.0 description: "学者/团队科研画像:跨仓库聚合分析 GitLink 用户或组织的科研产出,生成影响力雷达图与代表性成果报告。当用户需要了解某学者/团队的科研产出全貌、评估科研影响力时触发。" metadata: -requires: -bins: ["gitlink-cli"] -cliHelp: "gitlink-cli user --help" + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli user --help" --- # gitlink-scholar-profile(学者/团队科研画像)