Merge pull request '新增demo' (#24) from surponess_br into master

This commit is contained in:
Surponess 2026-07-06 11:18:58 +08:00
commit 86bb4cab98
19 changed files with 1994 additions and 2 deletions

View File

@ -23,8 +23,20 @@ workflow:
ssh_cmd: '"if [ -d /root/gitlink-cli/.git ]; then cd /root/gitlink-cli && git fetch origin && git checkout master && git reset --hard origin/master; else rm -rf /root/gitlink-cli && git clone https://gitlink.org.cn/whale_hihihi/gitlink-cli.git /root/gitlink-cli && cd /root/gitlink-cli && git checkout master; fi && docker stop gitlink-cli 2>/dev/null; docker rm gitlink-cli 2>/dev/null; docker rmi gitlink-cli:latest 2>/dev/null; docker build --no-cache -t gitlink-cli:latest . && docker run -d --name gitlink-cli -p 8080:8080 -e GITLINK_TOKEN=((gitlink_token.token)) gitlink-cli:latest && echo Deploy success"'
needs:
- start
- ref: ssh_cmd_1
name: 构建并部署demo网页(8000)
task: ssh_cmd@1.1.1
input:
ssh_pass: ((deploy_server.password))
ssh_ip: '"121.41.222.73"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_cmd: '"cd /root/gitlink-cli && docker stop gitlink-cli-demo 2>/dev/null; docker rm gitlink-cli-demo 2>/dev/null; docker rmi gitlink-cli-demo:latest 2>/dev/null; docker build --no-cache -f demo/Dockerfile -t gitlink-cli-demo:latest . && docker run -d --name gitlink-cli-demo -p 8000:8000 --restart unless-stopped gitlink-cli-demo:latest && echo Demo deploy success at http://121.41.222.73:8000"'
needs:
- ssh_cmd_0
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0
- ssh_cmd_1

4
.gitignore vendored
View File

@ -1,6 +1,6 @@
docs/
doc/
# Pythondemo 网页后端)
__pycache__/
*.pyc
*.exe
research-output/
demo/bin/

29
demo/Dockerfile Normal file
View File

@ -0,0 +1,29 @@
# demo/Dockerfile — GitLink CLI 演示网页后端
# 多阶段Go 编译 Linux 二进制 → Python 运行时跑 server.py
# 构建上下文 = 仓库根gitlink-cli/ docker build -f demo/Dockerfile -t gitlink-cli-demo .
# ---------- Stage 1: 编译 gitlink-cliLinux ----------
FROM golang:1.26-alpine AS builder
ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /out/gitlink-cli .
# ---------- Stage 2: Python 运行时 ----------
FROM python:3.11-alpine
RUN apk add --no-cache git ca-certificates
WORKDIR /app
# 二进制
COPY --from=builder /out/gitlink-cli /usr/local/bin/gitlink-cli
# 演示网页 + Skillsserver.py 要读 SKILL.md
COPY demo/ /app/demo/
COPY skills/ /app/skills/
ENV HOST=0.0.0.0
ENV PORT=8000
ENV GITLINK_BIN=/usr/local/bin/gitlink-cli
EXPOSE 8000
WORKDIR /app/demo/web
CMD ["python3", "server.py"]

91
demo/README.md Normal file
View File

@ -0,0 +1,91 @@
# GitLink CLI · 演示网页demo/
> 一个**可交互演示站**,展示子任务一~四全部成果25 域命令浏览器、48 Skill 卡片墙、pr-guard 工作流、科研四维画像。
> 后端 `web/server.py`Python 标准库,零依赖)真跑 `gitlink-cli`**访客自带 token零凭据上云**。
## 目录结构
```
demo/
├── web/ # 演示网页(核心)
│ ├── server.py # Python 后端(/api/run /api/skill /api/analyze
│ ├── index.html # 前端单页Tailwind+Chart.js CDN
│ └── README.md # 网页本地启动说明
├── Dockerfile # demo 部署镜像Go 编译 + Python 运行时)
├── build-demo.sh # 本地一键产 Linux 二进制(测 Dockerfile 用)
├── research-insight-workflow.sh # 任务四:科研画像端到端脚本
├── pr-guard-workflow.sh # 任务三:质量看门人脚本
├── live-demo.sh / snippet-live-demo.sh
└── *.md # 各任务指南 + 验证记录 + 报告原件
```
---
## 一、本地跑Windows / Linux / macOS
```bash
# 1. 编译 CLI仓库根
cd gitlink-cli # 含 go.mod 的仓库根
go build -o gitlink-cli . # Windows 产出 gitlink-cli.exe
# 2. 启动后端
cd demo/web
python server.py # → http://0.0.0.0:8000
# 3. 浏览器开 http://localhost:8000
# 顶栏粘自己的 GitLink token → 平台命令真跑snippet 等本地命令免 token
```
> 二进制自动探测:`GITLINK_BIN` > 仓库根 `gitlink-cli[.exe]` > PATH。
> 端口/主机:`PORT=9000 HOST=127.0.0.1 python server.py`。
---
## 二、云端部署(用你们已有服务器 121.41.222.73
已配置好「**push 即上线**」:`.devops/自动构建部署.yml` 在原有 CLI 部署之后,**追加一步**构建并运行 demo 容器:
```
docker build -f demo/Dockerfile -t gitlink-cli-demo .
docker run -d --name gitlink-cli-demo -p 8000:8000 --restart unless-stopped gitlink-cli-demo
```
**你只需**
1. 把本目录提交并 push 到 `whale_hihihi/gitlink-cli` master → CI 自动跑 → demo 上线。
2. 在服务器安全组/防火墙**开放 8000 端口**(阿里云安全组加一条入方向 TCP 8000
3. 访问 `http://121.41.222.73:8000`
> 手动部署(不走 CISSH 到服务器,`cd /root/gitlink-cli && docker build -f demo/Dockerfile -t gitlink-cli-demo . && docker run -d --name gitlink-cli-demo -p 8000:8000 --restart unless-stopped gitlink-cli-demo`。
### 镜像里有什么demo/Dockerfile 多阶段)
- Stage1 `golang:1.26-alpine``CGO_ENABLED=0 GOOS=linux go build` 产 Linux 二进制。
- Stage2 `python:3.11-alpine`:装 `git`/`ca-certificates`,放二进制到 `/usr/local/bin/gitlink-cli`,拷 `demo/``skills/``ENV PORT=8000``CMD python3 demo/web/server.py`。
---
## 三、安全模型(为什么能放心公网开放)
| 点 | 做法 |
|----|------|
| 团队 token | **不烘焙**进镜像/代码。镜像里没有任何 GitLink 凭据。 |
| 访客 token | 只存在访客自己的浏览器 localStorage按请求传后端 → 注入子进程 `GITLINK_TOKEN` → 用完即弃,**不落盘、不写日志**。 |
| 命令注入 | 后端白名单(仅 30 个 gitlink-cli 顶层域)+ subprocess 列表参数(不经 shell+ 30s 超时。 |
| 写操作 | CLI 写操作本就要 `--dry-run`/确认;演示页默认只点只读命令。 |
→ 公网开放的安全风险≈0泄露的至多是访客自己输错的那一次请求。
---
## 四、访客怎么用(写进 PPT/答辩)
1. 打开 `http://121.41.222.73:8000`
2. 顶栏粘自己的 GitLink 个人访问令牌GitLink → 个人中心 → 个人令牌)。
3. 点「命令域」里任意动词 → 终端真跑;或点 Skill 卡片读 SKILL.md或科研区「实拉分析」任一仓库。
---
## 五、其它 PaaS 部署(可选,不占你们服务器)
也可部署到 Render / Railway / Koyeb 等(需能跑 Docker
- 用 `demo/Dockerfile`,暴露端口环境变量 `PORT`(已支持)。
- 这些平台默认按其给的端口注入 `PORT`server.py 已读 `PORT` 环境变量,无需改。

96
demo/SHOWCASE.md Normal file
View File

@ -0,0 +1,96 @@
# Skills 功能验收演示文稿
> 用法:照此 5 分钟流程演示。**Demo 1 可现场实跑**(零依赖、最稳),其余讲解设计。
> 配套:`snippet-live-demo.sh`(实演脚本)、`../Skills工作总结.md`(完整成果)
---
## 演示总览5 分钟)
| 环节 | 时长 | 形式 | 目的 |
|------|:----:|------|------|
| 开场Skills 是什么 | 30s | 口述 + 成果速览 | 讲清价值定位 |
| **Demo 1 · snippet 实演** | 1.5min | **跑脚本** | 证明 Skill 真能驱动 CLI |
| Demo 2 · onboarding 设计 | 1.5min | 打开 SKILL.md 讲 | 展示 AI 工作流设计深度 |
| Demo 3 · digest/todo 体验优化 | 1min | 讲设计 + 分工 | 展示体验优化与去重思考 |
| 收尾:成果 + 验证 | 30s | 数字 | 强化贡献 |
---
## 开场30 秒)
> 一句话:**Skills 是写给 AI 的「菜谱」**——告诉 AI「什么场景、按什么顺序、调哪些 gitlink-cli 命令」。我们把 gitlink-cli 从「开发者工具」升级为「AI 可驱动的平台」。
>
> 本次新增 **5 个 Skill** + 补全 **28 个 examples** + snippet **7 命令端到端实测通过**
---
## Demo 1 · snippet 现场实演(核心,必演)
```bash
bash demo/snippet-live-demo.sh
```
**脚本会演示的闭环**(每个场景都展示「🧑用户提问 → 🤖AI 读 SKILL.md 决策 → 执行命令 → 输出」):
| 场景 | 命令 | SKILL.md 规则 |
|------|------|--------------|
| 保存代码 | `snippet +create` | --title 必填、--tags 逗号分隔 |
| 浏览 | `snippet +list` | 可按 tag/language 过滤 |
| 检索 | `snippet +search` | 全文匹配 |
| 详情 | `snippet +view` | 按 id |
| 导出 | `snippet +export` | -o 写文件 |
| 更新 | `snippet +update` | 至少一个字段 |
| 删除 | `snippet +delete` | 不可逆,先确认 |
**讲解要点**:注意每个场景 AI 都先「读 SKILL.md 决策」再执行——这就是 Skills 的核心价值,**AI 不是瞎调命令,而是按菜谱编排**。输出严格符合 `{"ok":true,"data":{...}}` 格式。
---
## Demo 2 · onboarding 设计深度(展示 B 类工作流)
**操作**:打开 `gitlink-cli/gitlink-cli/skills/gitlink-onboarding/SKILL.md`
**重点讲三处**(评分重点):
1. **5 维度友好度评估表**(决策规则章节)——把「哪个 Issue 适合新人」从主观判断变成可量化打分:标题清晰度 / 描述完整度 / 代码定位 / 改动范围 / 难度标签。
2. **4 个工作流**——项目概览 → 找任务 → 生成引导评论 → 贡献全流程Fork→Branch→PR
3. **引导评论输出模板**——AI 能自动生成「欢迎贡献 + 代码定位 + 修改步骤」的个性化评论。
> 一句话A 类(命令包装)做不到「智能推荐 + 生成评论」,所以选 B 类AI 工作流)。
---
## Demo 3 · digest / todo 体验优化(展示第二批 + 去重思考)
| Skill | 解决的痛点 | 与团队已有 Skill 的关系 |
|-------|-----------|----------------------|
| `gitlink-digest` | 信息太分散,看动态要挨个刷 | 与团队 `notification-digest` **分工**它做通知中心我做项目全景日报Issue+PR+CI+活跃度) |
| `gitlink-todo` | 没有「我的」视角,不知哪些在等我 | 团队**无对应**,真缺口 |
**去重思考(加分点)**:曾设计「僵尸唤醒 stale」核查发现团队已有完整的 `gitlink-stale-issue-manager`563 行),为避免重复造已删除——**体现对项目整体的理解和工程素养**。
---
## 收尾:成果 + 验证30 秒)
| 指标 | 数据 |
|------|------|
| 新增 Skill | **5 个**onboarding / auth / snippet / digest / todo |
| 补充 examples | **28 个**23 个补已有 Skill + 5 个新增自带) |
| 端到端实测 | snippet 全 7 命令通过 |
| 命令可调用性 | 新增 Skill 全用已注册命令域,可真实调用 |
> 演示结束。完整设计详见 `Skills工作总结.md`
---
## 答辩 Q&A 预备
| 可能的提问 | 回答要点 |
|-----------|---------|
| 工作边界? | 新增 5 个 Skill + 补 23 个 examples团队原有 42 个(见总结第二节) |
| 怎么证明 Skill 真能用? | 刚跑的 snippet 7 命令闭环;其余 4 个登录后可按 SKILL.md 工作流验证 |
| 为什么 onboarding 选 B 类? | 需智能推荐 + 生成评论A 类命令包装做不到 |
| digest 和团队 notification-digest 重复吗? | 不重复,分工明确:通知中心 vs 项目全景日报 |
| Skill 遵循什么规范? | 项目模板YAML frontmatter + CRITICAL 三连 + 引用 gitlink-shared |

13
demo/build-demo.sh Normal file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
# 一键构建 demo 所需的 Linux gitlink-cli 二进制(本地测试 Dockerfile 用)
# 用法bash demo/build-demo.sh → 产物 demo/bin/gitlink-cli
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$DIR/.." && pwd)" # 仓库根
OUT="$DIR/bin"
mkdir -p "$OUT"
echo "→ 在 $ROOT 编译 Linux amd64 二进制..."
( cd "$ROOT" && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o "$OUT/gitlink-cli" . )
echo "✓ 产出:$OUT/gitlink-cli"
echo " 本地测容器docker build -f $DIR/Dockerfile -t gitlink-cli-demo '$ROOT'"
echo " docker run --rm -p 8000:8000 gitlink-cli-demo"

146
demo/live-demo-guide.md Normal file
View File

@ -0,0 +1,146 @@
# 4 个 Skill 登录实演指南onboarding / digest / todo / auth
> 用法:登录后,**在 Claude Code 里用自然语言触发**,让真 AI 读 SKILL.md 自主编排 —— 这是最强的实演证据。
> 配套:`live-demo.sh`(辅助采集脚本,不想手敲命令时用)。
---
## 〇、前置准备
1. **登录**`gitlink-cli auth login`(或 `--token`),用 `gitlink-cli auth status` 确认已登录
2. **准备测试仓库**:用一个你自己的/有权限的公开仓库作为演示对象(避免在团队主仓库留痕)
3. **开着 Claude Code**:在本仓库目录下启动,让 AI 能读到 `skills/*/SKILL.md`
4. **安全原则**:只读命令随便跑;写操作(评论/关闭)让 AI 先 `--dry-run` 或确认
---
## 〇〇、推荐演示方式:自然语言触发真 AI最有说服力
> 不要告诉 AI 用哪个命令,只描述需求。看它是否**自主读 SKILL.md → 调对命令 → 输出符合模板**。
> 这是"Skills 让 AI 能驱动 CLI"的活证据,比手敲命令强得多。
每个 Skill 下面都给:**① 触发语**(你对 AI 说这句)→ **② 预期 AI 行为** → **③ 手动备选**(想自己跑时)→ **④ 讲解要点**。
---
## 一、auth 实演(最简单,开场暖身)
**① 触发语**
> "检查一下我的 gitlink 登录状态"
**② 预期 AI 行为**:读 `auth/SKILL.md` → 调 `gitlink-cli auth status` → 报告登录用户、Token 有效期、存储位置。
**③ 手动备选**
```bash
gitlink-cli auth status
gitlink-cli auth login --token # 如需演示登录流程
```
**④ 讲解要点**
- auth Skill 与 `gitlink-shared` 分工shared 讲认证原理auth 讲具体命令操作
- 决策树:遇到 401 → 引导 `auth login`403 → 查权限CI 环境 → 用 `--token`
- 演示 `logout` 后提醒:会清凭证,需重新登录
---
## 二、onboarding 实演核心亮点5 维度评估)
**① 触发语**
> "我想参与 <owner>/<repo> 这个项目,帮我找几个适合新手的任务"
**② 预期 AI 行为**:读 `onboarding/SKILL.md`
1. `search +issues --keyword "good first issue" --category opened`(找新手 Issue
2. `repo +info` + `repo +readme`(项目概览)
3. 对候选 Issue 做 **5 维度友好度评估**(标题/描述/定位/范围/难度)
4. 输出「推荐新手任务」清单 + 可选生成引导评论
**③ 手动备选**
```bash
gitlink-cli search +issues --owner <owner> --repo <repo> --keyword "good first issue" --category opened
gitlink-cli repo +info --owner <owner> --repo <repo>
gitlink-cli repo +readme --owner <owner> --repo <repo>
```
**④ 讲解要点**
- **5 维度评估表**是设计亮点:把"哪个 Issue 适合新人"从主观判断变成可量化打分(指着 AI 输出的评分讲)
- 引导评论模板AI 能生成「欢迎贡献 + 代码定位 + 修改步骤」个性化评论(写操作,会先确认)
- 若无 good-first-issue 标签AI 应从开放 Issue 推荐最简单的(决策规则)
---
## 三、digest 实演(亮点:跨源聚合成简报)
**① 触发语**
> "给我一份 <owner>/<repo> 的项目简报,今天有什么动态"
**② 预期 AI 行为**:读 `digest/SKILL.md` → 并行采集 → 聚合分类 →
1. `issue +list --state open` + `pr +list`Issue/PR 动态)
2. `ci +builds`CI 状态)
3. `api GET "users/<me>/messages.json"`(通知)
4. 按 🔴需关注 / 🟢新增 / 🔵进行中 / 📊指标 分类,输出 Markdown 简报
**③ 手动备选**
```bash
gitlink-cli issue +list --state open --format json
gitlink-cli pr +list --format json
gitlink-cli ci +builds --owner <owner> --repo <repo> --format json
gitlink-cli api GET "users/<me>/messages.json"
```
**④ 讲解要点**
- **跨源聚合**是亮点:一份简报汇总 Issue/PR/CI/通知,不用挨个刷
- 与团队 `notification-digest` 分工它做通知中心标记已读digest 做项目全景(不做标记已读)—— 体现去重思考
- 纯只读,安全可随时跑
---
## 四、todo 实演(亮点:补上「我的」视角)
**① 触发语**
> "我的待办有哪些?哪些 Issue/PR 在等我处理"
**② 预期 AI 行为**:读 `todo/SKILL.md`
1. `api GET "users/me"`(识别身份)
2. `search +issues --assignee <me> --category opened`(分配我的)
3. `api GET "users/<me>/messages.json"`@我的)
4. `pr +list`(我的 PR 状态)
5. 按紧急度(@我 > 待 review > 指派)排序,输出待办清单
**③ 手动备选**
```bash
gitlink-cli api GET "users/me" --format json
gitlink-cli search +issues --assignee <me> --category opened
gitlink-cli api GET "users/<me>/messages.json"
```
**④ 讲解要点**
- **「我的」视角**是 gitlink 最缺的:跨 Issue/PR 汇总个人待办
- 紧急度排序逻辑:@我且停留 >24h → 🔴紧急;待 review 的 PR → 🟡本周
- 团队无对应 Skill是真正的新增价值
---
## 五、验收串场词5 分钟版)
```
开场30sSkills 让 AI 能驱动 gitlink-cli。先看 snippet 实演(跑 snippet-live-demo.sh
转场snippet 是本地功能。接下来演示需要平台 API 的 4 个 Skill
我用自然语言提问,看 AI 是否自主读 SKILL.md 编排命令。
① auth30s「检查登录状态」→ AI 调 auth status。
② onboarding1.5min):「找新手任务」→ AI 5 维度评估出推荐清单。(重点讲评估表)
③ digest1.5min):「给我项目简报」→ AI 跨源聚合出报告。(重点讲与团队分工)
④ todo1min「我的待办」→ AI 汇总排序。(重点讲个人视角是缺口)
收尾30s5 个新增 Skill 都能被 AI 正确调用snippet 7 命令实测通过。
```
---
## 六、安全清单(实演前确认)
- [ ] 用**测试仓库**演示,不用团队主仓库
- [ ] 写操作onboarding 引导评论、issue close让 AI **先确认 / --dry-run**
- [ ] 演示完 `auth logout` 的话,记得重新登录
- [ ] 只读命令list/view/search/info/messages可放心反复跑

59
demo/live-demo.sh Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# ============================================================
# 4 个 Skill 登录实演 · 辅助采集脚本
# 作用:把每个 Skill 的「只读采集命令」串起来自动跑,展示真实数据
# 分析(评估/聚合/排序)部分由 AI 在 Claude Code 里做——那才是亮点
# 用法bash demo/live-demo.sh <owner> <repo> [your-username]
# 例bash demo/live-demo.sh myorg myproject zhangsan
# 前置:先 gitlink-cli auth login
# ============================================================
_DIR="$(cd "$(dirname "$0")" && pwd)"
CLI="$_DIR/../gitlink-cli.exe"; [ -f "$CLI" ] || CLI="$_DIR/../gitlink-cli" # Win→.exeLinux→无后缀
OWNER="${1:-}"; REPO="${2:-}"; ME="${3:-$OWNER}"
G='\033[0;32m'; Y='\033[1;33m'; C='\033[0;36m'; R='\033[0;31m'; B='\033[1m'; N='\033[0m'
banner() { echo -e "\n${B}══════════════════════════════════════════════════════${N}"; echo -e "${B} $1${N}"; echo -e "${B}══════════════════════════════════════════════════════${N}"; }
section() { echo -e "\n${C}━━━ $1 ━━━${N}"; }
run() { echo -e "${Y} $1${N}"; eval "$1" 2>&1 | head -16; echo; }
# ---------- 前置检查 ----------
banner "4 Skill 登录实演 · 辅助采集"
[ -f "$CLI" ] || { echo -e "${R}✗ 找不到 gitlink-cli${N}"; exit 1; }
if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
echo -e "${R}用法: bash $0 <owner> <repo> [your-username]${N}"
echo -e "${R}例 : bash $0 myorg myproject zhangsan${N}"; exit 1
fi
echo -e "${G}${N} 目标仓库: ${B}$OWNER/$REPO${N},当前用户: ${B}$ME${N}"
echo -e "${C}提示:本脚本只跑只读采集命令;分析(评估/聚合/排序)请在 Claude Code 里让 AI 做${N}"
# ---------- 0. auth登录状态 ----------
section "auth · 登录状态"
run "\"$CLI\" auth status"
# ---------- 1. onboarding找新手任务 ----------
section "onboarding · 新手 Issue + 项目概览(供 AI 做 5 维度评估)"
run "\"$CLI\" search +issues --owner $OWNER --repo $REPO --keyword 'good first issue' --category opened --format json"
run "\"$CLI\" repo +info --owner $OWNER --repo $REPO --format json"
# ---------- 2. digest多源数据供 AI 聚合成简报)----------
section "digest · Issue / PR / CI / 通知(供 AI 跨源聚合)"
run "\"$CLI\" issue +list --owner $OWNER --repo $REPO --state open --format json"
run "\"$CLI\" pr +list --owner $OWNER --repo $REPO --format json"
run "\"$CLI\" ci +builds --owner $OWNER --repo $REPO --format json"
run "\"$CLI\" api GET \"users/$ME/messages.json\""
# ---------- 3. todo个人待办数据供 AI 排序)----------
section "todo · 分配给我的 Issue + @我消息(供 AI 排序成待办)"
run "\"$CLI\" api GET \"users/me\" --format json"
run "\"$CLI\" search +issues --assignee $ME --category opened --format json"
run "\"$CLI\" api GET \"users/$ME/messages.json\""
# ---------- 总结 ----------
banner "采集完成"
echo -e "${B}接下来${N}:在 Claude Code 里用自然语言触发,让 AI 读对应 SKILL.md 分析以上数据:"
echo -e "${C}「找适合新手的任务」${N} → onboarding 的 5 维度评估"
echo -e "${C}「给我项目简报」${N} → digest 的跨源聚合"
echo -e "${C}「我的待办有哪些」${N} → todo 的紧急度排序"
echo -e "\n详见 ${Y}live-demo-guide.md${N}"
read -p "按回车键继续..."

View File

@ -0,0 +1,118 @@
# 代码质量看门人 · 工作流说明与架构(子任务三)
> 端到端自动化工作流PR 提交后自动跑完「采集 → AI Review → CI → 评论 → 质量判定/合并」。
> 对应 Skill`skills/gitlink-pr-guard/SKILL.md`;可复现脚本:`demo/pr-guard-workflow.sh`。
---
## 一、工作流架构图
```
┌─────────────────────────────────────────────┐
│ 触发PR 提交 / 更新 │
│ (或用户:帮我把关 PR #<id>
└──────────────────────┬──────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 1 采集 PR 变更 │
│ pr +view → pr +files → pr +diff --stat │
│ 产出PR 详情 / 变更文件 / diff 统计 │
└──────────────────────┬──────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 2 AI Review复用 code-review 逻辑) │
│ 逐文件分析 diff → 分级找问题 │
│ 🔴 Critical / 🟡 Warning / 🔵 Suggestion │
└──────────────────────┬──────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 3 CI 检查 │
│ ci +builds → 匹配分支最新构建 → 状态 │
│ success / failure / pending │
└──────────────────────┬──────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 4 发布质量看门人报告 │
│ api POST .../pulls/:id/reviews │
│ 报告:判定 + 问题清单 + CI + 处置建议 │
└──────────────────────┬──────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 5 质量判定(门禁规则) │
│ ┌─────────────────────────────────────┐ │
│ │ 0 Critical + CI success → ✅ 合并 │ │
│ │ 有 Critical → 🔴 请求修改 │ │
│ │ CI failure → 🔴 请求修改 │ │
│ └─────────────────────────────────────┘ │
│ 达标+确认 → pr +merge │
└─────────────────────────────────────────────┘
```
---
## 二、串联的 CLI 命令 / Skill满足"≥3 步"
| Step | 命令域 | 具体调用 | 类型 |
|:----:|:------:|---------|:----:|
| 1 | pr | `pr +view` / `+files` / `+diff` | 采集 |
| 2 | code-review | Review 分析逻辑(分级找问题) | AI 分析 |
| 3 | ci | `ci +builds` / `+log` | 采集 |
| 4 | api | `POST .../pulls/:id/reviews` | 写(评论) |
| 5 | pr | `pr +merge`(达标确认后) | 写(合并) |
> **共串联 4 个命令域 + 5 个步骤 + 2 处写操作**,远超任务三"≥3 步"要求。
---
## 三、与子任务二的区别(关键)
| 维度 | 子任务二 Skill如 code-review | 子任务三 本工作流pr-guard |
|------|--------------------------------|-----------------------------|
| **交付单位** | 单个 Skill | 串联多步的**完整解决方案** |
| **职责** | 只做 Review | Review + CI + 评论 + 合并决策 |
| **触发** | 用户要 Review | PR 提交自动跑完整流水线 |
| **决策** | 输出意见 | **质量门禁判定(通过/拒绝/合并)** |
> code-review 是"审查员"pr-guard 是"看门人"——后者在前者基础上加了 CI 维度和合并决策,形成完整门禁。
---
## 四、可复现性(对应交付要求)
| 要求 | 满足方式 |
|------|---------|
| 串联 ≥3 步 CLI/Skill | 5 步、4 域 ✅ |
| 含自定义 Skill 兼容 Agent | `gitlink-pr-guard` SKILL.mdClaude Code 可读)✅ |
| 可复现执行脚本 | `demo/pr-guard-workflow.sh`(参数化)✅ |
| 真实 GitLink 项目演示 | 登录后对真实 PR 运行(见下) |
| 工作流说明 + 架构图 | 本文档 ✅ |
---
## 五、真实演示步骤(登录后)
```bash
# 1. 登录
gitlink-cli auth login
# 2. 找一个真实 PR
gitlink-cli pr +list --owner <owner> --repo <repo> --state open
# 3. 跑质量看门人流水线脚本采集AI 在 Claude Code 做 Step2 分析)
bash demo/pr-guard-workflow.sh <owner> <repo> <pr_id>
# 或在 Claude Code 里自然语言触发:
# "读 skills/gitlink-pr-guard/SKILL.md帮我把关 <owner>/<repo> 的 PR #42"
```
**预期 AI 行为**:读 pr-guard SKILL.md → 按工作流跑 5 步 → 输出质量看门人报告 + 判定(通过/拒绝)+ 合并建议。
---
## 六、交付清单
- [x] `skills/gitlink-pr-guard/SKILL.md` — 工作流定义 + 门禁规则 + 报告模板
- [x] `demo/pr-guard-workflow.sh` — 可复现脚本5 步串联)
- [x] `demo/pr-guard-architecture.md` — 本文档(说明 + 架构图)
- [ ] 真实项目演示(登录后运行 + 截图/录屏)
- [ ] 报告(暂缓,后续按统一策略补《新需求构思》《变更影响测试》)

76
demo/pr-guard-workflow.sh Normal file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env bash
# ============================================================
# 代码质量看门人 · 端到端工作流脚本(子任务三)
# 串联 5 步:采集 PR → AI Review → CI 检查 → 汇总评论 → 质量判定
# 用法bash demo/pr-guard-workflow.sh <owner> <repo> <pr_id>
# 例bash demo/pr-guard-workflow.sh myorg myproject 42
# 前置gitlink-cli auth login涉及平台 API
# 说明采集命令真实执行Review 分析由 AI Agent读 pr-guard/SKILL.md完成
# ============================================================
_DIR="$(cd "$(dirname "$0")" && pwd)"
CLI="$_DIR/../gitlink-cli.exe"; [ -f "$CLI" ] || CLI="$_DIR/../gitlink-cli" # Win→.exeLinux→无后缀
OWNER="${1:-}"; REPO="${2:-}"; PR_ID="${3:-}"
G='\033[0;32m'; Y='\033[1;33m'; C='\033[0;36m'; R='\033[0;31m'; B='\033[1m'; N='\033[0m'
banner() { echo -e "\n${B}══════════════════════════════════════════════════════${N}"; echo -e "${B} $1${N}"; echo -e "${B}══════════════════════════════════════════════════════${N}"; }
step() { echo -e "\n${C}━━━ Step $1 ━━━ ${B}$2${N}"; }
ai() { echo -e "🤖 ${G}AI读 pr-guard/SKILL.md 后):${N} $1"; }
run() { echo -e "${Y} $1${N}"; eval "$1" 2>&1 | head -16; echo; }
# ---------- 前置检查 ----------
banner "代码质量看门人 · PR #${PR_ID:-?} 质量流水线"
[ -f "$CLI" ] || { echo -e "${R}✗ 找不到 gitlink-cli${N}"; exit 1; }
if [ -z "$OWNER" ] || [ -z "$REPO" ] || [ -z "$PR_ID" ]; then
echo -e "${R}用法: bash $0 <owner> <repo> <pr_id>${N}"
echo -e "${R}例 : bash $0 myorg myproject 42${N}"; exit 1
fi
echo -e "${G}${N} 目标: ${B}$OWNER/$REPO${N} PR #${B}$PR_ID${N}"
# ---------- Step 1采集 PR 变更 ----------
step 1 "采集 PR 变更pr +view / +files / +diff"
ai "先拉 PR 详情、变更文件、diff 统计,作为审查输入。"
run "\"$CLI\" pr +view --id $PR_ID --format json"
run "\"$CLI\" pr +files --id $PR_ID --format json"
run "\"$CLI\" pr +diff --id $PR_ID --stat"
# ---------- Step 2AI Review复用 code-review 逻辑)----------
step 2 "AI Review按 code-review 分级找问题)"
ai "逐文件分析 diff按安全红线/错误处理/规范分级。这一步由 AI Agent 完成(读 code-review/SKILL.md。"
echo -e " ${C}分级框架${N}"
echo -e " 🔴 Critical硬编码密钥 / SQL·命令注入 / 路径遍历(安全红线,阻断合并)"
echo -e " 🟡 Warning :错误处理缺失 / 边界条件 / 明文敏感信息"
echo -e " 🔵 Suggestion命名 / 性能 / 可配置化"
echo -e " ${C}Agent 在此输出分级清单(示例见 SKILL.md 输出模板)${N}"
# ---------- Step 3CI 检查 ----------
step 3 "CI 检查ci +builds"
ai "查 PR 对应分支的最新构建状态,作为门禁第二维。"
run "\"$CLI\" ci +builds --owner $OWNER --repo $REPO --format json"
echo -e " ${C}判定:从返回按 source_branch 匹配最新构建 → success / failure / pending${N}"
# ---------- Step 4发布汇总评论 ----------
step 4 "发布质量看门人报告api POST .../reviews"
ai "把 Review 意见 + CI 状态 + 质量判定组装成报告,评论到 PR。"
echo -e "${Y} gitlink-cli api POST /$OWNER/$REPO/pulls/$PR_ID/reviews --body '<报告>'${N}"
echo -e " ${C}报告含${N}:质量判定 + Critical/Warning 清单 + CI 状态 + 处置建议"
echo -e " ${C}[实演时此处真实发送;脚本演示仅展示结构]${N}"
# ---------- Step 5质量判定 ----------
step 5 "质量判定(门禁规则 → 合并 / 请求修改)"
ai "按门禁规则决策。注意:合并是写操作,默认只建议,确认后才执行。"
echo -e " ${C}门禁规则${N}"
echo -e " 0 Critical + CI success → ✅ 通过,建议合并"
echo -e " 有 Critical 任一 → 🔴 拒绝,请求修改"
echo -e " CI failure → 🔴 拒绝,附 CI 日志"
echo -e " 仅 Warning/Suggestion → 🟡 通过(带建议)"
echo ""
echo -e " ${G}若判定通过 + 用户确认 →${N} ${Y}gitlink-cli pr +merge --id $PR_ID --method squash${N}"
# ---------- 总结 ----------
banner "流水线完成"
echo -e "${B}代码质量看门人${N} 串联了 ${B}5 步${N},覆盖 ${B}4 个 CLI 域${N}"
echo -e " pr采集/合并)+ code-reviewReview+ ci构建+ api评论"
echo -e "\n${C}真实演示${N}:登录后对本仓库一个真实 PR 跑此脚本,由 AI 完成 Step 2 分析。"
echo -e "详见 ${Y}pr-guard-architecture.md${N}(工作流说明 + 架构图)"
read -p "按回车键继续..."

View File

@ -0,0 +1,157 @@
# 科研仓库画像 · 使用指南(子任务四)
> 把 GitLink 上的开源科研项目,转化成**可评估、可引用、可复现**的科研画像。
> 对应 Skill`skills/gitlink-research-insight/SKILL.md`;脚本:`demo/research-insight-workflow.sh`。
> 定位:科研辅助(加分项),面向科研工作者/课题组,无需 GoMarkdown + CLI。
---
## 一、它能做什么
科研工作者面对 GitLink 上的一个开源科研项目,常问三个问题:
1. **能不能复现?**(别人的实验我跑得出同样结果吗)
2. **能不能引用?**(写论文能引这个项目/代码吗,引哪个版本)
3. **能不能合作?**(项目活跃吗,核心圈开放吗)
本工具用 gitlink-cli 采集仓库数据,从**可复现性 / 活跃度 / 引用价值 / 协作健康**四个维度评估,并生成**贡献者协作知识图谱**,产出一份《科研仓库画像报告》。
---
## 二、快速使用
```bash
# 1. 登录
gitlink-cli auth login
# 2. 对一个科研仓库跑画像脚本采集AI 评分)
bash demo/research-insight-workflow.sh <owner> <repo>
# 或在 Claude Code 里自然语言触发:
# "读 skills/gitlink-research-insight/SKILL.md帮我评估 <owner>/<repo> 这个科研项目值不值得引用和复现"
```
---
## 三、工作流架构
```
┌──────────────────────────────────────┐
│ 输入:一个 GitLink 科研仓库 │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ Step 0 fork 检测(避免给 fork 错评) │
│ repo +info → fork_info │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ Step 1 数据采集(只读) │
│ repo +info/languages/contributors │
│ issue +list / pr +list │
│ file +get LICENSE/CI / release +list │
│ git clone 兜底repo +commits 不存在)│
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ Step 2 四维科研评分 │
│ 🔁 可复现性 📈 活跃度 │
│ 📑 引用价值 🤝 协作健康 │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ Step 3 协作知识图谱mermaid
│ 贡献者协作网络 + 巴士因子 │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ Step 4 科研仓库画像报告 │
│ 定性 + 综合评分 + 四维详情 + 建议 │
└──────────────────────────────────────┘
```
---
## 四、四维评分体系(科研创新点)
### 🔁 可复现性(满分 10科研最核心
| 检查项 | 分值 | 判定方法 |
|--------|:---:|---------|
| CI 配置 | +2 | `.gitea/workflows``.github/workflows` 存在 |
| 依赖锁定 | +2 | `go.sum` / `package-lock.json` / `requirements.txt` |
| 数据/数据说明 | +2 | README 提及数据集,或有 `data/` |
| 运行文档 | +2 | README 有安装/运行/环境说明 |
| 版本归档 | +2 | 有 release/tag可引用固定版本 |
### 📈 活跃度
近 3 月提交频率 + Issue/PR 活跃 + 贡献者趋势
### 📑 引用价值
LICENSE + 版本归档 + 文档完整 + 星标/关注
### 🤝 协作健康
Issue 响应时长 + PR 合并率 + **巴士因子**(核心贡献者提交占比,越分散越健康)
> **与 gitlink-health 区别**health 看"工程维护好不好",本工具看"科研上值不值得用"(可复现/引用/合作)。
---
## 五、报告样例(填好的示例)
```markdown
# 🔬 科研仓库画像 — someresearch/awesome-paper-code
> 一句话定性:这是一个活跃维护的 NLP 科研项目,适合引用与复现。
## 📊 综合评分:⭐ 8.5/10
| 维度 | 得分 | 评价 |
|------|:---:|------|
| 🔁 可复现性 | 8/10 | CI+依赖锁定+数据说明齐全,可复现 |
| 📈 活跃度 | 9/10 | 近3月持续提交活跃 |
| 📑 引用价值 | 9/10 | MIT 协议 + v2.1 版本,适合引用 |
| 🤝 协作健康 | 8/10 | 巴士因子健康(核心占 45% |
## 🔁 可复现性详情
- CI✅ GitHub Actions
- 依赖锁定:✅ requirements.txt
- 数据说明:✅ README 有数据集下载指引
- 版本归档:✅ 最新 v2.1
## 💡 给科研工作者的建议
1. 引用:建议引 v2.1MIT 协议)
2. 复现:按 README + CI 可复现
3. 合作:协作网络开放,可联系 @核心贡献者
```
---
## 六、真实仓库验证步骤(交付要求)
```bash
# 1. 选一个 GitLink 上的科研类仓库(有 LICENSE/CI/数据 的最佳)
# 2. 跑画像脚本
bash demo/research-insight-workflow.sh <科研仓库owner> <科研仓库>
# 3. 在 Claude Code 让 AI 读 SKILL.md 完成评分,产出报告
# 4. 截图/录屏报告 → 作为演示成果
```
---
## 七、与子任务二/三的关系
| 子任务 | 视角 | 本任务复用 |
|--------|------|-----------|
| 任务二 health | 工程健康度 | 复用其指标采集 + 加科研维度 |
| 任务三 pr-guard | 端到端工作流 | 复用"多步串联 + Skill + 脚本"模式 |
| **任务四 本工具** | 科研价值 | 综合复用,加可复现性/引用/知识图谱 |
---
## 八、交付清单
- [x] `skills/gitlink-research-insight/SKILL.md` — 科研洞悉 Skill自定义遵循规范
- [x] `demo/research-insight-workflow.sh` — 可复现脚本5 步fork 检测 + 采集 + 评分 + 图谱 + 报告)
- [x] `demo/research-insight-guide.md` — 本文档(完整中文 + 架构 + 报告样例)
- [x] 真实科研仓库验证 — 已对 `whale_hihihi/gitlink-cli` 跑通,见 `demo/research-insight-verification.md` + 报告 `demo/research-insight-report-whale_gitlink-cli.md`
- [x] 命令缺陷修复 — SKILL.md 与脚本原用的 `repo +commits`/`+raw`/`+tags` 三条不存在命令,已改为 `file +get`/`release +list`/`git clone` 兜底并实测通过
- [ ] 演示录屏/截图(脚本与 AI 评分均可现场复现)

View File

@ -0,0 +1,87 @@
# 🔬 科研仓库画像 — whale_hihihi/gitlink-cli
> **一句话定性**:这是官方 `Gitlink/gitlink-cli` 的一个 **fork**(团队/课程竞赛工作副本),是一个 **工程类 CLI 工具 + AI Agent Skills 集合**,并非科研产出(无数据集/实验/论文/CITATION。**可高置信复现与构建,但不适合作为独立科研对象引用——引用应指向 upstream。**
>
> *由 gitlink-research-insight Skill 评估,数据采集于 2026-07-05仓库最近提交 2026-07-02仍活跃。*
## 📊 综合评分:⭐ 6.5/10作为工程仓库优秀作为"科研引用对象"偏低)
| 维度 | 得分 | 评价 |
|------|:---:|------|
| 🔁 可复现性 | 8/10 | CI + go.sum + 一键 npm 安装 + 完整文档,构建复现性强 |
| 📈 活跃度 | 9/10 | 近 3 个月 ~300 次提交、102 个 PR 合并,当日仍在提交 |
| 📑 引用价值 | 3/10 | 是 fork、无 release、无 CITATION、0 星标,引用应指 upstream |
| 🤝 协作健康 | 6/10 | 多人 PR 流程活跃,但核心维护者高度集中、无 Issue 追踪 |
## 📋 基础信息
| 项 | 值 |
|----|-----|
| 描述 | gitlink-cli 是 GitLink 平台的官方 CLIfork 自 `Gitlink/gitlink-cli`project_id 1513956 |
| 主要语言 | Go 97.3% · JavaScript 2.2% · Shell 0.4% · Makefile 0.1% |
| 许可证 | Mulan PSL v2木兰宽松许可证清晰开源 |
| 贡献者数 | 20按提交计GitLink 注册用户 3 |
| 版本归档 | ⚠️ 1 个 tag`v0.1.14`),但 **0 个正式 release** |
| 规模 / 关注 | 26 MB · ⭐ 0 · 👁 0 · fork 0 |
## 🔁 可复现性详情(科研核心)
| 检查项 | 判定 | 依据 |
|--------|------|------|
| CI 配置 | ✅ 有 | `.gitea/workflows/ci.yml`GitLink+ `.github/workflows/{ci,release,test}.yml`,跑 Build/Lint/Test/fmtGo 1.22 |
| 依赖锁定 | ✅ go.sum | Go 模块校验文件齐全 |
| 数据说明 | ❌ N/A | 是 CLI 工具,本无数据集(评分表工程类适配,不扣分) |
| 运行/环境文档 | ✅ 完整 | README 33KB + zh-CN含 RequirementsNode 14+/Go 1.26+、Installation、Quick Start、Makefile、Dockerfile |
| 版本归档 | ⚠️ 部分 | 有 tag `v0.1.14` 可 pin但无 release 产物 |
> **可复现性结论****构建/运行层面极易复现**——`npm install -g @gitlink-ai/cli` 一键装、CI 全绿、文档完备。"复现实验结果"不适用(非实验型科研)。
## 📈 活跃度详情
- 提交分布2026-04 → ~16 次2026-05 → ~140 次2026-06 → ~133 次,持续走高
- 近 3 月提交 ~300 次,**最近一次提交 2026-07-02**(评估前 3 天)
- 102 个 PR 合并提交20 名贡献者——非常活跃,但仓库仅诞生 ~2.5 个月,属**新兴-高活跃**
## 🤝 协作网络图
```mermaid
graph LR
subgraph 核心["核心圈(提交+评审)"]
T[wbtiger<br/>~38% commits / 主评审]
S[Surponess<br/>提交+评审]
W[whale<br/>提交+评审]
X[wauxing<br/>重构/合并 upstream]
end
subgraph 外围["PR 贡献者fork/branch 提交)"]
Y[wangyue111]
M[muel]
G[yangsai/Mengz]
O[其余 10+ 人]
end
R((whale_hihihi/gitlink-cli))
T -->|主提交+主评审| R
S -->|提交+评审| R
W -->|提交+评审| R
X -->|重构/合并| R
Y & M & G & O -.->|PR| R
T -.评审/合并.-> Y
T -.评审/合并.-> M
```
- **巴士因子**top1 `wbtiger` 占 ~38% 提交;评审更集中——`wbtiger` 一人合并了多数 PR。
- ⚠️ **单点风险**:未越过"单人 >50%"红线,但核心维护者高度依赖 `wbtiger` 一人,且**无 Issue 追踪**issues_count=0协作瓶颈与知识单点并存。
## 💡 给科研工作者的建议
1. **引用**:❌ **不建议引用本 fork**。它是 `Gitlink/gitlink-cli` 的工作副本,无独立学术贡献、无 CITATION.cff、无 release、0 社区关注。如需引用该工具,**请引用 upstream `Gitlink/gitlink-cli`**,按 Mulan PSL v2 协议、pin 到 `v0.1.14` tag。
2. **复现/构建**:✅ **强烈可复现**。`npm install -g @gitlink-ai/cli` 或 `make build`Go 1.22+),配合 CI 与 README 几分钟可跑通。这是该仓库最大优点。
3. **合作**:⚠️ 协作网络活跃但**核心圈封闭**——PR 走 fork 流程规范,但评审权集中在 `wbtiger`;新贡献者门槛中等。
---
### 执行备注skill 命令缺陷,已修复)
按 skill 工作流执行时,原 SKILL.md/脚本的 `repo +commits`/`+tags`/`+raw` 三条命令在本二进制中不存在GitLink 的 commits/pulls API 路径亦返回 HTML。本次评估改用 `file +get`(读文件)、`release +list`(版本)、`git clone`(提交时间线)完成采集,缺陷已同步修复进 SKILL.md 与脚本。详见 `research-insight-verification.md`
---
*由 gitlink-research-insight 科研画像生成 · 评估对象 whale_hihihi/gitlink-cli · 2026-07-05*

View File

@ -0,0 +1,104 @@
# 任务四 · research-insight 真实验证记录
> 对应 Skill`skills/gitlink-research-insight/SKILL.md`;脚本:`demo/research-insight-workflow.sh`。
> 验证对象:`whale_hihihi/gitlink-cli`GitLink 上的真实仓库)。
> 验证时间2026-07-05。方法对照模板法见《验证剧本.md》三层证据
> 配套报告原件:`demo/research-insight-report-whale_gitlink-cli.md`。
---
## 一、验证对象与结论
| 项 | 值 |
|----|----|
| 验证仓库 | `whale_hihihi/gitlink-cli`(公开、真实、有 LICENSE/CI/贡献者) |
| 关键发现 | 是 `Gitlink/gitlink-cli`**fork** —— 触发 SKILL.md「Step 0 fork 检测」,引用价值改评 upstream |
| 总体结论 | ✅ **达标**:命令层 / 编排层 / 输出层 三层证据齐备,报告符合 SKILL.md 模板 |
| 附带产出 | 发现并修复 SKILL.md + 脚本 3 条不存在命令(见第四节) |
---
## 二、三层证据(对照《验证剧本.md》模型
| 层 | 证明什么 | 证据 | 结果 |
|----|---------|------|:---:|
| ① 命令层 | 命令真实执行、返回真实数据 | `repo +info/+languages/+contributors`、`file +get LICENSE`、`repo +tree .gitea/workflows`、`release +list`、`issue/pr +list` 均返回真实 JSON非 404/unknown | ✅ |
| ② 编排层 | AI 按 SKILL.md **顺序**调命令 | 触发语后AI 严格走 Step 0(fork 检测) → Step 1(采集) → Step 2(四维评分) → Step 3(协作图谱) → Step 4(报告) | ✅ |
| ③ 输出层 | 输出**符合 SKILL.md 模板** | 报告含:综合评分 + 四维得分表 + 可复现性逐项 + mermaid 协作图 + 引用/复现/合作建议 + fork 改评 upstream 结论 | ✅ |
> ③ 是判定「达到效果」的硬标准——AI 实际输出与 SKILL.md「输出模板」并排对照关键字段全部命中。
---
## 三、命令层证据(采集到的真实数据片段)
```bash
# fork 检测Step 0脚本正确识别为 fork
⚠️ whale_hihihi/gitlink-cli 是 Gitlink/gitlink-cli 的 fork —— 引用价值应改评 upstream Gitlink/gitlink-cli
# repo +info 关键字段
contributor_users_count: 3 # GitLink 注册贡献者
pull_requests_count: 21 # PR 计数
version_releases_count: 0 # 无正式 release
fork_info.fork_project_user_login: "Gitlink" # ← fork 来源
# repo +languages
Go 97.3% / JavaScript 2.2% / Shell 0.4% / Makefile 0.1%
# repo +contributors巴士因子输入
wbtiger 131 38.42% ← 核心,单人 <50%未越红线
wauxing 35 10.x %
whale 34 10.x %
...
# file +get LICENSE → Mulan PSL v2木兰宽松许可证
# repo +tree .gitea/workflows → ci.ymlCI 配置存在)
# release +list → releases: [](确认 0 release与 info 一致)
# git 兜底repo +commits 不存在)
近 3 月提交: 300 次 | 最近提交: 2026-07-02评估前 3 天) | tag: v0.1.14 | PR 合并数: 102
```
---
## 四、验证中发现的命令缺陷与修复(重要)
验证过程暴露 SKILL.md 与脚本原用了 **3 条 gitlink-cli 不存在的命令**,脚本 Step 1 会连续报错、采不到数据。已全部修复并实测通过:
| 原命令(不存在) | 现象 | 修复后(可用) | 实测 |
|------------------|------|----------------|:---:|
| `repo +commits --limit` | `unknown flag`,无此子命令 | `git clone --depth 100` + `git log`API 无 commits JSON 端点) | ✅ 300 次/3 月 |
| `repo +raw --path` | 无此子命令 | `file +get --path`(自动解码 base64+ `repo +tree --path` 列目录 | ✅ 读到 LICENSE |
| `repo +tags` | 无此子命令 | `release +list` + `repo +info.version_releases_count` | ✅ 确认 0 release |
**同步修复的文件**
- `skills/gitlink-research-insight/SKILL.md` —— Step 1 命令清单 + 降级方案 + 工程类仓库评分适配(已由前期协作完成)
- `demo/research-insight-workflow.sh` —— Step 1 采集命令 + 新增 Step 0 fork 检测
- `demo/research-insight-guide.md` —— 架构图 + 交付清单
**另发现的设计缺口(已在 SKILL.md 补)**
- 原 Skill 无 **fork 检测**步骤 → 对 fork 仓库会给出错误引用结论;已加 Step 0。
- 评分表「数据/数据获取说明」项面向数据科学项目,套到 CLI 工具会扣冤枉分 → 已加「工程类 N/A」适配。
---
## 五、脚本一键复现
```bash
# 登录后对任意 GitLink 仓库复现本次验证
bash demo/research-insight-workflow.sh whale_hihihi gitlink-cli
# AI 层(四维评分 + 协作图谱 + 报告)在 Claude Code 触发:
# 读 skills/gitlink-research-insight/SKILL.md评估 whale_hihihi/gitlink-cli 值不值得引用和复现
```
---
## 六、对照《验证剧本.md》结论表任务四行
| 任务 | 验证项 | 命令层 | 编排层 | 输出层 | 结论 |
|------|--------|:---:|:---:|:---:|------|
| 四 | research-insight | ✅ | ✅ | ✅ | 四维画像达标(含 fork 检测 + 命令缺陷修复) |
---
*本验证记录于 2026-07-05 生成,数据为当日实采。仓库活跃(最近提交 2026-07-02数字可能随时间增长。*

View File

@ -0,0 +1,104 @@
#!/usr/bin/env bash
# ============================================================
# 科研仓库画像 · 端到端工作流脚本(子任务四)
# 4 步:采集数据 → 四维评分 → 协作图谱 → 科研画像报告
# 用法bash demo/research-insight-workflow.sh <owner> <repo>
# 例bash demo/research-insight-workflow.sh someresearch awesome-paper-code
# 前置gitlink-cli auth login只读分析不改数据
# 说明:采集命令真实执行;四维评分 + 协作图谱由 AI读 research-insight/SKILL.md完成
# ============================================================
_DIR="$(cd "$(dirname "$0")" && pwd)"
CLI="$_DIR/../gitlink-cli.exe"; [ -f "$CLI" ] || CLI="$_DIR/../gitlink-cli" # Win→.exeLinux→无后缀
OWNER="${1:-}"; REPO="${2:-}"
G='\033[0;32m'; Y='\033[1;33m'; C='\033[0;36m'; R='\033[0;31m'; B='\033[1m'; N='\033[0m'
banner() { echo -e "\n${B}══════════════════════════════════════════════════════${N}"; echo -e "${B} $1${N}"; echo -e "${B}══════════════════════════════════════════════════════${N}"; }
step() { echo -e "\n${C}━━━ Step $1 ━━━ ${B}$2${N}"; }
ai() { echo -e "🤖 ${G}AI读 research-insight/SKILL.md 后):${N} $1"; }
run() { echo -e "${Y} $1${N}"; eval "$1" 2>&1 | head -14; echo; }
# ---------- 前置检查 ----------
banner "🔬 科研仓库画像 · $OWNER/${REPO:-?}"
[ -f "$CLI" ] || { echo -e "${R}✗ 找不到 gitlink-cli${N}"; exit 1; }
if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
echo -e "${R}用法: bash $0 <owner> <repo>${N}"
echo -e "${R}例 : bash $0 someresearch awesome-paper-code${N}"; exit 1
fi
echo -e "${G}${N} 分析对象: ${B}$OWNER/$REPO${N}(只读,不改数据)"
# ---------- Step 0fork 检测(避免给 fork 错评) ----------
step 0 "fork 检测(引用价值要改评 upstream"
ai "先看 repo +info 的 fork_info。是 fork 则引用价值/活跃度改评 upstream。"
INFO="$("$CLI" repo +info --owner "$OWNER" --repo "$REPO" --format json 2>/dev/null)"
UPSTREAM="$(printf '%s' "$INFO" | grep -o '"fork_project_user_login": *"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//')"
# fork_project_user_login 缺失或为 null → 非空才算 fork
[ "$UPSTREAM" = "null" ] && UPSTREAM=""
if [ -n "$UPSTREAM" ]; then
echo -e " ${R}⚠️ $OWNER/$REPO${B}$UPSTREAM/$REPO${N}${R} 的 fork —— 引用价值应改评 upstream ${B}$UPSTREAM/$REPO${N}"
else
echo -e " ${G}${N} 独立仓库(非 fork正常评估"
fi
# ---------- Step 1采集科研仓库数据 ----------
step 1 "采集科研仓库数据repo / file / issue / pr + 本地 git 兜底)"
ai "拉基础画像、活跃度、合规复现性数据,作为科研评估输入。"
echo -e "${Y} 基础画像repo +info / +languages / +contributors${N}"
"$CLI" repo +info --owner "$OWNER" --repo "$REPO" --format json 2>&1 | head -14
"$CLI" repo +languages --owner "$OWNER" --repo "$REPO" --format json 2>&1 | head -8
"$CLI" repo +contributors --owner "$OWNER" --repo "$REPO" --format json 2>&1 | head -12
echo -e "\n${Y} 复现性文件file +get 读 LICENSE / CI —— 替代不存在的 repo +raw${N}"
"$CLI" file +get --owner "$OWNER" --repo "$REPO" --path LICENSE --format json 2>&1 | head -3
"$CLI" repo +tree --owner "$OWNER" --repo "$REPO" --path .gitea/workflows --format json 2>&1 | head -6
echo -e "\n${Y} 版本归档release +list —— 替代不存在的 repo +tags${N}"
"$CLI" release +list --owner "$OWNER" --repo "$REPO" --format json 2>&1 | head -6
echo -e "\n${Y} 活跃度issue/pr + git 兜底 —— repo +commits 不存在)${N}"
"$CLI" issue +list --owner "$OWNER" --repo "$REPO" --format json 2>&1 | head -8
"$CLI" pr +list --owner "$OWNER" --repo "$REPO" --format json 2>&1 | head -8
echo -e " ${C}repo +commits 不存在 → 用 git clone 兜底读提交时间线${N}"
TMP="/tmp/${OWNER}-${REPO}-analyze"; rm -rf "$TMP"
if git clone --quiet --depth 100 "https://gitlink.org.cn/$OWNER/$REPO.git" "$TMP" 2>/dev/null; then
echo -e " 近 3 月提交: $(git -C "$TMP" log --oneline --since='3 months ago' 2>/dev/null | wc -l)"
echo -e " 最近提交 : $(git -C "$TMP" log -1 --format='%ci %an' 2>/dev/null)"
echo -e " tag 列表 : $(git -C "$TMP" tag 2>/dev/null | tr '\n' ' ')"
echo -e " PR 合并数 : $(git -C "$TMP" log --merges --oneline 2>/dev/null | wc -l)"
else
echo -e " ${R}✗ git clone 失败(无 git 或无网络)→ 活跃度改用 repo +info 计数近似${N}"
fi
# ---------- Step 2四维科研评分 ----------
step 2 "四维科研评分AI 按指标体系打分)"
ai "对采集数据按科研四维评分。这一步由 AI 完成(指标体系见 SKILL.md。"
echo -e " ${C}🔁 可复现性${N}科研核心满分10CI(+2) / 依赖锁定(+2) / 数据说明(+2) / 运行文档(+2) / 版本归档(+2)"
echo -e " ${C}📈 活跃度${N}近3月提交频率 + Issue/PR 活跃 + 贡献者趋势"
echo -e " ${C}📑 引用价值${N}LICENSE + 版本归档 + 文档完整 + 星标"
echo -e " ${C}🤝 协作健康${N}Issue响应 + PR合并率 + 巴士因子(核心贡献者占比)"
echo -e " ${C}Agent 在此输出各维度得分 + 判定依据${N}"
# ---------- Step 3协作知识图谱 ----------
step 3 "协作知识图谱(贡献者协作网络)"
ai "从贡献者 + PR 协作数据生成 mermaid 协作网络,呼应『知识图谱』要求。"
cat <<'MERMAID'
graph LR
A[核心贡献者1] -->|主提交| P((项目))
B[核心贡献者2] -->|主提交| P
C[偶发贡献者] -->|贡献| P
A -.评审.-> C
B -.评审.-> C
MERMAID
echo -e " ${C}巴士因子${N}:核心贡献者提交占比 → <健康 / 单点风险>"
# ---------- Step 4科研画像报告 ----------
step 4 "生成科研画像报告"
ai "组装成《科研仓库画像报告》:一句话定性 + 综合评分 + 四维详情 + 协作图 + 引用/复现/合作建议。"
echo -e " ${C}报告含${N}:🔬综合评分 / 📋基础信息 / 🔁可复现性详情 / 🤝协作网络图 / 💡给科研工作者建议"
echo -e " ${C}模板见 SKILL.md「输出模板」+ research-insight-guide.md${N}"
# ---------- 总结 ----------
banner "分析完成"
echo -e "${B}科研仓库画像${N} 串联 ${B}5 步${N}fork 检测 + 采集 + 评分 + 图谱 + 报告),覆盖 CLI 域(只读):"
echo -e " repo / file / release / issue / pr + 本地 git提交时间线兜底repo +commits 不存在)"
echo -e "\n${C}科研视角创新${N}:可复现性评分 + 引用价值 + 协作知识图谱(区别于普通 health 工程视角)"
echo -e "${C}真实验证${N}:登录后对 GitLink 一个科研类仓库跑此脚本,由 AI 完成评分 → 产出报告 + 截图"
echo -e "详见 ${Y}research-insight-guide.md${N}(完整中文使用文档 + 报告样例)"
read -p "按回车键继续..."

94
demo/snippet-live-demo.sh Normal file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env bash
# ============================================================
# GitLink Skills 功能演示 —— snippet 完整闭环
# 核心卖点:AI 读取 SKILL.md → 自动编排 gitlink-cli 命令 → 完成完整场景
# 特点:snippet 是本地功能,无需登录,可安全现场实演
# 用法:bash demo/snippet-live-demo.sh
# ============================================================
# 不用 set -e:保证演示连续性,关键步骤手动检查
_DIR="$(cd "$(dirname "$0")" && pwd)"
CLI="$_DIR/../gitlink-cli.exe"; [ -f "$CLI" ] || CLI="$_DIR/../gitlink-cli" # Win→.exeLinux→无后缀
# ANSI 颜色
G='\033[0;32m'; Y='\033[1;33m'; C='\033[0;36m'; R='\033[0;31m'; B='\033[1m'; N='\033[0m'
banner() { echo -e "\n${B}══════════════════════════════════════════════════════${N}"; echo -e "${B} $1${N}"; echo -e "${B}══════════════════════════════════════════════════════${N}"; }
scene() { echo -e "\n${C}━━━ 场景 $1 ━━━ ${B}$2${N}"; }
user() { echo -e "🧑 ${B}用户:${N} $1"; }
ai() { echo -e "🤖 ${G}AI读 snippet/SKILL.md 后):${N} $1"; }
show() { echo -e "${Y} $1${N}"; }
# ---------- 前置检查 ----------
banner "GitLink Skills 演示 · snippet 闭环"
[ -f "$CLI" ] || { echo -e "${R}✗ 找不到 gitlink-cli: $CLI${N}"; exit 1; }
echo -e "${G}${N} gitlink-cli 就绪"
echo -e "${G}${N} snippet 为本地功能(${B}无需登录${N}),可安全现场演示"
echo -e "${G}${N} 演示数据用完即删,不污染环境"
# ---------- 场景 1创建 ----------
scene 1 "保存一段常用代码"
user "帮我存一段快速排序代码,语言 python标签 algorithm"
ai "决策 → \`snippet +create\`(⚠️ Write。SKILL.md 规则:--title 必填、--tags 逗号分隔、--language 标注。"
show "gitlink-cli snippet +create --title '快速排序(演示)' --language python --tags algorithm,demo --content '...'"
OUT=$("$CLI" snippet +create --title '快速排序(演示)' --language python --tags algorithm,demo \
--content 'def qs(a): return a if len(a)<2 else qs([x for x in a[1:] if x<=a[0]])+[a[0]]+qs([x for x in a[1:] if x>a[0]])' \
--format json 2>&1)
echo "$OUT" | head -12
DEMO_ID=$(echo "$OUT" | grep -oE '"id":[[:space:]]*"[0-9a-f]+"' | head -1 | grep -oE '[0-9a-f]{8}')
echo -e "${G}${N} 已创建id = ${B}$DEMO_ID${N}"
# ---------- 场景 2列表 ----------
scene 2 "浏览片段库"
user "我存了哪些片段?"
ai "决策 → \`snippet +list\`Read。可按 --tag / --language / --keyword 过滤。"
show "gitlink-cli snippet +list --format json"
"$CLI" snippet +list --format json 2>&1 | head -14
# ---------- 场景 3搜索 ----------
scene 3 "全文检索"
user "帮我找包含 '排序' 的片段"
ai "决策 → \`snippet +search\`Read全文匹配 title + content。"
show "gitlink-cli snippet +search --query '排序' --format json"
"$CLI" snippet +search --query '排序' --format json 2>&1 | head -10
# ---------- 场景 4查看详情 ----------
scene 4 "查看指定片段"
user "看看 id=$DEMO_ID 这个的详情"
ai "决策 → \`snippet +view --id\`Read。"
show "gitlink-cli snippet +view --id $DEMO_ID --format json"
"$CLI" snippet +view --id "$DEMO_ID" --format json 2>&1 | head -12
# ---------- 场景 5导出 ----------
scene 5 "导出到文件复用"
user "把它导出成文件,我要贴到项目里"
ai "决策 → \`snippet +export --output\`Read。SKILL.md默认输出到 stdout-o 写文件。"
TMP="$PWD/.demo_export_$$.py"
show "gitlink-cli snippet +export --id $DEMO_ID --output $TMP"
"$CLI" snippet +export --id "$DEMO_ID" --output "$TMP" >/dev/null 2>&1
echo -e "${G}${N} 已导出,文件内容:"; cat "$TMP"; rm -f "$TMP"
# ---------- 场景 6更新 ----------
scene 6 "更新片段字段"
user "给这个片段补个 tag 'sort'"
ai "决策 → \`snippet +update\`(⚠️ Write。--id 必填,至少一个字段。"
show "gitlink-cli snippet +update --id $DEMO_ID --tags algorithm,demo,sort"
"$CLI" snippet +update --id "$DEMO_ID" --tags algorithm,demo,sort --format json 2>&1 | head -8
# ---------- 场景 7删除清理----------
scene 7 "删除演示片段(清理)"
user "演示结束,删掉刚才的测试片段"
ai "决策 → \`snippet +delete\`(🔴 Destructive。SKILL.md删除不可逆建议先 view 确认。"
show "gitlink-cli snippet +delete --id $DEMO_ID"
"$CLI" snippet +delete --id "$DEMO_ID" --format json 2>&1 | head -4
echo -e "${G}${N} 演示数据已清理"
# ---------- 总结 ----------
banner "演示完成"
echo -e "${B}gitlink-snippet${N} Skill 的 7 个命令全部实测通过:"
echo -e " create / list / search / view / export / update / delete"
echo ""
echo -e "${B}核心价值${N}AI 读取 SKILL.md 后,能自动编排 gitlink-cli 命令完成完整场景,"
echo -e "输出严格符合 SKILL.md 定义的 envelope 格式 {\"ok\":true,\"data\":{...}}。"
echo -e "\n${C}其他 Skillonboarding / digest / todo涉及平台 API登录后可按其 SKILL.md 的「工作流」演示。${N}"
read -p "按回车键继续..."

56
demo/web/README.md Normal file
View File

@ -0,0 +1,56 @@
# GitLink CLI 智能化能力展示(演示网页)
一个**可交互的演示站**:点动词/敲命令 → 真跑 gitlink-cli → 显示真实输出,配合 25 域命令浏览器、48 Skill 卡片墙、pr-guard 流程、科研四维雷达,全面展示子任务一~四的成果。
> 位置:仓库内 `demo/web/``server.py` + `index.html`)。后端零依赖(仅 Python 标准库)。
## 架构(访客自带 token零凭据上云
```
浏览器 index.html ──fetch──▶ server.pyPython 标准库)
顶栏 token + owner/repo │ GET /api/skill 读 SKILL.md
命令域 / 终端 / Skill 墙 │ POST /api/run 真跑 CLItoken 透传给子进程)
pr-guard / 科研雷达 │ POST /api/analyze 四维评分 + 巴士因子
gitlink-cli仓库根 ../../gitlink-cli[.exe]
```
- 访客 token 仅存在**访客自己的浏览器**localStorage按请求传后端 → 注入子进程 `GITLINK_TOKEN` → 用完即弃,**不落服务端、不写日志**。
- 本地命令(`snippet`/`auth`)免 token 即可真跑;平台命令(`repo`/`issue`/`pr`…)需访客填自己的 token。
## 本地启动3 步)
```bash
# 1. 在仓库根编译 CLI已有可跳过
cd gitlink-cli # 仓库根(含 go.mod
go build -o gitlink-cli . # Windows 会生成 gitlink-cli.exe
# 2. 启动后端(零依赖)
cd demo/web
python server.py # → http://0.0.0.0:8000
# 3. 浏览器打开 http://localhost:8000
# 顶栏粘自己的 GitLink tokenauth login --token 拿)→ 平台命令即可真跑
```
> 服务端会自动探测二进制:`GITLINK_BIN` 环境变量 > 仓库根 `gitlink-cli`/`gitlink-cli.exe` > PATH。
> 端口/主机可设:`PORT=9000 HOST=127.0.0.1 python server.py`。
## 展示区
| 区块 | 内容 |
|------|------|
| ① 命令全域浏览器 | 25 域 160+ 动词,按子任务分组 + 搜索;点动词填终端真跑 |
| ② Skill 全集 | 48 个 Skill 卡片(按 全部/新增/科研/质量 筛选),点开读 SKILL.md 全文 |
| ③ pr-guard | 5 步门禁动画 + 「用真实 PR 跑」(填 token |
| ④ 科研画像 | 「实拉分析」目标仓库 → 四维雷达 + 协作网络 + 巴士因子 |
| ⑤ 验证 | 命令层 / 编排层 / 输出层 三层证据 |
## 安全
- 后端白名单(仅 30 个 gitlink-cli 顶层域)+ subprocess 列表参数(不经 shell+ 30s 超时。
- 访客 token 不落服务端。公网部署也**不烘焙任何团队 token**。
## 云端部署
见上级 [`demo/README.md`](../README.md)Dockerfile + `.devops` 流水线 + 服务器部署说明)。

437
demo/web/index.html Normal file
View File

@ -0,0 +1,437 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitLink CLI · 智能化能力展示</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --txt:#c9d1d9; --acc:#58a6ff; --green:#3fb950; --red:#f85149; --yellow:#d29922; }
body { background:var(--bg); color:var(--txt); font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
.panel { background:var(--panel); border:1px solid var(--border); }
.mono { font-family: "Cascadia Code", Consolas, "Courier New", monospace; }
#term-out { background:#010409; min-height:340px; max-height:460px; overflow-y:auto; padding:14px; }
.l-cmd { color:var(--acc); } .l-out { color:var(--txt); white-space:pre-wrap; word-break:break-all; }
.l-err { color:var(--red); } .l-muted { color:#6e7681; font-style:italic; }
#term-input { background:#010409; border-top:1px solid var(--border); }
#term-input:focus-within { outline:none; }
.skill-card { transition:.2s; }
.skill-card:hover { border-color:var(--acc); transform:translateY(-2px); }
.step { transition:.3s; }
.step.active { border-color:var(--green); background:#1c2820; }
.btn-cmd { transition:.15s; }
.btn-cmd:hover { background:#21262d; border-color:var(--acc); }
.verb { font-size:11px; padding:2px 7px; border-radius:4px; border:1px solid var(--border); background:#0d1117; cursor:pointer; transition:.15s; }
.verb:hover { border-color:var(--acc); color:var(--acc); }
.chip { background:#21262d; border:1px solid var(--border); }
.fbtn { font-size:12px; padding:4px 12px; border-radius:9999px; border:1px solid var(--border); cursor:pointer; transition:.15s; }
.fbtn.active { background:var(--acc); color:#000; border-color:var(--acc); }
#skill-modal-body h1,#skill-modal-body h2,#skill-modal-body h3,#skill-modal-body h4 { color:#fff; margin:.7em 0 .35em; font-weight:600; }
#skill-modal-body h1 { font-size:1.3em; border-bottom:1px solid var(--border); padding-bottom:.2em; }
#skill-modal-body h2 { font-size:1.15em; } #skill-modal-body h3 { font-size:1.02em; }
#skill-modal-body p { margin:.4em 0; line-height:1.6; }
#skill-modal-body table { border-collapse:collapse; margin:.5em 0; display:block; overflow-x:auto; }
#skill-modal-body th,#skill-modal-body td { border:1px solid var(--border); padding:4px 8px; text-align:left; font-size:.85em; }
#skill-modal-body th { background:#21262d; }
#skill-modal-body code { background:#010409; padding:1px 5px; border-radius:3px; color:var(--green); font-size:.85em; }
#skill-modal-body pre { background:#010409; padding:10px; border-radius:6px; overflow-x:auto; margin:.5em 0; border:1px solid var(--border); }
#skill-modal-body pre code { background:none; padding:0; color:var(--txt); }
#skill-modal-body strong { color:#fff; }
#skill-modal-body blockquote { border-left:3px solid var(--acc); padding-left:10px; color:#8b949e; margin:.5em 0; }
#skill-modal-body ul,#skill-modal-body ol { padding-left:1.4em; margin:.4em 0; }
#skill-modal-body hr { border-color:var(--border); margin:.8em 0; }
#skill-modal-body a { color:var(--acc); }
</style>
</head>
<body class="min-h-screen">
<!-- Header -->
<header class="border-b border-[var(--border)] panel">
<div class="max-w-7xl mx-auto px-6 py-5 flex items-center justify-between flex-wrap gap-4">
<div>
<h1 class="text-2xl font-bold text-white">🚀 GitLink CLI · 智能化能力展示</h1>
<p class="text-sm text-[#8b949e] mt-1">从「开发者工具」升级为「AI 可驱动平台」—— 命令 · Skills · 工作流 · 科研辅助</p>
</div>
<div class="flex gap-2 flex-wrap text-xs">
<span class="chip px-3 py-1.5 rounded-full">🛠 25 域 / 160+ 命令</span>
<span class="chip px-3 py-1.5 rounded-full">🧠 48 Skills</span>
<span class="chip px-3 py-1.5 rounded-full">🚪 pr-guard 工作流</span>
<span class="chip px-3 py-1.5 rounded-full">🔬 科研四维画像</span>
</div>
</div>
<!-- 顶栏:访客 token + 目标仓库 -->
<div class="max-w-7xl mx-auto px-6 pb-4 flex items-center gap-3 flex-wrap text-sm">
<span class="text-[#8b949e] whitespace-nowrap">🔑 GitLink Token</span>
<input id="token-input" type="password" placeholder="粘贴你的 GitLink 个人访问令牌(仅存本机浏览器)" class="flex-1 min-w-[220px] bg-[#0d1117] border border-[var(--border)] rounded px-3 py-1.5 text-xs mono">
<span class="text-[#8b949e] whitespace-nowrap">📦 目标仓库</span>
<input id="owner-input" value="Gitlink" class="w-28 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1.5 text-xs mono" placeholder="owner">
<span class="text-[#6e7681]">/</span>
<input id="repo-input" value="gitlink-cli" class="w-36 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1.5 text-xs mono" placeholder="repo">
<span id="token-state" class="text-xs text-[#6e7681]"></span>
</div>
</header>
<main class="max-w-7xl mx-auto px-6 py-8 space-y-10">
<!-- ① 命令全域浏览器 + 终端 -->
<section>
<h2 class="text-xl font-bold text-white mb-1">▶ 命令全域浏览器 <span class="text-sm text-[#8b949e] font-normal">25 个命令域 · 点动词填命令 · 真跑 gitlink-cli</span></h2>
<p class="text-sm text-[#8b949e] mb-4">点域里的动词自动填入终端;或在终端自由输入 <code class="text-[var(--green)]">gitlink-cli ...</code> 回车。平台命令需在顶栏填 tokensnippet 等本地命令免登录。</p>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="panel rounded-lg p-4">
<div class="flex items-center gap-2 mb-3">
<h3 class="font-semibold text-white">命令域</h3>
<input id="cmd-search" placeholder="🔍 搜索域/动词" class="flex-1 bg-[#0d1117] border border-[var(--border)] rounded px-2 py-1 text-xs mono">
</div>
<div id="cmd-groups" class="space-y-3 text-sm max-h-[460px] overflow-y-auto pr-1"></div>
</div>
<div class="panel rounded-lg lg:col-span-2 flex flex-col">
<div class="flex items-center gap-2 px-4 py-2 border-b border-[var(--border)] text-xs text-[#8b949e]">
<span class="w-3 h-3 rounded-full bg-[var(--red)]"></span>
<span class="w-3 h-3 rounded-full bg-[var(--yellow)]"></span>
<span class="w-3 h-3 rounded-full bg-[var(--green)]"></span>
<span class="ml-2">gitlink-cli @ web-demo</span>
<span id="term-target" class="ml-auto text-[var(--acc)]"></span>
</div>
<div id="term-out" class="mono text-sm flex-1"></div>
<div class="flex items-center px-4 py-2 mono text-sm" id="term-input">
<span class="text-[var(--green)] mr-2">$</span>
<input id="cmd-input" class="flex-1 bg-transparent text-[var(--green)] mono" placeholder="输入命令,如 snippet +list 然后回车" autocomplete="off">
</div>
</div>
</div>
</section>
<!-- ② Skill 卡片墙 -->
<section>
<div class="flex items-center justify-between flex-wrap gap-3 mb-4">
<h2 class="text-xl font-bold text-white">🧠 Skill 全集 <span class="text-sm text-[#8b949e] font-normal">48 个 · 点卡片读 SKILL.md 全文)</span></h2>
<div class="flex gap-2 flex-wrap">
<button class="fbtn active" data-filter="all" onclick="filterSkill('all',this)">全部 48</button>
<button class="fbtn" data-filter="new" onclick="filterSkill('new',this)">✨ 本次新增 7</button>
<button class="fbtn" data-filter="research" onclick="filterSkill('research',this)">🔬 科研</button>
<button class="fbtn" data-filter="quality" onclick="filterSkill('quality',this)">🚪 代码质量</button>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3" id="skill-wall"></div>
</section>
<!-- ③ pr-guard 流程 -->
<section class="panel rounded-lg p-6">
<h2 class="text-xl font-bold text-white mb-1">🚪 任务三代码质量看门人pr-guard</h2>
<p class="text-sm text-[#8b949e] mb-5">PR 提交后自动跑完 5 步质量门禁 ——「模拟流水线」看流程;填 token + PR 号可拉真实 PR 数据</p>
<div class="flex items-center justify-between gap-2 mb-5 flex-wrap" id="pr-steps"></div>
<div class="flex gap-2 flex-wrap">
<button onclick="runPipeline()" class="bg-[var(--acc)] text-black px-4 py-2 rounded font-semibold text-sm hover:opacity-90">▶ 模拟流水线</button>
<button onclick="livePR()" class="btn-cmd border border-[var(--border)] text-[var(--txt)] px-4 py-2 rounded text-sm">🔌 用真实 PR 跑(需 token</button>
</div>
<div id="pr-verdict" class="mt-4 text-sm mono"></div>
</section>
<!-- ④ 科研画像 -->
<section class="panel rounded-lg p-6">
<h2 class="text-xl font-bold text-white mb-1">🔬 任务四科研仓库画像research-insight</h2>
<p class="text-sm text-[#8b949e] mb-5">四维科研评分 + 协作网络 —— 实拉实算目标仓库,回答「能不能复现/引用/合作」</p>
<div class="flex items-center gap-2 mb-5 flex-wrap">
<span class="text-sm text-[#8b949e]">分析对象即顶栏 owner/repo</span>
<button onclick="analyze()" class="bg-[var(--green)] text-black px-4 py-2 rounded font-semibold text-sm hover:opacity-90">🔍 实拉分析(需 token</button>
<span id="analyze-status" class="text-xs text-[#8b949e]"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div><canvas id="radar"></canvas>
<div id="repro-detail" class="text-xs text-[#8b949e] mt-3 mono"></div>
</div>
<div>
<h4 class="text-white font-semibold mb-2">协作网络图 <span id="bus-info" class="text-xs text-[#8b949e] font-normal">(点「实拉分析」用真实贡献者重绘)</span></h4>
<svg id="collab-svg" viewBox="0 0 360 260" class="w-full panel rounded border border-[var(--border)]"></svg>
<div id="repo-meta" class="text-xs text-[#8b949e] mt-2"></div>
</div>
</div>
</section>
<!-- ⑤ 验证 -->
<section>
<h2 class="text-xl font-bold text-white mb-4">✅ 验证:三层证据(对照模板法)</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="panel rounded-lg p-4"><div class="text-[var(--green)] text-2xl mb-1"></div><h4 class="text-white font-semibold">命令层</h4><p class="text-sm text-[#8b949e] mt-1">命令真实执行,返回真实数据(非 unknown/401</p></div>
<div class="panel rounded-lg p-4"><div class="text-[var(--yellow)] text-2xl mb-1"></div><h4 class="text-white font-semibold">编排层</h4><p class="text-sm text-[#8b949e] mt-1">AI 按 SKILL.md 的顺序调命令</p></div>
<div class="panel rounded-lg p-4"><div class="text-[var(--acc)] text-2xl mb-1"></div><h4 class="text-white font-semibold">输出层</h4><p class="text-sm text-[#8b949e] mt-1">输出符合 SKILL.md 模板(判定/评分/清单)</p></div>
</div>
</section>
<footer class="text-center text-xs text-[#6e7681] py-6 border-t border-[var(--border)]">
GitLink CLI 智能化能力展示 · 子任务一~四 + DevOps · 后端 demo/web/server.py访客自带 token零凭据上云
</footer>
</main>
<script>
// ===== token & owner/repolocalStorage 持久化)=====
const tokenInput = document.getElementById('token-input');
const ownerInput = document.getElementById('owner-input');
const repoInput = document.getElementById('repo-input');
tokenInput.value = localStorage.getItem('gl_token') || '';
const tokenState = document.getElementById('token-state');
function syncTokenState(){ tokenState.textContent = tokenInput.value ? '✅ 已提供(仅存本机)' : '⚠️ 未提供,平台命令需 token'; tokenState.style.color = tokenInput.value ? 'var(--green)' : 'var(--yellow)'; }
tokenInput.addEventListener('input', ()=>{ localStorage.setItem('gl_token', tokenInput.value); syncTokenState(); });
syncTokenState();
function getToken(){ return tokenInput.value.trim(); }
function getOwner(){ return ownerInput.value.trim() || 'Gitlink'; }
function getRepo(){ return repoInput.value.trim() || 'gitlink-cli'; }
function targetStr(){ return getOwner() + '/' + getRepo(); }
document.getElementById('term-target').textContent = targetStr();
[ownerInput, repoInput].forEach(el => el.addEventListener('input', ()=> document.getElementById('term-target').textContent = targetStr()));
// ===== 命令全域数据25 域,按子任务分组)=====
const DOMAIN_GROUPS = [
{ g:'资源管理(仓库/文件/版本)', domains:[
{d:'repo', v:['info','languages','contributors','contributor-stats','code-stats','readme','tree','list','create','delete','fork','follow','like','stargazers','watchers','unfollow','unlike']},
{d:'file', v:['get','browse','create','update','delete']},
{d:'branch', v:['list','create','delete','protect','unprotect']},
{d:'release', v:['list','view','create','update','delete','edit','download']},
{d:'search', v:['issues','repos','users']},
{d:'compare', v:['view','files']},
]},
{ g:'Issue 跟踪', domains:[
{d:'issue', v:['list','view','create','update','close','comment','assigners','authors','priorities','statuses','tags','batch-create','batch-update','batch-close','batch-open','batch-assign','batch-label','batch-delete']},
{d:'label', v:['list','create','update','delete']},
{d:'milestone', v:['list','view','create','close','delete']},
]},
{ g:'PR 与代码审查', domains:[
{d:'pr', v:['list','view','create','merge','refuse','reopen','diff','files','versions','version-diff','comment','review','reviews','branches','check-merge']},
]},
{ g:'CI/CD · 流水线 · 工作流', domains:[
{d:'ci', v:['builds','logs','restart','stop','enable','disable','authorize']},
{d:'pipeline', v:['list','runs','run','view','logs','results','save-yaml','enable','disable','delete']},
{d:'workflow', v:['triage','health','pr-summary','repo-report']},
]},
{ g:'项目管理 · 协作', domains:[
{d:'pm', v:['boards','sprints','weekly','tags','pipelines','actions']},
{d:'org', v:['list','info','members','create']},
{d:'member', v:['list','add','remove']},
{d:'webhook', v:['list','view','create','update','delete','history','test']},
{d:'wiki', v:['list','view','create','update','delete','mkdir','rmdir','rename','renamedir']},
]},
{ g:'用户与画像', domains:[
{d:'user', v:['me','info','headmaps','stats-activity','stats-develop','stats-major','stats-role','trends']},
{d:'profile', v:['activity','ability','contribution','role','major']},
]},
{ g:'数据 · 合规 · 健康', domains:[
{d:'dataset', v:['list','view','create','update','delete-attachment']},
{d:'license', v:['list']},
{d:'ignore', v:['list']},
{d:'health', v:['fetch']},
]},
{ g:'本地工具(免登录)', domains:[
{d:'snippet', v:['create','list','view','search','update','delete','export']},
{d:'auth', v:['login','status','logout'], raw:true},
]},
];
const LOCAL_DOMAINS = new Set(['snippet','auth','config','version','doctor']);
// ===== 渲染命令域 =====
const input = document.getElementById('cmd-input');
function renderDomains(q=''){
let html='';
const Q = q.trim().toLowerCase();
DOMAIN_GROUPS.forEach(grp => {
const doms = grp.domains.filter(dm => {
if(!Q) return true;
return dm.d.includes(Q) || dm.v.some(x=>x.includes(Q));
});
if(!doms.length) return;
html += `<div><div class="text-xs text-[var(--yellow)] mb-1.5">${grp.g}</div><div class="space-y-1.5">`;
doms.forEach(dm => {
const local = LOCAL_DOMAINS.has(dm.d);
const tag = local ? '<span class="text-[10px] text-[var(--green)] ml-1">本地</span>' : '<span class="text-[10px] text-[#6e7681] ml-1">需token</span>';
const verbs = dm.v.filter(x=> !Q || x.includes(Q) || dm.d.includes(Q)).map(v=>{
const c = dm.raw ? `${dm.d} ${v}` : `${dm.d} +${v}`;
return `<span class="verb" data-cmd="${c}">+${v}</span>`;
}).join(' ');
html += `<div class="panel rounded p-2"><div class="mono text-xs text-[var(--acc)] mb-1">${dm.d}${tag}</div><div class="flex flex-wrap gap-1">${verbs}</div></div>`;
});
html += `</div></div>`;
});
document.getElementById('cmd-groups').innerHTML = html || '<p class="text-xs text-[#6e7681]">无匹配</p>';
document.querySelectorAll('.verb').forEach(el => el.onclick = ()=>{
// 平台命令自动带 owner/repoauth/snippet 除外)
let c = el.dataset.cmd;
const dom = c.split(' ')[0];
if(!LOCAL_DOMAINS.has(dom) && !c.includes('--owner')) c += ` --owner ${getOwner()} --repo ${getRepo()}`;
input.value = c; input.focus();
});
}
renderDomains();
document.getElementById('cmd-search').addEventListener('input', e=>renderDomains(e.target.value));
// ===== 终端 =====
const out = document.getElementById('term-out');
let history=[], hidx=0;
function appendOut(text, cls='l-out'){
const div=document.createElement('div'); div.className='mono text-sm '+cls; div.textContent=text;
out.appendChild(div); out.scrollTop=out.scrollHeight;
}
async function runCmd(){
let cmd=input.value.trim();
if(!cmd||cmd.startsWith('#')){ if(cmd) appendOut('$ '+cmd,'l-cmd'); input.value=''; return; }
history.push(cmd); hidx=history.length;
appendOut('$ '+cmd,'l-cmd'); input.value='';
const m=document.createElement('div'); m.className='mono text-sm l-muted'; m.textContent='⏳ 运行中...';
out.appendChild(m); out.scrollTop=out.scrollHeight;
try{
const r=await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd,token:getToken()})});
const d=await r.json(); m.remove();
if(d.ok){
appendOut((d.stdout||'')+(d.stderr?'\n[stderr] '+d.stderr:'')||'(无输出)','l-out');
if(d.code!==0) appendOut(`[退出码 ${d.code}]`,'l-err');
} else {
if(d.needs_token && !d.token_provided) appendOut('⚠️ '+d.error+'(此为平台命令,请在顶栏填 GitLink token','l-err');
else appendOut('❌ '+d.error,'l-err');
}
}catch(e){ m.remove(); appendOut('❌ 请求失败:'+e+'(后端 server.py 是否启动?)','l-err'); }
}
input.addEventListener('keydown',e=>{
if(e.key==='Enter') runCmd();
else if(e.key==='ArrowUp'){ if(hidx>0){hidx--;input.value=history[hidx]||'';e.preventDefault();} }
else if(e.key==='ArrowDown'){ if(hidx<history.length){hidx++;input.value=history[hidx]||'';e.preventDefault();} }
});
// ===== Skill 全集48=====
const SKILLS = ["auth","branch","ci","ci-health","code-review","commit-quality","compare","compliance","contributor-insight","dataset","digest","file","gatekeeper","health","insight","issue","issueops","issue-tag","issue-triage","label","license-compliance","member","milestone","notification-digest","onboarding","org","pipeline","pipeline-guardian","pm","pr","pr-guard","release","release-auto","repo","research-fork-impact","research-insight","research-tracker","scholar-profile","search","shared","snippet","stale-issue-manager","todo","user","webhook","webhook-sentinel","wiki","wiki-builder","workflow"];
const NEW_SKILLS = new Set(['onboarding','auth','snippet','digest','todo','pr-guard','research-insight']);
const SKILL_ICON = {onboarding:'🚀',auth:'🔑',snippet:'📦',digest:'📰',todo:'📋','pr-guard':'🚪','research-insight':'🔬','code-review':'👁','commit-quality':'✨','health':'❤','release-auto':'🏷','workflow':'🔁','compliance':'✅','license-compliance':'📜','wiki-builder':'📚','pipeline-guardian':'🛡','webhook-sentinel':'📡',stale:'💤'};
function skillCat(n){
if(NEW_SKILLS.has(n)) return 'new';
if(/research|scholar|contributor-insight|insight|fork-impact|tracker/.test(n)) return 'research';
if(/code-review|commit-quality|gatekeeper|pr-guard|compliance|license-compliance/.test(n)) return 'quality';
return 'other';
}
function skillEmoji(n){ return SKILL_ICON[n] || (/research|scholar/.test(n)?'🔬':/wiki/.test(n)?'📚':/pipeline|ci|webhook/.test(n)?'🛠':'📁'); }
let SKILL_FILTER='all';
function filterSkill(f,btn){
SKILL_FILTER=f;
document.querySelectorAll('.fbtn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
renderSkills();
}
function renderSkills(){
const list = SKILLS.filter(n => SKILL_FILTER==='all' ? true : (SKILL_FILTER==='new'?NEW_SKILLS.has(n):skillCat(n)===SKILL_FILTER));
document.getElementById('skill-wall').innerHTML = list.map(n=>{
const tag = NEW_SKILLS.has(n) ? '<span class="text-[10px] text-[var(--green)]">✨新增</span>' : (skillCat(n)==='research'?'<span class="text-[10px] text-[var(--acc)]">科研</span>':skillCat(n)==='quality'?'<span class="text-[10px] text-[var(--yellow)]">质量</span>':'');
return `<div class="skill-card panel rounded-lg p-3 cursor-pointer" onclick="openSkill('${n}')">
<div class="text-xl mb-1">${skillEmoji(n)}</div>
<div class="text-white text-sm font-semibold mono">gitlink-${n}</div>
<div class="mt-1">${tag}</div>
<div class="text-[10px] text-[var(--green)] mt-2">📖 SKILL.md →</div>
</div>`;
}).join('');
}
renderSkills();
// ===== pr-guard 流程 =====
const STEPS=[{n:'① 采集',d:'pr +diff/+files',c:'#8b949e'},{n:'② AI Review',d:'分级找问题',c:'#d29922'},{n:'③ CI 检查',d:'ci +builds',c:'#58a6ff'},{n:'④ 汇总评论',d:'api reviews',c:'#a371f7'},{n:'⑤ 质量判定',d:'门禁规则',c:'#3fb950'}];
document.getElementById('pr-steps').innerHTML = STEPS.map((s,i)=>`<div class="step panel rounded p-3 text-center flex-1 min-w-[120px]" id="step-${i}"><div class="text-sm font-semibold text-white">${s.n}</div><div class="text-xs mono mt-1" style="color:${s.c}">${s.d}</div></div>${i<STEPS.length-1?'<span class="text-[#6e7681]"></span>':''}`).join('');
function runPipeline(){
document.getElementById('pr-verdict').textContent='';
STEPS.forEach((_,i)=>document.getElementById('step-'+i).classList.remove('active'));
let i=0; const tick=setInterval(()=>{
if(i>0) document.getElementById('step-'+(i-1)).classList.remove('active');
if(i>=STEPS.length){ clearInterval(tick); document.getElementById('pr-verdict').innerHTML='<span style="color:var(--green)">✅ 质量判定:通过</span> 0 Critical + CI success → 建议合并'; return; }
document.getElementById('step-'+i).classList.add('active'); i++;
},600);
}
async function livePR(){
if(!getToken()){ appendOut('⚠️ 请先在顶栏填 token再用真实 PR 跑','l-err'); return; }
document.getElementById('pr-verdict').textContent='⏳ 拉取真实 PR 列表...';
try{
const r=await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:`pr +list --owner ${getOwner()} --repo ${getRepo()} --format json`,token:getToken()})});
const d=await r.json();
if(d.ok){ document.getElementById('pr-verdict').innerHTML=`<span style="color:var(--acc)">🔌 真实数据:</span>${getOwner()}/${getRepo()} 共 ${d.stdout.includes('"id"')?(d.stdout.match(/"id"/g)||[]).length:'?'} 个 PR原始 JSON 已在上方终端)`; appendOut('$ pr +list --owner '+getOwner()+' --repo '+getRepo(),'l-cmd'); appendOut(d.stdout||'(空)','l-out'); }
else document.getElementById('pr-verdict').textContent='❌ '+d.error;
}catch(e){ document.getElementById('pr-verdict').textContent='❌ '+e; }
}
// ===== 雷达图 + 协作图 =====
let radarChart = new Chart(document.getElementById('radar'),{
type:'radar',
data:{labels:['🔁 可复现性','📈 活跃度','📑 引用价值','🤝 协作健康'],datasets:[{label:'示例(点「实拉分析」换真实数据)',data:[6,5,5,6],fill:true,backgroundColor:'rgba(88,166,255,0.18)',borderColor:'#58a6ff',pointBackgroundColor:'#58a6ff'}]},
options:{plugins:{legend:{labels:{color:'#c9d1d9',font:{size:11}}}},scales:{r:{min:0,max:10,ticks:{color:'#6e7681',backdropColor:'transparent',stepSize:2},grid:{color:'#30363d'},pointLabels:{color:'#c9d1d9',font:{size:12}},angleLines:{color:'#30363d'}}}}
});
function drawCollab(contribs, repoName){
const svg=document.getElementById('collab-svg'); svg.innerHTML='';
const cx=180, cy=130, R=95;
// 中心项目节点
svg.innerHTML += `<circle cx="${cx}" cy="${cy}" r="24" fill="#1f6feb"/><text x="${cx}" y="${cy+3}" text-anchor="middle" fill="#fff" font-size="9">${(repoName||'repo').slice(0,8)}</text>`;
const top = (contribs||[]).slice(0,6);
if(!top.length){ svg.innerHTML+='<text x="180" y="250" text-anchor="middle" fill="#6e7681" font-size="10">(点「实拉分析」用真实贡献者重绘)</text>'; return; }
const max = top[0].contributions||1;
top.forEach((c,i)=>{
const a = (-Math.PI/2) + i*(2*Math.PI/top.length);
const r = R;
const x=cx+Math.cos(a)*r, y=cy+Math.sin(a)*r;
const rad = Math.max(8, 18*(c.contributions/max));
const core = i<2;
svg.innerHTML += `<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}" stroke="${core?'#3fb950':'#30363d'}" stroke-width="${core?2:1}" stroke-dasharray="${core?'':'4'}"/>`;
svg.innerHTML += `<circle cx="${x}" cy="${y}" r="${rad}" fill="${core?'#238636':'#6e7681'}"/>`;
svg.innerHTML += `<text x="${x}" y="${y+3}" text-anchor="middle" fill="#fff" font-size="9">${(c.name||'?').slice(0,8)}</text>`;
svg.innerHTML += `<text x="${x}" y="${y+rad+12}" text-anchor="middle" fill="#8b949e" font-size="8">${c.perc||''}</text>`;
});
}
drawCollab(null);
async function analyze(){
if(!getToken()){ document.getElementById('analyze-status').textContent='⚠️ 请先填 token'; return; }
document.getElementById('analyze-status').textContent='⏳ 采集中...';
try{
const r=await fetch('/api/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({owner:getOwner(),repo:getRepo(),token:getToken()})});
const d=await r.json();
if(!d.ok){ document.getElementById('analyze-status').textContent='❌ '+d.error; return; }
const s=d.scores;
radarChart.data.datasets[0].label = `${d.owner}/${d.repo}${d.is_fork?'(⚠️ fork of '+d.fork_from+'':''}`;
radarChart.data.datasets[0].data = [s.repro, s.activity, s.citation, s.collab];
radarChart.options.scales.r.max = 10;
radarChart.update();
document.getElementById('analyze-status').innerHTML = `✅ 完成 · ${d.contributor_count} 贡献者 · ${d.release_count} release · 协议 ${d.license}`;
document.getElementById('repo-meta').innerHTML = `${d.is_fork?'⚠️ <b style="color:var(--yellow)">是 fork</b>(引用应指 upstream '+d.fork_from+'':'✅ 独立仓库'} · 巴士因子 <b style="color:${d.bus_risk==='低'?'var(--green)':d.bus_risk==='中'?'var(--yellow)':'var(--red)'}">${d.bus_factor}%${d.bus_risk} 风险)</b>`;
document.getElementById('repro-detail').innerHTML = '可复现性 '+s.repro+'/'+d.repro_max+''+d.repro_detail.map(x=>x[1]?'✅'+x[0]:'❌'+x[0]).join(' · ');
drawCollab(d.contributors, d.name);
}catch(e){ document.getElementById('analyze-status').textContent='❌ '+e; }
}
// 初始提示
appendOut('💡 点左上方「命令域」里的动词自动填命令;或在下方输入 gitlink-cli 命令回车。','l-muted');
appendOut(' 本地命令snippet/auth免登录即可跑平台命令repo/issue/pr…请在顶栏填 token。','l-muted');
appendOut(' 下方「Skill 全集」48 个卡片可读 SKILL.md 全文;科研画像区可对目标仓库实拉四维评分。','l-muted');
// ===== Skill 详情模态框 =====
async function openSkill(name){
const modal=document.getElementById('skill-modal');
document.getElementById('skill-modal-title').textContent='📖 gitlink-'+name;
const body=document.getElementById('skill-modal-body');
body.innerHTML='<p class="text-[#8b949e]">⏳ 加载 SKILL.md...</p>';
modal.classList.remove('hidden');
try{
const r=await fetch('/api/skill?name='+encodeURIComponent(name));
const d=await r.json();
if(!d.ok){ body.innerHTML='<p class="l-err">❌ '+d.error+'</p>'; return; }
body.innerHTML=marked.parse(d.content);
}catch(e){ body.innerHTML='<p class="l-err">❌ '+e+'</p>'; }
}
function closeSkill(){ document.getElementById('skill-modal').classList.add('hidden'); }
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeSkill(); });
</script>
<!-- Skill 详情模态框 -->
<div id="skill-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" onclick="closeSkill()" style="background:rgba(0,0,0,.75)">
<div class="panel rounded-lg max-w-4xl w-full max-h-[88vh] flex flex-col" onclick="event.stopPropagation()">
<div class="flex items-center justify-between px-5 py-3 border-b border-[var(--border)]">
<h3 id="skill-modal-title" class="text-white font-bold text-lg"></h3>
<button onclick="closeSkill()" class="text-[#8b949e] hover:text-white text-3xl leading-none">×</button>
</div>
<div class="p-6 overflow-y-auto text-sm" id="skill-modal-body"></div>
</div>
</div>
</body>
</html>

313
demo/web/server.py Normal file
View File

@ -0,0 +1,313 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gitlink-cli 演示网页 · 后端零依赖 Python 标准库
作用接收前端命令 + 访客 token真跑 gitlink-cli返回真实输出
启动python server.py http://0.0.0.0:$PORT 默认 8000
位置仓库内 demo/web/server.pyCLI 在仓库根 ../../gitlink-cli[.exe]
安全本地/演示用已做白名单只允许 gitlink-cli 子命令subprocess 列表参数不经 shell
30s 超时访客 token 仅在请求内存中传给子进程不写日志不落盘
"""
import http.server
import json
import os
import re
import socketserver
import subprocess
import sys
from pathlib import Path
from urllib.parse import urlparse, parse_qs
# Windows GBK 终端兼容
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
PORT = int(os.getenv("PORT", "8000"))
HOST = os.getenv("HOST", "0.0.0.0") # 云端需 0.0.0.0;只听本机设 HOST=127.0.0.1
ROOT = Path(__file__).resolve().parent # .../demo/web
REPO_ROOT = ROOT.parent.parent # 仓库根 .../gitlink-cli
def _find_cli():
"""CLI 位置GITLINK_BIN > 仓库内 > PATH。Linux=gitlink-cliWindows=gitlink-cli.exe。"""
env = os.getenv("GITLINK_BIN")
if env and Path(env).exists():
return Path(env)
for name in ("gitlink-cli", "gitlink-cli.exe"):
p = REPO_ROOT / name
if p.exists():
return p
for d in os.getenv("PATH", "").split(os.pathsep):
p = Path(d) / "gitlink-cli"
if p.exists():
return p
return REPO_ROOT / "gitlink-cli" # 占位
CLI = _find_cli()
CWD = str(REPO_ROOT) # 让 --owner/--repo 可从 git remote 自动解析
ALLOWED_DOMAINS = { # 全部 30 个顶层域
"api", "auth", "branch", "ci", "compare", "config", "dataset", "doctor",
"file", "health", "ignore", "issue", "label", "license", "member",
"milestone", "org", "pipeline", "pm", "pr", "profile", "release", "repo",
"search", "snippet", "user", "version", "webhook", "wiki", "workflow",
}
NEEDS_TOKEN_DOMAINS = { # 平台域(访客需填 token
"repo", "issue", "pr", "branch", "release", "search", "label", "member",
"milestone", "webhook", "wiki", "org", "user", "ci", "compare", "dataset",
"health", "license", "pipeline", "pm", "profile", "workflow", "api",
}
LOCAL_DOMAINS = {"snippet", "auth", "config", "version", "doctor"}
def _run_cli(args, token="", timeout=30):
env = dict(os.environ)
if token:
env["GITLINK_TOKEN"] = token
r = subprocess.run(
[str(CLI)] + args, capture_output=True, text=True, timeout=timeout,
cwd=CWD, encoding="utf-8", errors="replace", env=env,
)
return r.stdout, r.stderr, r.returncode
def _parse_json(stdout):
s = stdout.strip()
i, j = s.find("{"), s.rfind("}")
if i < 0 or j < 0:
return None
try:
return json.loads(s[i:j + 1])
except Exception:
return None
def _license_name(text):
t = (text or "").lower()
if "mulan" in t: return "Mulan PSL v2"
if t.startswith("mit") or "mit license" in t: return "MIT"
if "apache" in t: return "Apache 2.0"
if "gpl" in t: return "GPL"
if "bsd" in t: return "BSD"
return "有 LICENSE" if text else ""
def analyze_repo(owner, repo, token):
"""采集 + 按 research-insight 评分表算四维 + 巴士因子。"""
def cli(*a):
return _run_cli(list(a), token=token, timeout=30)
info_o, _, _ = cli("repo", "+info", "--owner", owner, "--repo", repo, "--format", "json")
info = (_parse_json(info_o) or {}).get("data") or {}
contrib_o, _, _ = cli("repo", "+contributors", "--owner", owner, "--repo", repo, "--format", "json")
contribs = ((_parse_json(contrib_o) or {}).get("data") or {}).get("list") or []
contribs_sorted = sorted(contribs, key=lambda c: -(c.get("contributions") or 0))
rel_o, _, _ = cli("release", "+list", "--owner", owner, "--repo", repo, "--format", "json")
releases = ((_parse_json(rel_o) or {}).get("data") or {}).get("releases") or []
lic_o, _, _ = cli("file", "+get", "--owner", owner, "--repo", repo, "--path", "LICENSE", "--format", "json")
lic_text = ""
lic_parsed = _parse_json(lic_o)
if lic_parsed:
d = lic_parsed.get("data") or {}
entries = d.get("entries") if isinstance(d, dict) else None
if isinstance(entries, dict):
lic_text = entries.get("content") or ""
elif isinstance(d, str):
lic_text = d
ci_o, _, _ = cli("repo", "+tree", "--owner", owner, "--repo", repo, "--path", ".gitea/workflows", "--format", "json")
ci_entries = ((_parse_json(ci_o) or {}).get("data") or {}).get("entries") or []
has_ci = bool(ci_entries)
tree_o, _, _ = cli("repo", "+tree", "--owner", owner, "--repo", repo, "--format", "json")
root_files = [str(e.get("name", "")) for e in ((_parse_json(tree_o) or {}).get("data") or {}).get("entries") or []]
lock_files = {"go.sum", "package-lock.json", "yarn.lock", "Cargo.lock", "requirements.txt", "poetry.lock", "pom.xml"}
has_lock = any(f in lock_files for f in root_files)
has_readme = any(f.lower().startswith("readme") for f in root_files)
# 可复现性(工程类,满分 8数据项 N/A
repro, repro_detail = 0, []
repro += 2 if has_ci else 0; repro_detail.append(("CI 配置", has_ci))
repro += 2 if has_lock else 0; repro_detail.append(("依赖锁定", has_lock))
repro += 2 if has_readme else 0; repro_detail.append(("运行文档", has_readme))
ver = bool(releases or info.get("version_releases_count"))
repro += 2 if ver else 0; repro_detail.append(("版本归档", ver))
n_contrib = len(contribs)
activity = min(10, round(n_contrib / 3)) if n_contrib else 2 # 无 commits API用贡献者规模近似
citation = 0
citation += 3 if lic_text else 0
citation += 3 if ver else 0
citation += 2 if has_readme else 0
citation += 2 if (info.get("fork_info") or {}).get("fork_project_user_login") else 0
citation = min(10, citation)
top_perc = 0.0
if contribs_sorted:
try:
top_perc = float(re.sub(r"[^\d.]", "", str(contribs_sorted[0].get("contribution_perc", "0"))))
except Exception:
top_perc = 0.0
collab = 10 if top_perc < 33 else (6 if top_perc < 50 else 3)
fork_from = (info.get("fork_info") or {}).get("fork_project_user_login")
return {
"ok": True, "owner": owner, "repo": repo,
"is_fork": bool(fork_from), "fork_from": fork_from,
"name": info.get("name", repo),
"license": _license_name(lic_text),
"contributor_count": n_contrib,
"release_count": len(releases),
"version_releases_count": info.get("version_releases_count", 0),
"contributors": [
{"name": c.get("name") or c.get("login") or "?",
"contributions": c.get("contributions", 0),
"perc": c.get("contribution_perc", "")}
for c in contribs_sorted[:8]
],
"scores": {"repro": repro, "activity": activity, "citation": citation, "collab": collab},
"repro_max": 8, "repro_detail": repro_detail,
"bus_factor": top_perc,
"bus_risk": "" if top_perc < 33 else ("" if top_perc < 50 else ""),
}
class Handler(http.server.BaseHTTPRequestHandler):
def _cors(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
def do_OPTIONS(self):
self.send_response(204); self._cors(); self.end_headers()
def do_GET(self):
path = urlparse(self.path).path
if path in ("/", "/index.html"):
self._serve_file("index.html", "text/html")
elif path == "/api/cli":
self._json({"ok": True, "cli": str(CLI), "exists": CLI.exists()})
elif path == "/api/domains":
self._json({"ok": True, "domains": sorted(ALLOWED_DOMAINS), "local": sorted(LOCAL_DOMAINS)})
elif path == "/api/health":
self._json({"ok": True, "cli_exists": CLI.exists()})
elif path == "/api/skill":
self._handle_skill()
else:
self.send_error(404)
def _handle_skill(self):
q = parse_qs(urlparse(self.path).query)
name = (q.get("name") or [""])[0].strip()
if not name:
self._json({"ok": False, "error": "缺少 ?name="}); return
skill_md = REPO_ROOT / "skills" / f"gitlink-{name}" / "SKILL.md"
if not skill_md.exists():
self._json({"ok": False, "error": f"找不到 SKILL.mdgitlink-{name}"}); return
self._json({"ok": True, "name": name, "content": skill_md.read_text(encoding="utf-8")})
def do_POST(self):
path = urlparse(self.path).path
body = self._read_body()
if path == "/api/run":
self._handle_run(body)
elif path == "/api/analyze":
owner = (body.get("owner") or "").strip()
repo = (body.get("repo") or "").strip()
token = (body.get("token") or "").strip()
if not owner or not repo:
self._json({"ok": False, "error": "缺少 owner/repo"}); return
try:
self._json(analyze_repo(owner, repo, token))
except subprocess.TimeoutExpired:
self._json({"ok": False, "error": "采集超时(>30s"})
except Exception as e:
self._json({"ok": False, "error": str(e)})
else:
self.send_error(404)
def _handle_run(self, body):
cmd = (body.get("cmd") or "").strip()
token = (body.get("token") or "").strip()
if not cmd:
self._json({"ok": False, "error": "空命令"}); return
args = cmd.split()
while args and args[0] in ("gitlink-cli", "gitlink-cli.exe", "./gitlink-cli.exe"):
args = args[1:]
if not args:
self._json({"ok": False, "error": "缺少子命令"}); return
domain = args[0]
if domain not in ALLOWED_DOMAINS:
self._json({"ok": False, "error": f"不允许的命令:{domain}(仅限 gitlink-cli 子命令)"}); return
needs_token = domain in NEEDS_TOKEN_DOMAINS
try:
out, err, code = _run_cli(args, token=token, timeout=30)
self._json({
"ok": code == 0, "cmd": f"gitlink-cli {' '.join(args)}",
"stdout": out, "stderr": err, "code": code,
"needs_token": needs_token, "token_provided": bool(token),
})
except subprocess.TimeoutExpired:
self._json({"ok": False, "error": "命令超时(>30s可能涉及交互输入"})
except Exception as e:
self._json({"ok": False, "error": str(e)})
def _read_body(self):
length = int(self.headers.get("Content-Length", 0) or 0)
raw = self.rfile.read(length) if length else b"{}"
try:
return json.loads(raw)
except Exception:
return {}
def _json(self, obj):
data = json.dumps(obj, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self._cors()
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _serve_file(self, name, mime):
p = ROOT / name
if not p.exists():
self.send_error(404, f"{name} 不存在"); return
data = p.read_bytes()
self.send_response(200)
self.send_header("Content-Type", f"{mime}; charset=utf-8")
self._cors()
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *a):
pass
class ReuseTCPServer(socketserver.TCPServer):
allow_reuse_address = True
if __name__ == "__main__":
if not CLI.exists():
print(f"[!] 找不到 gitlink-cli 二进制:{CLI}")
print(" 请先编译cd <仓库根> && go build -o gitlink-cli . Linux")
print(" 或设环境变量 GITLINK_BIN 指向已有二进制。")
with ReuseTCPServer((HOST, PORT), Handler) as httpd:
print(f"[OK] gitlink-cli 演示后端已启动http://{HOST}:{PORT}")
print(f" CLI{CLI}exists={CLI.exists()}")
print(f" 访客在网页顶栏填自己的 GitLink token 即可跑平台命令。Ctrl+C 停止。")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n已停止")

BIN
gitlink-cli.exe Normal file

Binary file not shown.