Compare commits
No commits in common. "main" and "v0.1.0227.1" have entirely different histories.
main
...
v0.1.0227.
@ -1,144 +0,0 @@
|
||||
---
|
||||
name: gitea
|
||||
description: 统一 Gitea 总入口,支持 issue 查询、issue 拆单创建、git push 和 PR 基础操作,优先从当前仓库 origin 自动识别目标仓库。
|
||||
---
|
||||
|
||||
# Gitea - 统一入口
|
||||
|
||||
> **定位**:这是 Gitea 总入口。适合“看 issue”“拆 issue”“把提交 push 上去”“给当前分支开 PR”“在 PR 里留言”这类场景。
|
||||
>
|
||||
> `issue` 和 `issue-drive` 继续保留:`issue` 负责只读查看,`issue-drive` 负责拆单和创建工单;`gitea` 负责统一路由和 push / PR 能力。
|
||||
|
||||
当用户调用 `/gitea`、`$gitea`,或自然语言要求“处理 Gitea / push / PR / issue”时,按以下规则执行。
|
||||
|
||||
## 1. 先路由意图
|
||||
|
||||
先从用户输入里判断目标动作:
|
||||
|
||||
- 包含“看 / 查 / list / open / closed / #123 / issue”,且没有“拆 / 建 / 提 / 创建工单”语义:走 `issue`
|
||||
- 包含“拆 issue / 提 issue / 创建工单 / 沉淀问题”语义:走 `issue-drive`
|
||||
- 包含“push / 推送 / tag 推上去 / 推当前分支”语义:走 push 流程
|
||||
- 包含“PR / pull request / 拉请求 / 合并请求 / 评论 PR”语义:走 PR 流程
|
||||
|
||||
如果一句话里同时包含多个动作,先按用户描述顺序执行;默认不要自作主张串更多步骤。
|
||||
|
||||
## 2. 环境变量
|
||||
|
||||
统一使用:
|
||||
|
||||
- `GITEA_TOKEN`:必需
|
||||
- `GITEA_BASE_URL`:可选;当传 `owner/repo` 或当前仓库 `origin` 是 SSH 地址时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,先提示用户配置后停止,不继续执行 API、push fallback 或 PR 操作。
|
||||
|
||||
## 3. Issue 路由
|
||||
|
||||
如果判定为只读 issue:
|
||||
|
||||
- 直接执行 `issue` skill 的流程
|
||||
- 保持它的输入规则:当前仓库自动识别、支持 `owner/repo`、支持完整仓库 URL、支持单条 issue 编号
|
||||
- 不在 `gitea` 里重复抄写 `issue` 的长说明
|
||||
|
||||
如果判定为 issue 拆单 / 创建:
|
||||
|
||||
- 直接执行 `issue-drive` 的流程
|
||||
- 先整理事实和证据,再创建 issue
|
||||
- 创建时统一使用 `GITEA_TOKEN`
|
||||
|
||||
## 4. Push 流程
|
||||
|
||||
Push 一律先跑预检,再决定是否执行:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/gitea/scripts/push_gitea.py
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 默认推当前分支到同名远端分支
|
||||
- `ahead=0` 时直接告诉用户“没有可推送提交”
|
||||
- `dirty=true` 时明确提示“只会推送已提交内容”
|
||||
- 分支 diverged 时停止,不自动 rebase,也不自动 force
|
||||
- 只有用户明确要求“强推 / force push”时,才带 `--force`
|
||||
|
||||
真正执行时:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py --execute
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/gitea/scripts/push_gitea.py --execute
|
||||
```
|
||||
|
||||
若用户明确要求强推:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py --execute --force
|
||||
```
|
||||
|
||||
执行策略:
|
||||
|
||||
- 先尝试 `git push origin branch:branch`
|
||||
- 若远端鉴权失败,再使用 `GITEA_TOKEN` 调 `/api/v1/user` 获取登录名,构造一次性 HTTPS token URL 进行 fallback
|
||||
- 不改写本地 `origin`
|
||||
|
||||
## 5. PR 流程
|
||||
|
||||
PR 只做三件事:`list`、`create`、`comment`。
|
||||
|
||||
### 5.1 看 PR
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py list --state=open --limit=20
|
||||
```
|
||||
|
||||
### 5.2 开 PR
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py create --base main --title "标题" --body "正文"
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `head` 默认当前分支
|
||||
- `base` 默认远端默认分支;识别不到时再显式传 `--base`
|
||||
- 如果当前分支还没推送到远端,脚本会先走 push 流程,再创建 PR
|
||||
- v1 只支持同仓库 PR,不处理 fork 间 PR
|
||||
|
||||
### 5.3 在 PR 里留言
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py comment 12 --body "已完成回归"
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- PR 评论走 issue comment API,因为 Gitea PR 底层复用 issue 线程
|
||||
|
||||
## 6. 输出要求
|
||||
|
||||
- issue 查询:沿用 `issue` skill 的固定 Markdown 输出
|
||||
- issue 创建:沿用 `issue-drive` 的创建结果输出
|
||||
- push:输出预检 JSON 或执行结果 JSON,至少包含 `status`、`branch`、`ahead/behind`、是否执行、执行方式
|
||||
- PR:输出 PR 列表或单条创建 / 评论结果 JSON,至少包含编号、标题、URL、状态
|
||||
|
||||
## 7. 用法示例
|
||||
|
||||
```bash
|
||||
/gitea 看当前仓库 open issues
|
||||
/gitea 查 issue 17
|
||||
/gitea 把这个 bug 拆成两张 issue
|
||||
/gitea push
|
||||
/gitea 推送当前分支到远端
|
||||
/gitea 给当前分支开 PR 到 main
|
||||
/gitea 看当前仓库 open PR
|
||||
/gitea 在 PR 12 留言:已完成回归
|
||||
```
|
||||
@ -1,334 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared helpers for Gitea skill scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
|
||||
SSH_RE = re.compile(r"^git@(?P<host>[^:]+):(?P<path>.+?)(?:\.git)?/?$")
|
||||
REPO_PATH_RE = re.compile(r"^(?P<owner>[^/]+)/(?P<repo>[^/]+)$")
|
||||
|
||||
|
||||
class GitCommandError(RuntimeError):
|
||||
"""Raised when a git command fails."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepoContext:
|
||||
origin: str
|
||||
owner: str
|
||||
repo: str
|
||||
repo_url: str
|
||||
remote_name: str = "origin"
|
||||
remote_url: str | None = None
|
||||
|
||||
|
||||
def normalize_base_url(base_url: str) -> str:
|
||||
return base_url.rstrip("/")
|
||||
|
||||
|
||||
def ensure_git_repo() -> None:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--is-inside-work-tree"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0 or result.stdout.strip() != "true":
|
||||
raise SystemExit("Current directory is not a git repository.")
|
||||
|
||||
|
||||
def run_git(args: list[str], cwd: str | None = None, check: bool = True) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
detail = result.stderr.strip() or result.stdout.strip() or "unknown git error"
|
||||
raise GitCommandError(f"git {' '.join(args)} failed: {detail}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def parse_http_repo_url(repo_url: str) -> tuple[str, str, str, str]:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
if prefix:
|
||||
origin = f"{origin}/{prefix}"
|
||||
normalized_repo_url = f"{origin}/{owner}/{repo}"
|
||||
return origin, owner, repo, normalized_repo_url
|
||||
|
||||
|
||||
def parse_repo_target(
|
||||
repo_target: str,
|
||||
base_url: str | None = None,
|
||||
) -> tuple[str, str, str, str]:
|
||||
value = repo_target.strip()
|
||||
if not value:
|
||||
raise SystemExit("Repo target cannot be empty.")
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return parse_http_repo_url(value)
|
||||
|
||||
if value.startswith("ssh://"):
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit(
|
||||
"Invalid SSH repo URL. Expected ssh://git@host/owner/repo.git"
|
||||
)
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
host = parsed.hostname
|
||||
if not host:
|
||||
raise SystemExit("Invalid SSH repo URL. Missing host.")
|
||||
origin = (
|
||||
normalize_base_url(base_url)
|
||||
if base_url
|
||||
else f"https://{host}{f'/{prefix}' if prefix else ''}"
|
||||
)
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
ssh_match = SSH_RE.match(value)
|
||||
if ssh_match:
|
||||
path = ssh_match.group("path").rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit(
|
||||
"Invalid SSH repo target. Expected git@host:owner/repo.git"
|
||||
)
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = (
|
||||
normalize_base_url(base_url)
|
||||
if base_url
|
||||
else f"https://{ssh_match.group('host')}{f'/{prefix}' if prefix else ''}"
|
||||
)
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
path_match = REPO_PATH_RE.match(value)
|
||||
if path_match:
|
||||
if not base_url:
|
||||
raise SystemExit(
|
||||
"Repo shorthand owner/repo requires GITEA_BASE_URL or a full repo URL."
|
||||
)
|
||||
origin = normalize_base_url(base_url)
|
||||
owner = path_match.group("owner")
|
||||
repo = path_match.group("repo")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
raise SystemExit(
|
||||
"Invalid repo target. Use https://host/owner/repo, git@host:owner/repo.git, "
|
||||
"ssh://git@host/owner/repo.git, or owner/repo with GITEA_BASE_URL."
|
||||
)
|
||||
|
||||
|
||||
def get_remote_url(remote: str = "origin") -> str:
|
||||
ensure_git_repo()
|
||||
try:
|
||||
return run_git(["remote", "get-url", remote])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(f"Failed to read git remote '{remote}': {exc}") from exc
|
||||
|
||||
|
||||
def resolve_repo(
|
||||
repo_url: str | None = None,
|
||||
repo: str | None = None,
|
||||
remote: str = "origin",
|
||||
) -> RepoContext:
|
||||
base_url = os.getenv("GITEA_BASE_URL")
|
||||
remote_url = None
|
||||
target = repo_url or repo
|
||||
if not target:
|
||||
remote_url = get_remote_url(remote)
|
||||
target = remote_url
|
||||
origin, owner, repo_name, normalized_repo_url = parse_repo_target(
|
||||
target,
|
||||
base_url=base_url,
|
||||
)
|
||||
return RepoContext(
|
||||
origin=origin,
|
||||
owner=owner,
|
||||
repo=repo_name,
|
||||
repo_url=normalized_repo_url,
|
||||
remote_name=remote,
|
||||
remote_url=remote_url,
|
||||
)
|
||||
|
||||
|
||||
def api_base(context: RepoContext) -> str:
|
||||
return f"{context.origin}/api/v1/repos/{context.owner}/{context.repo}"
|
||||
|
||||
|
||||
def load_token(required: bool = True) -> str:
|
||||
token = os.getenv("GITEA_TOKEN", "").strip()
|
||||
if not token and required:
|
||||
raise SystemExit("Missing GITEA_TOKEN. Export it before using Gitea workflows.")
|
||||
return token
|
||||
|
||||
|
||||
def request_json(
|
||||
url: str,
|
||||
token: str,
|
||||
method: str = "GET",
|
||||
payload: dict | None = None,
|
||||
) -> dict | list:
|
||||
data = None
|
||||
headers = {"Accept": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace")
|
||||
raise SystemExit(
|
||||
f"Gitea API request failed: {exc.code} {exc.reason} | {body}"
|
||||
) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise SystemExit(f"Failed to reach Gitea API: {exc.reason}") from exc
|
||||
|
||||
|
||||
def get_current_user_login(origin: str, token: str) -> str:
|
||||
data = request_json(f"{origin}/api/v1/user", token)
|
||||
if not isinstance(data, dict):
|
||||
raise SystemExit("Unexpected user API response.")
|
||||
login = str(data.get("login") or "").strip()
|
||||
if not login:
|
||||
raise SystemExit("Failed to resolve current Gitea user login.")
|
||||
return login
|
||||
|
||||
|
||||
def current_branch() -> str:
|
||||
ensure_git_repo()
|
||||
try:
|
||||
branch = run_git(["symbolic-ref", "--quiet", "--short", "HEAD"])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(
|
||||
"Detached HEAD. Checkout a branch before running push or PR actions."
|
||||
) from exc
|
||||
if not branch:
|
||||
raise SystemExit("Failed to determine current git branch.")
|
||||
return branch
|
||||
|
||||
|
||||
def get_upstream(branch: str) -> str | None:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"rev-parse",
|
||||
"--abbrev-ref",
|
||||
"--symbolic-full-name",
|
||||
f"{branch}@{{upstream}}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
upstream = result.stdout.strip()
|
||||
return upstream or None
|
||||
|
||||
|
||||
def remote_branch_sha(remote: str, branch: str) -> str | None:
|
||||
try:
|
||||
output = run_git(["ls-remote", "--heads", remote, branch])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(f"Failed to query remote branch '{remote}/{branch}': {exc}") from exc
|
||||
if not output:
|
||||
return None
|
||||
return output.split()[0]
|
||||
|
||||
|
||||
def ahead_behind(compare_ref: str) -> tuple[int, int]:
|
||||
output = run_git(["rev-list", "--left-right", "--count", f"{compare_ref}...HEAD"])
|
||||
parts = output.split()
|
||||
if len(parts) != 2:
|
||||
raise SystemExit(f"Unexpected rev-list output: {output}")
|
||||
behind, ahead = (int(part) for part in parts)
|
||||
return behind, ahead
|
||||
|
||||
|
||||
def worktree_changes() -> list[str]:
|
||||
output = run_git(["status", "--short"])
|
||||
return [line for line in output.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def remote_default_branch(remote: str = "origin") -> str:
|
||||
try:
|
||||
output = run_git(["symbolic-ref", "--quiet", "--short", f"refs/remotes/{remote}/HEAD"])
|
||||
if output.startswith(f"{remote}/"):
|
||||
return output.split("/", 1)[1]
|
||||
except GitCommandError:
|
||||
pass
|
||||
|
||||
for candidate in ("main", "master"):
|
||||
if remote_branch_sha(remote, candidate):
|
||||
return candidate
|
||||
raise SystemExit(
|
||||
f"Failed to infer default branch for remote '{remote}'. Pass --base explicitly."
|
||||
)
|
||||
|
||||
|
||||
def build_authenticated_push_url(repo_url: str, username: str, token: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
path = parsed.path.rstrip("/")
|
||||
if not path.endswith(".git"):
|
||||
path = f"{path}.git"
|
||||
netloc = (
|
||||
f"{urllib.parse.quote(username, safe='')}:"
|
||||
f"{urllib.parse.quote(token, safe='')}@{parsed.netloc}"
|
||||
)
|
||||
return urllib.parse.urlunsplit((parsed.scheme or "https", netloc, path, "", ""))
|
||||
|
||||
|
||||
def mask_url(url: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
if "@" not in parsed.netloc:
|
||||
return url
|
||||
_, host = parsed.netloc.rsplit("@", 1)
|
||||
return urllib.parse.urlunsplit((parsed.scheme, f"***@{host}", parsed.path, "", ""))
|
||||
|
||||
|
||||
def is_auth_error(stderr: str) -> bool:
|
||||
message = stderr.lower()
|
||||
indicators = (
|
||||
"authentication failed",
|
||||
"permission denied",
|
||||
"could not read username",
|
||||
"could not read password",
|
||||
"http basic: access denied",
|
||||
"access denied",
|
||||
"unauthorized",
|
||||
)
|
||||
return any(indicator in message for indicator in indicators)
|
||||
@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List, create, and comment on Gitea pull requests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
from common import (
|
||||
api_base,
|
||||
current_branch,
|
||||
load_token,
|
||||
remote_default_branch,
|
||||
request_json,
|
||||
resolve_repo,
|
||||
)
|
||||
from push_gitea import compute_push_plan, execute_push
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="List, create, and comment on Gitea pull requests."
|
||||
)
|
||||
parser.add_argument("--repo-url", help="Explicit target repo URL.")
|
||||
parser.add_argument("--repo", help="Shorthand owner/repo. Requires GITEA_BASE_URL.")
|
||||
parser.add_argument("--remote", default="origin", help="Git remote name. Default: origin")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
list_parser = subparsers.add_parser("list", help="List pull requests.")
|
||||
list_parser.add_argument(
|
||||
"--state",
|
||||
default="open",
|
||||
choices=("open", "closed", "all"),
|
||||
help="Pull request state filter.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--limit",
|
||||
default=20,
|
||||
type=int,
|
||||
help="Maximum number of pull requests to return.",
|
||||
)
|
||||
|
||||
create_parser = subparsers.add_parser("create", help="Create a pull request.")
|
||||
create_parser.add_argument("--base", help="Base branch. Default: remote default branch.")
|
||||
create_parser.add_argument("--head", help="Head branch. Default: current branch.")
|
||||
create_parser.add_argument("--title", required=True, help="Pull request title.")
|
||||
create_parser.add_argument("--body", default="", help="Pull request body.")
|
||||
|
||||
comment_parser = subparsers.add_parser("comment", help="Comment on a pull request.")
|
||||
comment_parser.add_argument("pr", type=int, help="Pull request number.")
|
||||
comment_parser.add_argument("--body", required=True, help="Comment body.")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def summarize_pull_request(item: dict) -> dict:
|
||||
user = item.get("user") or {}
|
||||
head = item.get("head") or {}
|
||||
base = item.get("base") or {}
|
||||
return {
|
||||
"number": item.get("number"),
|
||||
"title": item.get("title"),
|
||||
"state": item.get("state"),
|
||||
"html_url": item.get("html_url"),
|
||||
"author": user.get("full_name") or user.get("login"),
|
||||
"head": head.get("ref"),
|
||||
"base": base.get("ref"),
|
||||
"created_at": item.get("created_at"),
|
||||
"updated_at": item.get("updated_at"),
|
||||
}
|
||||
|
||||
|
||||
def list_pull_requests(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
url = f"{api_base(context)}/pulls?state={args.state}&page=1&limit={args.limit}"
|
||||
data = request_json(url, token)
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit("Unexpected pull request list response.")
|
||||
return {
|
||||
"action": "list",
|
||||
"repo_url": context.repo_url,
|
||||
"state": args.state,
|
||||
"limit": args.limit,
|
||||
"count": len(data),
|
||||
"pull_requests": [summarize_pull_request(item) for item in data],
|
||||
}
|
||||
|
||||
|
||||
def create_pull_request(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
head = args.head or current_branch()
|
||||
base = args.base or remote_default_branch(args.remote)
|
||||
|
||||
push_plan = compute_push_plan(
|
||||
repo_url=args.repo_url,
|
||||
repo=args.repo,
|
||||
remote=args.remote,
|
||||
branch=head,
|
||||
force=False,
|
||||
)
|
||||
push_result = None
|
||||
if push_plan["needs_push"]:
|
||||
push_result = execute_push(push_plan, force=False)
|
||||
elif push_plan["status"] == "blocked":
|
||||
raise SystemExit(
|
||||
"Cannot create PR because the current branch has diverged from the remote branch."
|
||||
)
|
||||
|
||||
payload = {"base": base, "head": head, "title": args.title, "body": args.body}
|
||||
created = request_json(f"{api_base(context)}/pulls", token, method="POST", payload=payload)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected pull request creation response.")
|
||||
return {
|
||||
"action": "create",
|
||||
"repo_url": context.repo_url,
|
||||
"base": base,
|
||||
"head": head,
|
||||
"push": push_result,
|
||||
"pull_request": summarize_pull_request(created),
|
||||
}
|
||||
|
||||
|
||||
def comment_pull_request(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
payload = {"body": args.body}
|
||||
created = request_json(
|
||||
f"{api_base(context)}/issues/{args.pr}/comments",
|
||||
token,
|
||||
method="POST",
|
||||
payload=payload,
|
||||
)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected PR comment response.")
|
||||
user = created.get("user") or {}
|
||||
return {
|
||||
"action": "comment",
|
||||
"repo_url": context.repo_url,
|
||||
"pull_request": args.pr,
|
||||
"comment": {
|
||||
"id": created.get("id"),
|
||||
"html_url": created.get("html_url"),
|
||||
"author": user.get("full_name") or user.get("login"),
|
||||
"created_at": created.get("created_at"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.command == "list":
|
||||
payload = list_pull_requests(args)
|
||||
elif args.command == "create":
|
||||
payload = create_pull_request(args)
|
||||
else:
|
||||
payload = comment_pull_request(args)
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,236 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preflight and execute git push with optional Gitea token fallback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from common import (
|
||||
build_authenticated_push_url,
|
||||
current_branch,
|
||||
ensure_git_repo,
|
||||
get_current_user_login,
|
||||
get_upstream,
|
||||
is_auth_error,
|
||||
load_token,
|
||||
mask_url,
|
||||
remote_branch_sha,
|
||||
resolve_repo,
|
||||
worktree_changes,
|
||||
ahead_behind,
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Preflight and optionally push the current branch to Gitea."
|
||||
)
|
||||
parser.add_argument("--repo-url", help="Explicit target repo URL.")
|
||||
parser.add_argument("--repo", help="Shorthand owner/repo. Requires GITEA_BASE_URL.")
|
||||
parser.add_argument("--remote", default="origin", help="Git remote name. Default: origin")
|
||||
parser.add_argument("--branch", help="Branch to push. Default: current branch")
|
||||
parser.add_argument(
|
||||
"--execute",
|
||||
action="store_true",
|
||||
help="Run git push after preflight instead of only printing the plan.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Allow force push. Uses --force-with-lease under the hood.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def build_push_command(target: str, branch: str, force: bool) -> list[str]:
|
||||
command = ["git", "push"]
|
||||
if force:
|
||||
command.append("--force-with-lease")
|
||||
command.extend([target, f"{branch}:{branch}"])
|
||||
return command
|
||||
|
||||
|
||||
def command_text(command: list[str]) -> str:
|
||||
return " ".join(shlex.quote(part) for part in command)
|
||||
|
||||
|
||||
def compute_push_plan(
|
||||
repo_url: str | None = None,
|
||||
repo: str | None = None,
|
||||
remote: str = "origin",
|
||||
branch: str | None = None,
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
ensure_git_repo()
|
||||
context = resolve_repo(repo_url=repo_url, repo=repo, remote=remote)
|
||||
branch_name = branch or current_branch()
|
||||
dirty_lines = worktree_changes()
|
||||
upstream = get_upstream(branch_name)
|
||||
compare_ref = upstream
|
||||
remote_exists = True
|
||||
ahead = 0
|
||||
behind = 0
|
||||
|
||||
if upstream:
|
||||
behind, ahead = ahead_behind(upstream)
|
||||
else:
|
||||
remote_sha = remote_branch_sha(remote, branch_name)
|
||||
if remote_sha:
|
||||
compare_ref = f"{remote}/{branch_name}"
|
||||
behind, ahead = ahead_behind(remote_sha)
|
||||
else:
|
||||
remote_exists = False
|
||||
compare_ref = None
|
||||
ahead = None
|
||||
behind = 0
|
||||
|
||||
diverged = bool(remote_exists and behind > 0 and (ahead or 0) > 0)
|
||||
behind_only = bool(remote_exists and behind > 0 and (ahead or 0) == 0)
|
||||
needs_push = (not remote_exists) or ((ahead or 0) > 0)
|
||||
|
||||
status = "ready"
|
||||
if diverged and not force:
|
||||
status = "blocked"
|
||||
elif not needs_push:
|
||||
status = "noop"
|
||||
|
||||
messages: list[str] = []
|
||||
if dirty_lines:
|
||||
messages.append("Working tree is dirty; push will include only committed changes.")
|
||||
if not remote_exists:
|
||||
messages.append("Remote branch does not exist yet; push will create it.")
|
||||
if behind_only:
|
||||
messages.append("Local branch is behind the remote branch; there are no local commits to push.")
|
||||
if diverged and not force:
|
||||
messages.append("Branch has diverged from remote; rerun with explicit force only if that is intended.")
|
||||
|
||||
push_command = build_push_command(remote, branch_name, force)
|
||||
fallback_command = None
|
||||
token_available = bool(load_token(required=False))
|
||||
if token_available:
|
||||
try:
|
||||
token = load_token(required=False)
|
||||
login = get_current_user_login(context.origin, token)
|
||||
auth_url = build_authenticated_push_url(context.repo_url, login, token)
|
||||
fallback_command = build_push_command(auth_url, branch_name, force)
|
||||
except SystemExit as exc:
|
||||
messages.append(f"Failed to prepare HTTPS token fallback: {exc}")
|
||||
|
||||
return {
|
||||
"repo_url": context.repo_url,
|
||||
"origin": context.origin,
|
||||
"owner": context.owner,
|
||||
"repo": context.repo,
|
||||
"remote": remote,
|
||||
"branch": branch_name,
|
||||
"upstream": upstream,
|
||||
"compare_ref": compare_ref,
|
||||
"remote_branch_exists": remote_exists,
|
||||
"dirty": bool(dirty_lines),
|
||||
"dirty_paths": dirty_lines,
|
||||
"ahead": ahead,
|
||||
"behind": behind,
|
||||
"diverged": diverged,
|
||||
"behind_only": behind_only,
|
||||
"needs_push": needs_push,
|
||||
"status": status,
|
||||
"messages": messages,
|
||||
"push_command": command_text(push_command),
|
||||
"fallback_push_command": command_text(
|
||||
[
|
||||
*(fallback_command[:-2] if fallback_command else []),
|
||||
mask_url(fallback_command[-2]) if fallback_command else "",
|
||||
*(fallback_command[-1:] if fallback_command else []),
|
||||
]
|
||||
)
|
||||
if fallback_command
|
||||
else None,
|
||||
}
|
||||
|
||||
|
||||
def run_command(command: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(command, capture_output=True, text=True)
|
||||
|
||||
|
||||
def execute_push(plan: dict, force: bool = False) -> dict:
|
||||
if plan["status"] == "noop":
|
||||
return {
|
||||
"status": "noop",
|
||||
"executed": False,
|
||||
"reason": "nothing_to_push",
|
||||
"messages": plan["messages"],
|
||||
}
|
||||
if plan["diverged"] and not force:
|
||||
raise SystemExit(
|
||||
"Branch has diverged from remote. Refusing to push without explicit --force."
|
||||
)
|
||||
|
||||
primary_command = build_push_command(plan["remote"], plan["branch"], force)
|
||||
primary_result = run_command(primary_command)
|
||||
if primary_result.returncode == 0:
|
||||
return {
|
||||
"status": "pushed",
|
||||
"executed": True,
|
||||
"method": "remote",
|
||||
"command": command_text(primary_command),
|
||||
"stdout": primary_result.stdout.strip(),
|
||||
"stderr": primary_result.stderr.strip(),
|
||||
}
|
||||
|
||||
primary_stderr = primary_result.stderr.strip() or primary_result.stdout.strip()
|
||||
if not is_auth_error(primary_stderr):
|
||||
raise SystemExit(f"git push failed: {primary_stderr}")
|
||||
|
||||
token = load_token(required=True)
|
||||
login = get_current_user_login(plan["origin"], token)
|
||||
auth_url = build_authenticated_push_url(plan["repo_url"], login, token)
|
||||
fallback_command = build_push_command(auth_url, plan["branch"], force)
|
||||
fallback_result = run_command(fallback_command)
|
||||
if fallback_result.returncode == 0:
|
||||
return {
|
||||
"status": "pushed",
|
||||
"executed": True,
|
||||
"method": "https-token",
|
||||
"command": command_text(
|
||||
[
|
||||
*(fallback_command[:-2]),
|
||||
mask_url(fallback_command[-2]),
|
||||
fallback_command[-1],
|
||||
]
|
||||
),
|
||||
"stdout": fallback_result.stdout.strip(),
|
||||
"stderr": fallback_result.stderr.strip(),
|
||||
}
|
||||
|
||||
fallback_stderr = fallback_result.stderr.strip() or fallback_result.stdout.strip()
|
||||
raise SystemExit(
|
||||
"git push failed with remote auth and HTTPS token fallback: "
|
||||
f"{fallback_stderr}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
plan = compute_push_plan(
|
||||
repo_url=args.repo_url,
|
||||
repo=args.repo,
|
||||
remote=args.remote,
|
||||
branch=args.branch,
|
||||
force=args.force,
|
||||
)
|
||||
if not args.execute:
|
||||
print(json.dumps(plan, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
result = execute_push(plan, force=args.force)
|
||||
payload = {"plan": plan, "result": result}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,172 +0,0 @@
|
||||
---
|
||||
name: issue-drive
|
||||
description: 归集证据并把问题拆成 1 到多张 Gitea issue,支持从当前仓库 origin 自动识别仓库或用户显式指定,并通过环境变量配置的 Gitea API 批量创建工单。
|
||||
---
|
||||
|
||||
# Issue Drive - 通用 Gitea 问题拆单与创建
|
||||
|
||||
> **定位**:不是“查看 issue”,而是把一个真实问题沉淀成可执行的 Gitea issue。适合“线上出 bug 了”“要把一个需求拆成工单”“需要补齐工程治理任务”这类场景。
|
||||
>
|
||||
> 如果用户只是想查 issue 列表或详情,用 `issue`。如果用户想通过统一入口完成 issue / push / PR 组合操作,用 `gitea`。
|
||||
|
||||
当用户调用 `/issue-drive`、`$issue-drive`,或自然语言要求“拆 issue”“提 issue”“把问题沉淀成 Gitea 工单”时,执行以下步骤。
|
||||
|
||||
## 1. 先确定目标仓库
|
||||
|
||||
目标仓库按以下优先级确定:
|
||||
|
||||
1. 用户显式给出的完整仓库 URL:`https://git.example.com/owner/repo`
|
||||
2. 用户显式给出的仓库简写:`owner/repo`,此时需要 `GITEA_BASE_URL`
|
||||
3. 当前项目的 `git remote get-url origin`
|
||||
|
||||
解析规则:
|
||||
|
||||
- 支持 `https://host[/prefix]/owner/repo`
|
||||
- 支持 `git@host:owner/repo.git`
|
||||
- 支持 `git@host:prefix/owner/repo.git`
|
||||
- 支持 `ssh://git@host/owner/repo.git`
|
||||
- 当前仓库 `origin` 是 SSH 地址时:
|
||||
- 优先使用 `GITEA_BASE_URL`
|
||||
- 否则退回 `https://host`
|
||||
|
||||
如果参数和当前仓库都无法确定目标仓库,停止并提示:
|
||||
|
||||
```bash
|
||||
❌ 无法确定目标仓库
|
||||
|
||||
请显式指定仓库,例如:
|
||||
/issue-drive https://git.example.com/owner/repo
|
||||
|
||||
或配置:
|
||||
export GITEA_BASE_URL=https://git.example.com
|
||||
```
|
||||
|
||||
## 2. 检查环境变量
|
||||
|
||||
先读取环境变量:
|
||||
|
||||
- `GITEA_TOKEN`:必需,创建 issue 时使用
|
||||
- `GITEA_BASE_URL`:可选;仓库简写或 SSH origin 时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,输出:
|
||||
|
||||
```bash
|
||||
❌ 缺少 GITEA_TOKEN
|
||||
|
||||
请先在当前 shell 或 .env 中配置:
|
||||
export GITEA_TOKEN=your_gitea_token
|
||||
```
|
||||
|
||||
然后停止,不继续创建 issue。
|
||||
|
||||
## 3. 先读仓库基线和证据
|
||||
|
||||
不要先写 issue。先确认事实、范围、影响和证据入口。
|
||||
|
||||
优先读取以下文件;只读存在的那些:
|
||||
|
||||
1. `docs/ISSUE_WORKFLOW.md`
|
||||
2. `.gitea/ISSUE_TEMPLATE/`
|
||||
3. `.github/ISSUE_TEMPLATE/`
|
||||
4. `README.md`
|
||||
5. `doc/`, `docs/` 下与当前问题直接相关的说明、审计、回归、报表文档
|
||||
|
||||
如果仓库里没有现成 issue 基建,就直接基于用户描述、代码、日志、测试结果和已有文档整理事实,不要因为“模板不完整”而停住。
|
||||
|
||||
## 4. 判断该用哪种 issue
|
||||
|
||||
按下面规则分类:
|
||||
|
||||
- **缺陷 / 异常报告**:已有事实证据,目标是排查、修复、回归
|
||||
- **业务需求 / 功能请求**:目标是交付用户价值,需要产品化描述
|
||||
- **工程任务 / 重构 / 基建**:目标是稳定性、可观测性、测试、流程、治理
|
||||
|
||||
标签策略:
|
||||
|
||||
- 优先复用仓库远端已有标签
|
||||
- 如果远端没有对应标签,不要中断;用标题前缀和正文结构表达类型
|
||||
- 不要为了建 issue 先去重构整套标签体系
|
||||
|
||||
## 5. 拆单规则
|
||||
|
||||
不要默认一张大工单。优先按“可独立修复、可独立验证、可独立关闭”拆。
|
||||
|
||||
优先拆开的情况:
|
||||
|
||||
- 现象修复 和 口径 / 文案 / 导出字段修复 是两件事
|
||||
- 业务缺陷 和 工程可观测性补齐 是两件事
|
||||
- 一个问题需要不同 owner、不同验证方式或不同发布时间
|
||||
|
||||
## 6. 写 issue 前的最小检查
|
||||
|
||||
创建 issue 前必须确认:
|
||||
|
||||
- 标题是否直接表达现象或交付物
|
||||
- 正文是否写明:现象、期望、影响范围、证据入口、初步判断、完成标准
|
||||
- 事实数字是否已经从本地数据、日志、代码或用户描述中复核
|
||||
- 引用的仓库内链接是否已经存在于默认分支
|
||||
|
||||
如果本次要新建或更新以下文件:
|
||||
|
||||
- `.gitea/ISSUE_TEMPLATE/*`
|
||||
- `.github/ISSUE_TEMPLATE/*`
|
||||
- `.gitea/PULL_REQUEST_TEMPLATE.md`
|
||||
- `docs/ISSUE_WORKFLOW.md`
|
||||
- issue 正文会引用的证据文档
|
||||
|
||||
先做最小提交并推送到默认分支,再创建 issue。只提交与 issue 基建或证据直接相关的文件,不要顺手带上无关改动。
|
||||
|
||||
## 7. 用脚本批量创建 issue
|
||||
|
||||
优先使用本 skill 附带脚本,并按当前平台选择路径:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json --dry-run
|
||||
python3 .codex/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json --dry-run
|
||||
python3 .claude/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json
|
||||
```
|
||||
|
||||
JSON 结构固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"repo_url": "https://git.example.com/owner/repo",
|
||||
"issues": [
|
||||
{
|
||||
"title": "[缺陷][登录] 示例标题",
|
||||
"body": "Issue 正文",
|
||||
"labels": ["P1", "bug"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `repo_url` 可省略;脚本会按 `--repo-url` > `spec.repo_url` > `spec.repo` > 当前仓库 `origin` 的顺序解析
|
||||
- `spec.repo` 允许写 `owner/repo`,但需要 `GITEA_BASE_URL`
|
||||
- Token 固定读取 `GITEA_TOKEN`
|
||||
- `labels` 写标签名,脚本会自动解析成远端 label id
|
||||
- 远端不存在的标签会告警并自动跳过,不中断整个批次
|
||||
- 先跑 `--dry-run`,确认 payload 和目标仓库无误后再正式创建
|
||||
|
||||
## 8. 输出结果
|
||||
|
||||
创建成功后,输出:
|
||||
|
||||
- 已推送的提交信息(如果本次为了 issue 基建或证据先推了提交)
|
||||
- 新建 issue 的编号、标题、URL
|
||||
- 哪些标签成功命中,哪些因远端不存在被跳过
|
||||
- 如果拆成多张 issue,要说明每张工单分别驱动什么工作
|
||||
|
||||
## 9. 行为边界
|
||||
|
||||
- 默认以中文写 issue;用户明确要求英文时再切换
|
||||
- 不把一个大问题硬塞进一张工单
|
||||
- 不在 issue 里写超长散文,优先写清单、事实和完成标准
|
||||
- 不因为缺少完美自动化复现就拒绝提单;现网证据或稳定复现路径成立时,可以先提
|
||||
- 创建 issue 后,不自动继续写修复代码,除非用户明确要求
|
||||
@ -1,268 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create one or more Gitea issues from a JSON spec."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
SSH_RE = re.compile(r"^git@(?P<host>[^:]+):(?P<path>.+?)(?:\.git)?/?$")
|
||||
REPO_PATH_RE = re.compile(r"^(?P<owner>[^/]+)/(?P<repo>[^/]+)$")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Batch-create Gitea issues from a JSON spec."
|
||||
)
|
||||
parser.add_argument("spec", help="Path to JSON spec file")
|
||||
parser.add_argument(
|
||||
"--repo-url",
|
||||
help=(
|
||||
"Override target repo. Accepts full https URL, SSH git origin, "
|
||||
"or owner/repo with GITEA_BASE_URL."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repo",
|
||||
help="Shorthand owner/repo. Requires GITEA_BASE_URL.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Validate and print the payload without creating issues",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_token(required: bool = True) -> str:
|
||||
token = os.getenv("GITEA_TOKEN")
|
||||
if not token and required:
|
||||
raise SystemExit(
|
||||
"Missing GITEA_TOKEN. Export the token before creating issues."
|
||||
)
|
||||
return token or ""
|
||||
|
||||
|
||||
def load_spec(path: str) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
if not isinstance(data, dict) or not isinstance(data.get("issues"), list):
|
||||
raise SystemExit("Spec must be an object with an 'issues' array.")
|
||||
if not data["issues"]:
|
||||
raise SystemExit("Spec contains no issues.")
|
||||
return data
|
||||
|
||||
|
||||
def normalize_base_url(base_url: str) -> str:
|
||||
return base_url.rstrip("/")
|
||||
|
||||
|
||||
def parse_http_repo_url(repo_url: str) -> tuple[str, str, str]:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
if prefix:
|
||||
origin = f"{origin}/{prefix}"
|
||||
return origin, owner, repo
|
||||
|
||||
|
||||
def parse_repo_target(repo_target: str, base_url: str | None = None) -> tuple[str, str, str, str]:
|
||||
value = repo_target.strip()
|
||||
if not value:
|
||||
raise SystemExit("Repo target cannot be empty.")
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
origin, owner, repo = parse_http_repo_url(value)
|
||||
repo_url = f"{origin}/{owner}/{repo}"
|
||||
return origin, owner, repo, repo_url
|
||||
|
||||
if value.startswith("ssh://"):
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid SSH repo URL. Expected ssh://git@host/owner/repo.git")
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
host = parsed.hostname
|
||||
if not host:
|
||||
raise SystemExit("Invalid SSH repo URL. Missing host.")
|
||||
origin = normalize_base_url(base_url or f"https://{host}")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
ssh_match = SSH_RE.match(value)
|
||||
if ssh_match:
|
||||
path = ssh_match.group("path").rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid SSH repo target. Expected git@host:owner/repo.git")
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
origin = normalize_base_url(base_url or f"https://{ssh_match.group('host')}")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
path_match = REPO_PATH_RE.match(value)
|
||||
if path_match:
|
||||
if not base_url:
|
||||
raise SystemExit(
|
||||
"Repo shorthand owner/repo requires GITEA_BASE_URL or --repo-url."
|
||||
)
|
||||
origin = normalize_base_url(base_url)
|
||||
owner = path_match.group("owner")
|
||||
repo = path_match.group("repo")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
raise SystemExit(
|
||||
"Invalid repo target. Use https://host/owner/repo, git@host:owner/repo.git, "
|
||||
"ssh://git@host/owner/repo.git, or owner/repo with GITEA_BASE_URL."
|
||||
)
|
||||
|
||||
|
||||
def get_origin_repo_target() -> str:
|
||||
try:
|
||||
return subprocess.check_output(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
text=True,
|
||||
stderr=subprocess.STDOUT,
|
||||
).strip()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise SystemExit(f"Failed to read git origin: {exc.output.strip()}") from exc
|
||||
|
||||
|
||||
def resolve_repo(spec: dict, args: argparse.Namespace) -> tuple[str, str, str, str]:
|
||||
base_url = os.getenv("GITEA_BASE_URL")
|
||||
repo_target = (
|
||||
args.repo_url
|
||||
or args.repo
|
||||
or spec.get("repo_url")
|
||||
or spec.get("repo")
|
||||
or get_origin_repo_target()
|
||||
)
|
||||
return parse_repo_target(repo_target, base_url=base_url)
|
||||
|
||||
|
||||
def request_json(
|
||||
url: str,
|
||||
token: str,
|
||||
method: str = "GET",
|
||||
payload: dict | None = None,
|
||||
) -> dict | list:
|
||||
data = None
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace")
|
||||
raise SystemExit(
|
||||
f"Gitea API request failed: {exc.code} {exc.reason} | {body}"
|
||||
) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise SystemExit(f"Failed to reach Gitea API: {exc.reason}") from exc
|
||||
|
||||
|
||||
def fetch_label_map(api_base: str, token: str) -> dict[str, int]:
|
||||
data = request_json(f"{api_base}/labels?page=1&limit=100", token)
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit("Unexpected labels API response.")
|
||||
return {
|
||||
item["name"]: item["id"]
|
||||
for item in data
|
||||
if "name" in item and "id" in item
|
||||
}
|
||||
|
||||
|
||||
def resolve_label_ids(label_names: list[str], label_map: dict[str, int]) -> tuple[list[int], list[str]]:
|
||||
ids: list[int] = []
|
||||
missing: list[str] = []
|
||||
for name in label_names:
|
||||
if name in label_map:
|
||||
ids.append(label_map[name])
|
||||
else:
|
||||
missing.append(name)
|
||||
return ids, missing
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
token = load_token(required=not args.dry_run)
|
||||
spec = load_spec(args.spec)
|
||||
origin, owner, repo, repo_url = resolve_repo(spec, args)
|
||||
api_base = f"{origin}/api/v1/repos/{owner}/{repo}"
|
||||
label_map = fetch_label_map(api_base, token) if token else {}
|
||||
|
||||
issues = spec["issues"]
|
||||
for issue in issues:
|
||||
if not isinstance(issue, dict):
|
||||
raise SystemExit("Each issue entry must be an object.")
|
||||
if not issue.get("title") or not issue.get("body"):
|
||||
raise SystemExit("Each issue must include non-empty 'title' and 'body'.")
|
||||
|
||||
for issue in issues:
|
||||
label_names = issue.get("labels") or []
|
||||
if not isinstance(label_names, list) or not all(
|
||||
isinstance(name, str) for name in label_names
|
||||
):
|
||||
raise SystemExit("'labels' must be an array of strings.")
|
||||
|
||||
label_ids, missing_labels = resolve_label_ids(label_names, label_map)
|
||||
payload = {
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
"labels": label_ids,
|
||||
}
|
||||
|
||||
if args.dry_run:
|
||||
print(f"repo_url={repo_url}")
|
||||
print(f"DRY-RUN\t{issue['title']}")
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
if missing_labels:
|
||||
print(f"missing_labels={missing_labels}")
|
||||
if not token:
|
||||
print("warning\tno token provided, remote label validation skipped")
|
||||
continue
|
||||
|
||||
created = request_json(f"{api_base}/issues", token, method="POST", payload=payload)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected issue creation response.")
|
||||
print(
|
||||
f"#{created.get('number')}\t{created.get('title')}\t{created.get('html_url')}"
|
||||
)
|
||||
if missing_labels:
|
||||
print(f"warning\tmissing labels skipped: {', '.join(missing_labels)}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,222 +0,0 @@
|
||||
---
|
||||
name: issue
|
||||
description: 查看当前仓库或任意 Gitea 仓库的 issue 列表和单条详情,支持自动识别 git origin、用户指定仓库、状态筛选和格式化输出。
|
||||
---
|
||||
|
||||
# Issue - 通用 Gitea Issue 查看
|
||||
|
||||
> **定位**:这是只读 skill。用于查看当前仓库或指定仓库的 issue 列表、单条详情和评论,不负责拆单、创建工单、push 或 PR。
|
||||
>
|
||||
> 如果用户要做的是“把问题拆成 issue 并实际创建工单”,改用 `issue-drive`。如果用户想用统一入口处理 Gitea 相关任务,改用 `gitea`。
|
||||
|
||||
当用户调用 `/issue`、`$issue`,或自然语言要求“查看当前仓库 issue”“看某个 Gitea 仓库的 issue”“查 issue #17”时,执行以下步骤。
|
||||
|
||||
## 1. 解析输入
|
||||
|
||||
支持以下几种目标仓库写法:
|
||||
|
||||
- 不传仓库参数:默认使用当前项目的 `git remote get-url origin`
|
||||
- 完整仓库 URL:`https://git.example.com/owner/repo`
|
||||
- 仓库简写:`owner/repo`
|
||||
- 当前仓库 + 单条 issue:`/issue 17`
|
||||
- 指定仓库 + 单条 issue:`/issue owner/repo 17` 或 `/issue https://git.example.com/owner/repo 17`
|
||||
|
||||
同时支持:
|
||||
|
||||
- `--state=open|closed|all`,默认 `open`
|
||||
- `--limit=<N>`,默认 `50`
|
||||
|
||||
解析规则:
|
||||
|
||||
- 第二个位置参数如果是纯数字,视为 `issue-number`,进入详情模式
|
||||
- 如果第一个位置参数是纯数字,则视为“当前仓库的 issue 编号”
|
||||
- `owner/repo` 这种简写依赖 `GITEA_BASE_URL`
|
||||
- 如果没有显式仓库参数,就读取当前仓库的 `origin`
|
||||
|
||||
规范化仓库目标时,接受以下输入:
|
||||
|
||||
- `https://host[/prefix]/owner/repo`
|
||||
- `git@host:owner/repo.git`
|
||||
- `git@host:prefix/owner/repo.git`
|
||||
- `ssh://git@host/owner/repo.git`
|
||||
- `owner/repo`
|
||||
|
||||
提取结果必须包含:
|
||||
|
||||
- `origin`:例如 `https://git.example.com`,如果 Gitea 部署在子路径下,保留前缀,例如 `https://git.example.com/gitea`
|
||||
- `owner`
|
||||
- `repo`
|
||||
- `repo_path`:`owner/repo`
|
||||
|
||||
如果无法从参数或当前仓库推断出目标仓库,明确提示:
|
||||
|
||||
```bash
|
||||
❌ 无法确定目标仓库
|
||||
|
||||
请显式传入:
|
||||
/issue https://git.example.com/owner/repo
|
||||
|
||||
或先配置:
|
||||
export GITEA_BASE_URL=https://git.example.com
|
||||
```
|
||||
|
||||
## 2. 检查环境变量
|
||||
|
||||
先读取环境变量:
|
||||
|
||||
- `GITEA_TOKEN`:必需,读取 issue 时使用
|
||||
- `GITEA_BASE_URL`:可选;当仓库参数是 `owner/repo`,或当前仓库 `origin` 是 SSH 地址时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,输出:
|
||||
|
||||
```bash
|
||||
❌ 缺少 GITEA_TOKEN
|
||||
|
||||
请先在当前 shell 或 .env 中配置:
|
||||
export GITEA_TOKEN=your_gitea_token
|
||||
```
|
||||
|
||||
然后停止,不继续请求 API。
|
||||
|
||||
## 3. 解析当前仓库 origin
|
||||
|
||||
当没有显式传仓库参数时,执行:
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
```
|
||||
|
||||
处理规则:
|
||||
|
||||
- 如果是 `https://host[/prefix]/owner/repo(.git)`,直接使用
|
||||
- 如果是 `git@host:owner/repo(.git)` 或 `ssh://git@host/owner/repo(.git)`:
|
||||
- 优先用 `GITEA_BASE_URL` 作为 API/Web 基地址
|
||||
- 否则退回 `https://host`
|
||||
- 如果当前目录不是 git 仓库,或没有 `origin`,停止并提示用户显式传仓库
|
||||
|
||||
不要为了查 issue 再向用户追问仓库 URL;只有在当前项目和参数都无法推断时才提示。
|
||||
|
||||
## 4. 调用 Gitea API
|
||||
|
||||
不要调用仓库元信息接口,避免依赖额外 scope。仓库标题直接使用 `repo_path`。
|
||||
|
||||
### 4.1 列表模式
|
||||
|
||||
请求:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issues.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues?state=${state}&limit=${limit}"
|
||||
```
|
||||
|
||||
### 4.2 详情模式
|
||||
|
||||
先请求 issue 详情:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issue_detail.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues/${issue_number}"
|
||||
```
|
||||
|
||||
再请求评论:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issue_comments.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues/${issue_number}/comments"
|
||||
```
|
||||
|
||||
### 4.3 错误处理
|
||||
|
||||
根据 HTTP 状态码给出简短、直接的提示:
|
||||
|
||||
- `401`:`GITEA_TOKEN` 无效或未生效
|
||||
- `403`:token scope 不足,或当前用户无权访问该仓库 / issue
|
||||
- `404`:仓库不存在,或该 issue 编号不存在
|
||||
- 其他非 `2xx`:输出状态码和响应中的 `message`
|
||||
|
||||
如果列表接口返回项里存在 `pull_request` 且非空,排除这些项,只保留 issue。
|
||||
|
||||
## 5. 格式化输出
|
||||
|
||||
优先使用 `jq` 解析 JSON;如果环境没有 `jq`,再退回模型手工整理,但输出结构保持一致。
|
||||
|
||||
### 5.1 列表模式
|
||||
|
||||
输出结构固定为:
|
||||
|
||||
```markdown
|
||||
📋 {repo_path} Issues
|
||||
|
||||
状态: {state} | 数量: {count} | 限制: {limit}
|
||||
|
||||
| # | 标题 | 优先级 | 标签 | 状态 | 提出人 |
|
||||
|---|------|--------|------|------|--------|
|
||||
| 35 | 小红书笔记状态查询 | P:紧急 | P:紧急, 需求 | open | zhangsan |
|
||||
```
|
||||
|
||||
字段规则:
|
||||
|
||||
- `优先级`:从 labels 中提取首个匹配 `^P[::]` 的 label,没有则填 `-`
|
||||
- `标签`:保留全部 label 原文,用 `, ` 连接;没有则填 `-`
|
||||
- `提出人`:优先显示 `user.full_name`,没有则显示 `user.login`
|
||||
- `状态`:直接显示 `open` 或 `closed`
|
||||
|
||||
如果过滤后没有任何 issue,明确输出“无符合条件的 issue”。
|
||||
|
||||
### 5.2 详情模式
|
||||
|
||||
输出结构固定为:
|
||||
|
||||
```markdown
|
||||
## #{number} {title}
|
||||
|
||||
- 仓库: {repo_path}
|
||||
- 状态: {state}
|
||||
- 标签: {labels}
|
||||
- 提出人: {author}
|
||||
- 创建时间: {created_at}
|
||||
- 更新时间: {updated_at}
|
||||
|
||||
### 正文
|
||||
|
||||
{body 或 “无正文”}
|
||||
|
||||
### 评论
|
||||
|
||||
- {author} | {created_at} | {1-2 句摘要}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 正文为空时写“无正文”
|
||||
- 评论按时间顺序输出
|
||||
- 每条评论只保留 1 到 2 句摘要;不要整段照抄超长评论
|
||||
- 没有评论时明确写“无评论”
|
||||
|
||||
## 6. 行为边界
|
||||
|
||||
- 默认只做列表和单条详情,不主动做主题归纳、epic 合并、优先级建议
|
||||
- 用户后续如果要求摘要、优先级排序、相似 issue 合并,再基于已拉取的数据继续分析
|
||||
- 不要求用户额外配置固定仓库 URL;优先从当前项目推断
|
||||
- 当前仓库 origin 与 Web 域名不一致时,再使用 `GITEA_BASE_URL`
|
||||
|
||||
## 7. 用法示例
|
||||
|
||||
```bash
|
||||
/issue
|
||||
/issue 17
|
||||
/issue owner/repo
|
||||
/issue owner/repo --state=all --limit=20
|
||||
/issue https://git.example.com/owner/repo
|
||||
/issue https://git.example.com/owner/repo 17
|
||||
|
||||
$issue
|
||||
$issue 17
|
||||
$issue owner/repo
|
||||
$issue owner/repo --state=all --limit=20
|
||||
$issue https://git.example.com/owner/repo
|
||||
$issue https://git.example.com/owner/repo 17
|
||||
```
|
||||
@ -5,14 +5,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 最重要的事情
|
||||
|
||||
1. **最优路径优先** - 先判断当前约束下的最优路径,锁定后直接推进;不先为了自我防御铺退路、保守版或兼容版
|
||||
2. **TDD 先行** - fix/feat 必须先写失败测试,红黄绿循环
|
||||
3. **原子提交** - 每个 commit 只做一件事,可独立回滚
|
||||
4. **文档驱动** - feat 改动关联 doc/ 下文档,多输出表格、流程图、ASCII 原型图
|
||||
5. **知识沉淀** - 有价值的迭代沉淀到 CLAUDE.md(拿捏不准主动问我)
|
||||
6. **利用现有工具** - 不重复造轮子,会开车 > 会修车
|
||||
7. **有头有尾** - 头:只问会改变最优路径判断的问题,锁定后立刻动手;尾:自己跑验证,不把验证甩给用户
|
||||
8. **任务结束后追加** - 主人,用不用我沉淀 or git 提交?
|
||||
1. **TDD 先行** - fix/feat 必须先写失败测试,红黄绿循环
|
||||
2. **原子提交** - 每个 commit 只做一件事,可独立回滚
|
||||
3. **文档驱动** - feat 改动关联 doc/ 下文档,多输出表格、流程图、ASCII 原型图
|
||||
4. **知识沉淀** - 有价值的迭代沉淀到 CLAUDE.md(拿捏不准主动问我)
|
||||
5. **利用现有工具** - 不重复造轮子,会开车 > 会修车
|
||||
6. **有头有尾** - 头:确认清楚再动手,不清楚就一直问;尾:自己跑验证,不把验证甩给用户
|
||||
7. **任务结束后追加** - 主人,用不用我沉淀 or git 提交?
|
||||
|
||||
## 项目概述
|
||||
|
||||
@ -79,15 +78,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- **包管理器**: 使用 {{包管理器}}(不是 {{其他包管理器}})
|
||||
- **TDD 流程**: 先写测试再实现,核心业务逻辑覆盖率 100%
|
||||
- **日志规范**: 使用日志管理器,避免 console.log
|
||||
- **入口统一校验**: 所有外部输入(API 参数、用户输入、配置项)在入口处统一校验,内部代码信任已校验的数据,不重复检查
|
||||
- **空值显式报错**: 禁止静默吞掉 null/undefined/空字符串,遇到非预期空值必须抛出明确错误信息(含变量名和上下文),让问题在第一现场暴露
|
||||
- **关键节点结构化日志**: 在业务关键节点(请求进出、状态变更、外部调用、异常捕获)输出结构化日志,包含 traceId、操作、耗时、结果,便于排查和监控
|
||||
- **知识沉淀**: 将有价值的对话迭代沉淀到文档中,包括:
|
||||
- 重要技术决策和架构演进 → 更新 CLAUDE.md 相关章节
|
||||
- 新功能实现方案 → 更新组件职责、数据流等章节
|
||||
- 踩坑经验和解决方案 → 添加到踩坑经验章节
|
||||
- API 使用技巧和注意事项 → 更新相关技术栈说明
|
||||
- **成功经验复刻**: 一次对话形成可复用结果后,用 `/capture <目录>` 或 `$capture <目录>` 输出结构化记录;另留单独位置保存“味道”
|
||||
|
||||
{{在此添加项目特定的开发约定}}
|
||||
|
||||
@ -95,17 +90,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
### 任务有头有尾
|
||||
|
||||
**头 — 先锁定最优路径再动手**:
|
||||
**头 — 确认清楚再动手**:
|
||||
- 收到任务后,先复述理解、列出不确定的点
|
||||
- 只追问会改变最优路径判断的问题;能基于上下文和环境判断的,不拿来做前置防御
|
||||
- 一旦最优路径锁定,立即沿该路径推进,不先铺退路、折中版或兼容版
|
||||
- 不确定就问,一直问到双方对齐为止,**绝不带着假设开工**
|
||||
- 确认范围边界:做什么、不做什么、验收标准
|
||||
|
||||
**尾 — 自己验证,说到做到**:
|
||||
- 任务完成后,自己执行验证(跑测试、构建、截图、检查输出等)
|
||||
- 把验证结果直接展示给用户,而不是列一堆步骤让用户自己验
|
||||
- 验证不通过就自己修,循环直到通过
|
||||
- 最终交付物 = 已通过的验证结果
|
||||
- 最终交付物 = 已通过的验证结果根据既定的方案(所以倒逼开始的时候更明确才执行,否则自己打自己的脸。)
|
||||
|
||||
### 其他
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
just empety...
|
||||
@ -1,139 +0,0 @@
|
||||
---
|
||||
name: capture
|
||||
description: 复刻一次成功任务的经验,输出到用户指定目录。调用方式 `/capture <目录>` 或 `$capture <目录>`,生成只含 fenced YAML 的 Markdown 记录。
|
||||
---
|
||||
|
||||
# Capture - 成功经验复刻
|
||||
|
||||
> **定位**:把当前会话里已经跑通、值得复用的做法沉淀成结构化记录,供其他仓库直接复刻。
|
||||
|
||||
当用户调用 `/capture <target_dir>`、`$capture <target_dir>`,或自然语言要求“把这次成功经验沉淀/复刻到某个目录”时,执行以下步骤。
|
||||
|
||||
## 1. 锁定输出目录
|
||||
|
||||
- 如果用户给了目录,直接使用。
|
||||
- 绝对路径原样使用;相对路径相对当前仓库根目录。
|
||||
- 如果没给目录,只追问一次:`要保存到哪个目录?`
|
||||
- 目录不存在就创建。
|
||||
|
||||
## 2. 收集事实
|
||||
|
||||
只从这些来源取材:
|
||||
|
||||
- 当前会话里的目标、决策、问题、修复、验证结果
|
||||
- 当前任务产物、相关文件、diff、日志
|
||||
- 明确发生过且已经验证的修复
|
||||
|
||||
不要做这些事:
|
||||
|
||||
- 不复述用户原需求
|
||||
- 不编造未知信息
|
||||
- 不把“计划中但没发生”的内容写成事实
|
||||
|
||||
## 3. 生成记录
|
||||
|
||||
### 3.1 命名
|
||||
|
||||
- `task` 用任务本质的短名字,避免 `misc`、`update-doc` 这类空名。
|
||||
- 文件名:`YYYY-MM-DD-task-slug.md`
|
||||
- 若重名,依次追加 `-2`、`-3`
|
||||
|
||||
### 3.2 内容规则
|
||||
|
||||
- 文件内容只能有一个 fenced code block,语言标记固定为 `yaml`
|
||||
- code block 外不要写任何文字
|
||||
- `raw_request` 只写归一化摘要;拿不准就留空
|
||||
- 空标量字段留空
|
||||
- 空列表字段写 `[]`,不要保留空 `-`
|
||||
- 每个 bullet 尽量控制在两行内
|
||||
- 重点写:目标、输入输出、必须正确项、风险、关键决策、问题修复、成功原因、可复用模式
|
||||
- `why_it_worked` 只写真正起作用的因素
|
||||
- `reusable_pattern` 必须可执行,优先写先做什么、看什么、按什么顺序复用
|
||||
- `problems_and_fixes` 只写实际发生且已验证的项
|
||||
|
||||
### 3.3 固定模板
|
||||
|
||||
~~~~markdown
|
||||
```yaml
|
||||
task: <task_name>
|
||||
|
||||
goal: >
|
||||
<一句话目标>
|
||||
|
||||
context:
|
||||
scene:
|
||||
trigger:
|
||||
audience:
|
||||
deadline:
|
||||
constraints:
|
||||
-
|
||||
|
||||
input:
|
||||
raw_request:
|
||||
known:
|
||||
-
|
||||
unknown:
|
||||
-
|
||||
dependencies:
|
||||
-
|
||||
|
||||
output:
|
||||
deliverable:
|
||||
consumer:
|
||||
usage:
|
||||
acceptance:
|
||||
-
|
||||
|
||||
must_be_right:
|
||||
-
|
||||
|
||||
can_be_rough:
|
||||
-
|
||||
|
||||
risks:
|
||||
-
|
||||
|
||||
decisions:
|
||||
- choice:
|
||||
reason:
|
||||
|
||||
execution:
|
||||
-
|
||||
|
||||
problems_and_fixes:
|
||||
- problem:
|
||||
symptom:
|
||||
cause:
|
||||
fix:
|
||||
status:
|
||||
|
||||
why_it_worked:
|
||||
-
|
||||
|
||||
reusable_pattern:
|
||||
first_step:
|
||||
checkpoints:
|
||||
-
|
||||
reusable_steps:
|
||||
-
|
||||
anti_patterns:
|
||||
-
|
||||
|
||||
one_line_summary: >
|
||||
<一句话总结为什么成功>
|
||||
```
|
||||
~~~~
|
||||
|
||||
如果某个列表字段没有内容,改成 `[]`。如果某个标量字段没有内容,保持为空,不要删字段。
|
||||
|
||||
## 4. 保存和回执
|
||||
|
||||
- 保存到目标目录,不覆盖同名旧文件
|
||||
- 默认不把全文再贴回对话,除非用户明确要求
|
||||
- 回执只说三件事:保存路径、任务名、提醒主人还应该准备一个单独“保存味道”的地方
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 本 skill 只沉淀记录,不修改业务代码
|
||||
- 信息不足时允许留空,但不要跳过关键字段
|
||||
- 如果任务结果并不成功或还没稳定,不要硬写成“成功经验”
|
||||
@ -1,316 +0,0 @@
|
||||
---
|
||||
name: deploy
|
||||
description: Drone CI + 服务器 CD 全流程引导:从基础设施检查到生成配置文件到验证部署,交互式完成。
|
||||
---
|
||||
|
||||
# Deploy - CI/CD 全流程部署引导
|
||||
|
||||
> **定位**:基于公司 Drone CI + 私有 Docker Registry + Docker Compose 的自动化部署方案,交互式引导用户从零完成 CI/CD 接入。
|
||||
|
||||
当用户调用 `/deploy` 或 `/deploy <指令>` 时,执行以下步骤:
|
||||
|
||||
## 1. 收集项目信息
|
||||
|
||||
快速了解项目情况(已知的不重复问):
|
||||
|
||||
| 项目 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 项目名称 | 用于镜像命名 | `douyin`, `crm`, `blog` |
|
||||
| 需构建的服务 | 每个服务对应一个镜像 | `backend`, `frontend` |
|
||||
| 各服务的 Dockerfile 路径 | Docker build context | `./backend`, `./frontend` |
|
||||
| 生产服务器 SSH 端口 | 默认 22 | `22`, `3141` |
|
||||
| 部署目录 | 生产服务器上的路径 | `/opt/docker/myproject` |
|
||||
| 数据库迁移命令 | 如有 | `alembic upgrade head`, `npx prisma migrate deploy` |
|
||||
| 健康检查方式 | 三选一 | python / curl / host |
|
||||
| 健康检查 URL | 容器内地址 | `http://127.0.0.1:8000/health` |
|
||||
| 通知 Webhook | 可选,不配则跳过 | 企业微信/钉钉/飞书 |
|
||||
|
||||
确认后进入下一步。
|
||||
|
||||
## 2. 基础设施检查
|
||||
|
||||
输出 Checklist,让用户逐项确认(首次接入需全部完成,后续项目可跳过):
|
||||
|
||||
```
|
||||
□ 基础设施(一次性,已完成则跳过)
|
||||
□ Drone CI Server + Runner 已部署运行
|
||||
□ 私有 Docker Registry 已运行(默认 :5000)
|
||||
□ insecure-registries 已配置(Drone CI 服务器 + 生产服务器)
|
||||
□ SSH 密钥已配置(Drone CI 服务器 → 生产服务器免密登录)
|
||||
□ 生产服务器用户已加入 docker 组
|
||||
```
|
||||
|
||||
如果用户表示基础设施未就绪,输出对应的一次性搭建指引(参见下方「附录:基础设施搭建」)。
|
||||
|
||||
## 3. 生成配置文件
|
||||
|
||||
基于收集到的信息,生成以下文件:
|
||||
|
||||
### 3.1 `.drone.yml`
|
||||
|
||||
核心原则(踩坑总结,不可违反):
|
||||
1. 使用 `docker:27-cli` + 宿主机 Docker socket,**不用** `plugins/docker` DinD
|
||||
2. 使用 `environment: { VAR: { from_secret: name } }` 注入密钥,**不用** `secrets:` 字段
|
||||
3. 使用 `${DRONE_TAG:-latest}` 作为镜像 tag,**不自定义中间变量**(Drone 变量替换冲突)
|
||||
4. 触发条件只用 `event: [tag, cron]`,**不叠加** `cron: [name]`(AND 运算陷阱)
|
||||
|
||||
生成内容包括:
|
||||
- `trigger`: tag + cron
|
||||
- `volumes`: 挂载宿主机 Docker socket
|
||||
- 每个服务的 `build-<service>` step
|
||||
- `deploy` step(使用 `appleboy/drone-ssh`)
|
||||
- `notify-success` / `notify-failure` step(如配置了 Webhook)
|
||||
|
||||
### 3.2 `scripts/deploy-remote.sh`
|
||||
|
||||
部署脚本要点:
|
||||
- `set -euo pipefail` 严格模式
|
||||
- 部署锁(PID 文件防并发)
|
||||
- 同时 export `IMAGE_TAG` 和 `VERSION`(兼容不同 compose 变量命名)
|
||||
- 按顺序:pull → 停 beat → 更新核心服务 → 健康检查 → 数据库迁移 → 启动剩余服务 → 最终健康检查
|
||||
- 健康检查根据用户选择的方式生成(python / curl / host)
|
||||
|
||||
### 3.3 生成后展示
|
||||
|
||||
```
|
||||
已生成:
|
||||
📄 .drone.yml — Drone CI 流水线配置
|
||||
📄 scripts/deploy-remote.sh — 远程部署脚本
|
||||
|
||||
确认写入?[Y/n]
|
||||
```
|
||||
|
||||
用户确认后写入文件。
|
||||
|
||||
## 4. Drone 面板配置引导
|
||||
|
||||
生成文件后,输出需要在 Drone 面板手动配置的清单:
|
||||
|
||||
### 4.1 仓库设置
|
||||
|
||||
```
|
||||
在 Drone 面板完成以下配置:
|
||||
|
||||
1. 激活仓库:SYNC → 找到仓库 → ACTIVATE
|
||||
2. 开启 Trusted:Settings → General → Project Settings → 勾选 Trusted
|
||||
```
|
||||
|
||||
### 4.2 Secrets 配置
|
||||
|
||||
根据收集到的信息,输出具体的 Secret 列表:
|
||||
|
||||
```
|
||||
在 Drone 面板 → 仓库 Settings → Secrets 添加:
|
||||
|
||||
| Secret 名称 | 填写内容 |
|
||||
|-------------------|----------------------------------------------------|
|
||||
| backend_repo | docker.internal.intelligrow.cn:5000/{project}-backend |
|
||||
| frontend_repo | docker.internal.intelligrow.cn:5000/{project}-frontend |
|
||||
| deploy_host | {生产服务器 IP} |
|
||||
| deploy_user | {SSH 用户} |
|
||||
| deploy_ssh_key | cat ~/.ssh/drone_deploy 的完整内容 |
|
||||
| deploy_path | {部署目录} |
|
||||
| wecom_webhook | {Webhook URL}(如已配置) |
|
||||
```
|
||||
|
||||
### 4.3 Cron 配置(可选)
|
||||
|
||||
```
|
||||
如需定时构建,在 Settings → Cron Jobs 添加:
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|----------|------------------|-------------------------|
|
||||
| Name | nightly-build | 任务名称 |
|
||||
| Branch | main | 构建分支 |
|
||||
| Schedule | 0 16 * * * | UTC 16:00 = 北京 00:00 |
|
||||
```
|
||||
|
||||
## 5. 生产服务器配置引导
|
||||
|
||||
```
|
||||
在生产服务器上确认:
|
||||
|
||||
1. 部署目录结构:
|
||||
{deploy_path}/
|
||||
├── docker-compose.prod.yml
|
||||
├── .env
|
||||
└── scripts/
|
||||
└── deploy-remote.sh ← 需从代码仓库复制
|
||||
|
||||
2. .env 至少包含:
|
||||
DOCKER_REGISTRY=docker.internal.intelligrow.cn:5000
|
||||
(其他数据库密码等生产配置)
|
||||
|
||||
3. docker-compose.prod.yml 中镜像引用格式:
|
||||
image: ${DOCKER_REGISTRY}/{project}-backend:${VERSION:-latest}
|
||||
|
||||
⚠️ 变量一致性:Drone Secret 的镜像地址前缀 = .env 的 DOCKER_REGISTRY = compose 中的镜像名拼接结果
|
||||
```
|
||||
|
||||
## 6. 验证
|
||||
|
||||
自己执行可执行的验证,不能远程执行的给出命令让用户确认结果:
|
||||
|
||||
### 6.1 本地验证(自己执行)
|
||||
|
||||
```bash
|
||||
# 检查 .drone.yml 语法合法性(YAML 解析)
|
||||
# 检查 deploy-remote.sh 语法(bash -n)
|
||||
# 检查文件是否已正确写入
|
||||
```
|
||||
|
||||
### 6.2 远程验证引导(输出命令,让用户在服务器上执行并反馈结果)
|
||||
|
||||
```bash
|
||||
# 推送 Tag 触发首次构建
|
||||
git tag v{version}
|
||||
git push origin v{version}
|
||||
|
||||
# 观察 Drone 面板 pipeline 状态
|
||||
|
||||
# 生产服务器检查
|
||||
ssh -p {port} {user}@{host} "cd {deploy_path} && docker compose -f docker-compose.prod.yml ps"
|
||||
```
|
||||
|
||||
## 7. 完成输出
|
||||
|
||||
```
|
||||
✅ CI/CD 接入完成!
|
||||
|
||||
📄 生成的文件:
|
||||
- .drone.yml
|
||||
- scripts/deploy-remote.sh
|
||||
|
||||
🔧 Drone 面板配置(需手动):
|
||||
- [x] 仓库已激活
|
||||
- [x] Trusted 已开启
|
||||
- [x] Secrets 已添加
|
||||
- [ ] Cron Job(可选)
|
||||
|
||||
🖥️ 生产服务器:
|
||||
- [ ] .env 已配置
|
||||
- [ ] deploy-remote.sh 已复制
|
||||
- [ ] 首次部署成功
|
||||
|
||||
📖 回滚方案:
|
||||
方式1: ssh 到生产服务器执行 bash scripts/deploy-remote.sh {旧版本tag}
|
||||
方式2: git tag {旧版本}-rollback {旧版本} && git push origin {旧版本}-rollback
|
||||
|
||||
主人,用不用我沉淀 or git 提交?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:基础设施搭建
|
||||
|
||||
当用户表示基础设施未就绪时,按需输出以下指引:
|
||||
|
||||
### A. Drone CI 部署
|
||||
|
||||
在 Drone CI 服务器创建 `~/drone/docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
drone-server:
|
||||
image: drone/drone:2
|
||||
container_name: drone-server
|
||||
restart: always
|
||||
ports:
|
||||
- "3080:80"
|
||||
environment:
|
||||
- DRONE_GITEA_SERVER=https://<your-gitea-domain>
|
||||
- DRONE_GITEA_CLIENT_ID=<gitea-oauth-client-id>
|
||||
- DRONE_GITEA_CLIENT_SECRET=<gitea-oauth-client-secret>
|
||||
- DRONE_SERVER_HOST=<your-drone-domain>
|
||||
- DRONE_SERVER_PROTO=https
|
||||
- DRONE_RPC_SECRET=<openssl rand -hex 16 生成>
|
||||
- DRONE_USER_CREATE=username:<gitea用户名>,admin:true
|
||||
volumes:
|
||||
- ./data:/data
|
||||
|
||||
drone-runner:
|
||||
image: drone/drone-runner-docker:1
|
||||
container_name: drone-runner
|
||||
restart: always
|
||||
depends_on:
|
||||
- drone-server
|
||||
environment:
|
||||
- DRONE_RPC_PROTO=http
|
||||
- DRONE_RPC_HOST=drone-server
|
||||
- DRONE_RPC_SECRET=<与 server 相同>
|
||||
- DRONE_RUNNER_CAPACITY=2
|
||||
- DRONE_RUNNER_NAME=drone-runner-1
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
```
|
||||
|
||||
关键注意:
|
||||
- `DRONE_RPC_PROTO=http`:Runner 走 Docker 内网直连,不走 HTTPS
|
||||
- `DRONE_USER_CREATE` 的 username 必须与 Gitea **登录用户名**完全一致(不是邮箱)
|
||||
|
||||
### B. 私有 Registry
|
||||
|
||||
```bash
|
||||
docker run -d --name registry \
|
||||
-p 5000:5000 \
|
||||
-v /opt/registry-data:/var/lib/registry \
|
||||
--restart always \
|
||||
registry:2
|
||||
```
|
||||
|
||||
### C. insecure-registries 配置
|
||||
|
||||
在 Drone CI 服务器和生产服务器的 `/etc/docker/daemon.json` 添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"insecure-registries": ["<registry-host>:5000"]
|
||||
}
|
||||
```
|
||||
|
||||
**不要带 `http://` 前缀**,直接写 `host:port`。修改后 `sudo systemctl restart docker`。
|
||||
|
||||
### D. SSH 免密
|
||||
|
||||
```bash
|
||||
# Drone CI 服务器上生成密钥
|
||||
ssh-keygen -t ed25519 -C "drone-ci-deploy" -f ~/.ssh/drone_deploy -N ""
|
||||
|
||||
# 将公钥添加到生产服务器
|
||||
ssh-copy-id -i ~/.ssh/drone_deploy.pub -p <port> <user>@<production-ip>
|
||||
|
||||
# 验证
|
||||
ssh -i ~/.ssh/drone_deploy -p <port> <user>@<production-ip> "echo ok"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 踩坑清单(生成配置时必须规避)
|
||||
|
||||
| # | 坑 | 正确做法 |
|
||||
|---|-----|---------|
|
||||
| 1 | `insecure-registries` 带 `http://` 前缀 | 直接写 `host:port` |
|
||||
| 2 | Drone `${VAR}` 与 shell 变量冲突 | 直接用 `${DRONE_TAG:-latest}`,不赋中间变量 |
|
||||
| 3 | 用 `secrets:` 字段注入 secret | 用 `environment: { VAR: { from_secret: name } }` |
|
||||
| 4 | `plugins/docker` DinD 启动失败 | 用 `docker:27-cli` + 挂载 Docker socket |
|
||||
| 5 | `DRONE_USER_CREATE` 填邮箱 | 必须填 Gitea 登录用户名 |
|
||||
| 6 | `event + cron` 触发条件互斥 | 只用 `event: [tag, cron]`,不加 `cron:` 过滤 |
|
||||
| 7 | Registry 地址不一致(IP vs 域名) | Drone Secret、`.env`、compose 三处统一 |
|
||||
| 8 | SSH 端口不对 | `appleboy/drone-ssh` 显式指定 `port` |
|
||||
| 9 | Docker 权限不足 | `sudo usermod -aG docker <user>` 后重新登录 |
|
||||
| 10 | `daemon.json` 被覆盖 | 修改前先 cat 查看,合并内容 |
|
||||
|
||||
---
|
||||
|
||||
## 故障排查速查表
|
||||
|
||||
| 现象 | 检查方向 |
|
||||
|------|---------|
|
||||
| Pipeline 不触发 | Gitea Webhook 是否勾选"创建"事件;`.drone.yml` trigger |
|
||||
| Step 一直 pending | Runner 是否连通 Server;仓库是否 Trusted |
|
||||
| 构建报 secret 为空 | `environment: from_secret` 而非 `secrets:` |
|
||||
| Docker push 失败 (HTTPS) | 两台服务器 `insecure-registries` 配置 |
|
||||
| SSH 部署超时 | 密钥是否正确;端口是否匹配;Docker 权限 |
|
||||
| 镜像名 invalid reference | `.env` 的 `DOCKER_REGISTRY` 变量是否正确 |
|
||||
| 数据库迁移失败 | `docker compose logs -f <service>` |
|
||||
| 健康检查超时 | 增大 `MAX_ATTEMPTS`;检查服务启动日志 |
|
||||
@ -1,101 +0,0 @@
|
||||
---
|
||||
name: doc
|
||||
description: 渐进式文档生成器。首次只写精炼梗概(≤300字),后续通过迭代不断完善。
|
||||
---
|
||||
|
||||
# Doc - 渐进式文档生成器
|
||||
|
||||
> **核心理念**:文档是缝缝补补长出来的,不是一步到位写出来的。首次只写最重要的梗概,后续通过讨论和迭代逐步完善。
|
||||
|
||||
当用户调用 `/doc` 或 `/doc <指令>` 时,执行以下步骤:
|
||||
|
||||
## 1. 理解需求
|
||||
|
||||
如果用户提供了参数,使用该描述。否则询问要写什么文档。
|
||||
|
||||
快速确认(已知的不重复问):
|
||||
|
||||
| 项目 | 默认值 |
|
||||
|------|--------|
|
||||
| 文档主题 | 用户指令 |
|
||||
| 输出路径 | 询问用户 |
|
||||
| 作者署名 | 询问用户 |
|
||||
|
||||
简短讨论文档边界:列出你理解的覆盖范围,标注不确定的点,让用户拍板。
|
||||
|
||||
> 你是执笔人,不是决策者。"写什么、不写什么"由用户决定。
|
||||
|
||||
## 2. 快速调研
|
||||
|
||||
聚焦调研,不求面面俱到:
|
||||
- 扫描相关代码和现有文档
|
||||
- 提取核心概念、关键接口、主要数据流
|
||||
- 了解项目现有文档风格(命名、格式)
|
||||
|
||||
调研完成后,用 2-3 句话告诉用户你发现了什么,有什么存疑的点。**不需要正式的大纲确认环节**,直接写梗概,快速拿到反馈比完美大纲更重要。
|
||||
|
||||
## 3. 生成梗概
|
||||
|
||||
### 3.1 写作原则
|
||||
|
||||
**首次写作(默认模式)**:
|
||||
- **言简意赅**,建议控制在 300 字以内
|
||||
- 只写最重要的骨架:是什么、为什么、怎么用
|
||||
- 留白是刻意的,后续迭代会填充细节
|
||||
- 不需要面面俱到,抓住核心价值
|
||||
|
||||
**迭代补充**(用户再次调用 `/doc` 指向同一文件时):
|
||||
- 读取现有内容,在此基础上增量补充
|
||||
- 递增版本号,更新 `updated` 日期
|
||||
- 每次迭代聚焦一个方面,不要一次补太多
|
||||
|
||||
### 3.2 文档头部
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: {文档标题}
|
||||
version: v1.0
|
||||
created: {YYYY-MM-DD}
|
||||
updated: {YYYY-MM-DD}
|
||||
author: {作者署名}
|
||||
---
|
||||
```
|
||||
|
||||
### 3.3 内容要求
|
||||
|
||||
- **准确**:基于实际代码,不编造
|
||||
- **精炼**:一句话能说清的不用两句
|
||||
- **实用**:面向读者,提供可操作的信息
|
||||
- 善用表格、代码块、ASCII 图示(但首次不强求)
|
||||
|
||||
## 4. 保存并输出
|
||||
|
||||
保存到用户指定路径。如果文件已存在,先询问是覆盖还是增量更新。
|
||||
|
||||
输出摘要:
|
||||
|
||||
```text
|
||||
文档: {标题} | 版本: v1.0 | 路径: {path}
|
||||
字数: ~{N}字(首版梗概)
|
||||
|
||||
后续可以通过 /doc 继续补充完善。
|
||||
主人,用不用我沉淀 or git 提交?
|
||||
```
|
||||
|
||||
## 工作流总览
|
||||
|
||||
```text
|
||||
/doc <指令>
|
||||
│
|
||||
├── 1. 理解需求(简短确认主题、路径、署名)
|
||||
├── 2. 快速调研(聚焦核心,不求全面)
|
||||
├── 3. 生成梗概(≤300字,抓骨架)
|
||||
└── 4. 保存输出(鼓励后续迭代)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **少即是多**:首版宁短勿长,300 字是指导建议而非硬限制
|
||||
- **鼓励迭代**:每次 `/doc` 都是一次对话机会,文档在讨论中成长
|
||||
- **不做代码改动**:本 skill 只生成文档
|
||||
- **风格一致**:与项目已有文档风格保持一致
|
||||
@ -1,144 +0,0 @@
|
||||
---
|
||||
name: gitea
|
||||
description: 统一 Gitea 总入口,支持 issue 查询、issue 拆单创建、git push 和 PR 基础操作,优先从当前仓库 origin 自动识别目标仓库。
|
||||
---
|
||||
|
||||
# Gitea - 统一入口
|
||||
|
||||
> **定位**:这是 Gitea 总入口。适合“看 issue”“拆 issue”“把提交 push 上去”“给当前分支开 PR”“在 PR 里留言”这类场景。
|
||||
>
|
||||
> `issue` 和 `issue-drive` 继续保留:`issue` 负责只读查看,`issue-drive` 负责拆单和创建工单;`gitea` 负责统一路由和 push / PR 能力。
|
||||
|
||||
当用户调用 `/gitea`、`$gitea`,或自然语言要求“处理 Gitea / push / PR / issue”时,按以下规则执行。
|
||||
|
||||
## 1. 先路由意图
|
||||
|
||||
先从用户输入里判断目标动作:
|
||||
|
||||
- 包含“看 / 查 / list / open / closed / #123 / issue”,且没有“拆 / 建 / 提 / 创建工单”语义:走 `issue`
|
||||
- 包含“拆 issue / 提 issue / 创建工单 / 沉淀问题”语义:走 `issue-drive`
|
||||
- 包含“push / 推送 / tag 推上去 / 推当前分支”语义:走 push 流程
|
||||
- 包含“PR / pull request / 拉请求 / 合并请求 / 评论 PR”语义:走 PR 流程
|
||||
|
||||
如果一句话里同时包含多个动作,先按用户描述顺序执行;默认不要自作主张串更多步骤。
|
||||
|
||||
## 2. 环境变量
|
||||
|
||||
统一使用:
|
||||
|
||||
- `GITEA_TOKEN`:必需
|
||||
- `GITEA_BASE_URL`:可选;当传 `owner/repo` 或当前仓库 `origin` 是 SSH 地址时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,先提示用户配置后停止,不继续执行 API、push fallback 或 PR 操作。
|
||||
|
||||
## 3. Issue 路由
|
||||
|
||||
如果判定为只读 issue:
|
||||
|
||||
- 直接执行 `issue` skill 的流程
|
||||
- 保持它的输入规则:当前仓库自动识别、支持 `owner/repo`、支持完整仓库 URL、支持单条 issue 编号
|
||||
- 不在 `gitea` 里重复抄写 `issue` 的长说明
|
||||
|
||||
如果判定为 issue 拆单 / 创建:
|
||||
|
||||
- 直接执行 `issue-drive` 的流程
|
||||
- 先整理事实和证据,再创建 issue
|
||||
- 创建时统一使用 `GITEA_TOKEN`
|
||||
|
||||
## 4. Push 流程
|
||||
|
||||
Push 一律先跑预检,再决定是否执行:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/gitea/scripts/push_gitea.py
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 默认推当前分支到同名远端分支
|
||||
- `ahead=0` 时直接告诉用户“没有可推送提交”
|
||||
- `dirty=true` 时明确提示“只会推送已提交内容”
|
||||
- 分支 diverged 时停止,不自动 rebase,也不自动 force
|
||||
- 只有用户明确要求“强推 / force push”时,才带 `--force`
|
||||
|
||||
真正执行时:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py --execute
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/gitea/scripts/push_gitea.py --execute
|
||||
```
|
||||
|
||||
若用户明确要求强推:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py --execute --force
|
||||
```
|
||||
|
||||
执行策略:
|
||||
|
||||
- 先尝试 `git push origin branch:branch`
|
||||
- 若远端鉴权失败,再使用 `GITEA_TOKEN` 调 `/api/v1/user` 获取登录名,构造一次性 HTTPS token URL 进行 fallback
|
||||
- 不改写本地 `origin`
|
||||
|
||||
## 5. PR 流程
|
||||
|
||||
PR 只做三件事:`list`、`create`、`comment`。
|
||||
|
||||
### 5.1 看 PR
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py list --state=open --limit=20
|
||||
```
|
||||
|
||||
### 5.2 开 PR
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py create --base main --title "标题" --body "正文"
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `head` 默认当前分支
|
||||
- `base` 默认远端默认分支;识别不到时再显式传 `--base`
|
||||
- 如果当前分支还没推送到远端,脚本会先走 push 流程,再创建 PR
|
||||
- v1 只支持同仓库 PR,不处理 fork 间 PR
|
||||
|
||||
### 5.3 在 PR 里留言
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py comment 12 --body "已完成回归"
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- PR 评论走 issue comment API,因为 Gitea PR 底层复用 issue 线程
|
||||
|
||||
## 6. 输出要求
|
||||
|
||||
- issue 查询:沿用 `issue` skill 的固定 Markdown 输出
|
||||
- issue 创建:沿用 `issue-drive` 的创建结果输出
|
||||
- push:输出预检 JSON 或执行结果 JSON,至少包含 `status`、`branch`、`ahead/behind`、是否执行、执行方式
|
||||
- PR:输出 PR 列表或单条创建 / 评论结果 JSON,至少包含编号、标题、URL、状态
|
||||
|
||||
## 7. 用法示例
|
||||
|
||||
```bash
|
||||
/gitea 看当前仓库 open issues
|
||||
/gitea 查 issue 17
|
||||
/gitea 把这个 bug 拆成两张 issue
|
||||
/gitea push
|
||||
/gitea 推送当前分支到远端
|
||||
/gitea 给当前分支开 PR 到 main
|
||||
/gitea 看当前仓库 open PR
|
||||
/gitea 在 PR 12 留言:已完成回归
|
||||
```
|
||||
@ -1,334 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared helpers for Gitea skill scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
|
||||
SSH_RE = re.compile(r"^git@(?P<host>[^:]+):(?P<path>.+?)(?:\.git)?/?$")
|
||||
REPO_PATH_RE = re.compile(r"^(?P<owner>[^/]+)/(?P<repo>[^/]+)$")
|
||||
|
||||
|
||||
class GitCommandError(RuntimeError):
|
||||
"""Raised when a git command fails."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepoContext:
|
||||
origin: str
|
||||
owner: str
|
||||
repo: str
|
||||
repo_url: str
|
||||
remote_name: str = "origin"
|
||||
remote_url: str | None = None
|
||||
|
||||
|
||||
def normalize_base_url(base_url: str) -> str:
|
||||
return base_url.rstrip("/")
|
||||
|
||||
|
||||
def ensure_git_repo() -> None:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--is-inside-work-tree"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0 or result.stdout.strip() != "true":
|
||||
raise SystemExit("Current directory is not a git repository.")
|
||||
|
||||
|
||||
def run_git(args: list[str], cwd: str | None = None, check: bool = True) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
detail = result.stderr.strip() or result.stdout.strip() or "unknown git error"
|
||||
raise GitCommandError(f"git {' '.join(args)} failed: {detail}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def parse_http_repo_url(repo_url: str) -> tuple[str, str, str, str]:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
if prefix:
|
||||
origin = f"{origin}/{prefix}"
|
||||
normalized_repo_url = f"{origin}/{owner}/{repo}"
|
||||
return origin, owner, repo, normalized_repo_url
|
||||
|
||||
|
||||
def parse_repo_target(
|
||||
repo_target: str,
|
||||
base_url: str | None = None,
|
||||
) -> tuple[str, str, str, str]:
|
||||
value = repo_target.strip()
|
||||
if not value:
|
||||
raise SystemExit("Repo target cannot be empty.")
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return parse_http_repo_url(value)
|
||||
|
||||
if value.startswith("ssh://"):
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit(
|
||||
"Invalid SSH repo URL. Expected ssh://git@host/owner/repo.git"
|
||||
)
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
host = parsed.hostname
|
||||
if not host:
|
||||
raise SystemExit("Invalid SSH repo URL. Missing host.")
|
||||
origin = (
|
||||
normalize_base_url(base_url)
|
||||
if base_url
|
||||
else f"https://{host}{f'/{prefix}' if prefix else ''}"
|
||||
)
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
ssh_match = SSH_RE.match(value)
|
||||
if ssh_match:
|
||||
path = ssh_match.group("path").rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit(
|
||||
"Invalid SSH repo target. Expected git@host:owner/repo.git"
|
||||
)
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = (
|
||||
normalize_base_url(base_url)
|
||||
if base_url
|
||||
else f"https://{ssh_match.group('host')}{f'/{prefix}' if prefix else ''}"
|
||||
)
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
path_match = REPO_PATH_RE.match(value)
|
||||
if path_match:
|
||||
if not base_url:
|
||||
raise SystemExit(
|
||||
"Repo shorthand owner/repo requires GITEA_BASE_URL or a full repo URL."
|
||||
)
|
||||
origin = normalize_base_url(base_url)
|
||||
owner = path_match.group("owner")
|
||||
repo = path_match.group("repo")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
raise SystemExit(
|
||||
"Invalid repo target. Use https://host/owner/repo, git@host:owner/repo.git, "
|
||||
"ssh://git@host/owner/repo.git, or owner/repo with GITEA_BASE_URL."
|
||||
)
|
||||
|
||||
|
||||
def get_remote_url(remote: str = "origin") -> str:
|
||||
ensure_git_repo()
|
||||
try:
|
||||
return run_git(["remote", "get-url", remote])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(f"Failed to read git remote '{remote}': {exc}") from exc
|
||||
|
||||
|
||||
def resolve_repo(
|
||||
repo_url: str | None = None,
|
||||
repo: str | None = None,
|
||||
remote: str = "origin",
|
||||
) -> RepoContext:
|
||||
base_url = os.getenv("GITEA_BASE_URL")
|
||||
remote_url = None
|
||||
target = repo_url or repo
|
||||
if not target:
|
||||
remote_url = get_remote_url(remote)
|
||||
target = remote_url
|
||||
origin, owner, repo_name, normalized_repo_url = parse_repo_target(
|
||||
target,
|
||||
base_url=base_url,
|
||||
)
|
||||
return RepoContext(
|
||||
origin=origin,
|
||||
owner=owner,
|
||||
repo=repo_name,
|
||||
repo_url=normalized_repo_url,
|
||||
remote_name=remote,
|
||||
remote_url=remote_url,
|
||||
)
|
||||
|
||||
|
||||
def api_base(context: RepoContext) -> str:
|
||||
return f"{context.origin}/api/v1/repos/{context.owner}/{context.repo}"
|
||||
|
||||
|
||||
def load_token(required: bool = True) -> str:
|
||||
token = os.getenv("GITEA_TOKEN", "").strip()
|
||||
if not token and required:
|
||||
raise SystemExit("Missing GITEA_TOKEN. Export it before using Gitea workflows.")
|
||||
return token
|
||||
|
||||
|
||||
def request_json(
|
||||
url: str,
|
||||
token: str,
|
||||
method: str = "GET",
|
||||
payload: dict | None = None,
|
||||
) -> dict | list:
|
||||
data = None
|
||||
headers = {"Accept": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace")
|
||||
raise SystemExit(
|
||||
f"Gitea API request failed: {exc.code} {exc.reason} | {body}"
|
||||
) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise SystemExit(f"Failed to reach Gitea API: {exc.reason}") from exc
|
||||
|
||||
|
||||
def get_current_user_login(origin: str, token: str) -> str:
|
||||
data = request_json(f"{origin}/api/v1/user", token)
|
||||
if not isinstance(data, dict):
|
||||
raise SystemExit("Unexpected user API response.")
|
||||
login = str(data.get("login") or "").strip()
|
||||
if not login:
|
||||
raise SystemExit("Failed to resolve current Gitea user login.")
|
||||
return login
|
||||
|
||||
|
||||
def current_branch() -> str:
|
||||
ensure_git_repo()
|
||||
try:
|
||||
branch = run_git(["symbolic-ref", "--quiet", "--short", "HEAD"])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(
|
||||
"Detached HEAD. Checkout a branch before running push or PR actions."
|
||||
) from exc
|
||||
if not branch:
|
||||
raise SystemExit("Failed to determine current git branch.")
|
||||
return branch
|
||||
|
||||
|
||||
def get_upstream(branch: str) -> str | None:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"rev-parse",
|
||||
"--abbrev-ref",
|
||||
"--symbolic-full-name",
|
||||
f"{branch}@{{upstream}}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
upstream = result.stdout.strip()
|
||||
return upstream or None
|
||||
|
||||
|
||||
def remote_branch_sha(remote: str, branch: str) -> str | None:
|
||||
try:
|
||||
output = run_git(["ls-remote", "--heads", remote, branch])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(f"Failed to query remote branch '{remote}/{branch}': {exc}") from exc
|
||||
if not output:
|
||||
return None
|
||||
return output.split()[0]
|
||||
|
||||
|
||||
def ahead_behind(compare_ref: str) -> tuple[int, int]:
|
||||
output = run_git(["rev-list", "--left-right", "--count", f"{compare_ref}...HEAD"])
|
||||
parts = output.split()
|
||||
if len(parts) != 2:
|
||||
raise SystemExit(f"Unexpected rev-list output: {output}")
|
||||
behind, ahead = (int(part) for part in parts)
|
||||
return behind, ahead
|
||||
|
||||
|
||||
def worktree_changes() -> list[str]:
|
||||
output = run_git(["status", "--short"])
|
||||
return [line for line in output.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def remote_default_branch(remote: str = "origin") -> str:
|
||||
try:
|
||||
output = run_git(["symbolic-ref", "--quiet", "--short", f"refs/remotes/{remote}/HEAD"])
|
||||
if output.startswith(f"{remote}/"):
|
||||
return output.split("/", 1)[1]
|
||||
except GitCommandError:
|
||||
pass
|
||||
|
||||
for candidate in ("main", "master"):
|
||||
if remote_branch_sha(remote, candidate):
|
||||
return candidate
|
||||
raise SystemExit(
|
||||
f"Failed to infer default branch for remote '{remote}'. Pass --base explicitly."
|
||||
)
|
||||
|
||||
|
||||
def build_authenticated_push_url(repo_url: str, username: str, token: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
path = parsed.path.rstrip("/")
|
||||
if not path.endswith(".git"):
|
||||
path = f"{path}.git"
|
||||
netloc = (
|
||||
f"{urllib.parse.quote(username, safe='')}:"
|
||||
f"{urllib.parse.quote(token, safe='')}@{parsed.netloc}"
|
||||
)
|
||||
return urllib.parse.urlunsplit((parsed.scheme or "https", netloc, path, "", ""))
|
||||
|
||||
|
||||
def mask_url(url: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
if "@" not in parsed.netloc:
|
||||
return url
|
||||
_, host = parsed.netloc.rsplit("@", 1)
|
||||
return urllib.parse.urlunsplit((parsed.scheme, f"***@{host}", parsed.path, "", ""))
|
||||
|
||||
|
||||
def is_auth_error(stderr: str) -> bool:
|
||||
message = stderr.lower()
|
||||
indicators = (
|
||||
"authentication failed",
|
||||
"permission denied",
|
||||
"could not read username",
|
||||
"could not read password",
|
||||
"http basic: access denied",
|
||||
"access denied",
|
||||
"unauthorized",
|
||||
)
|
||||
return any(indicator in message for indicator in indicators)
|
||||
@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List, create, and comment on Gitea pull requests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
from common import (
|
||||
api_base,
|
||||
current_branch,
|
||||
load_token,
|
||||
remote_default_branch,
|
||||
request_json,
|
||||
resolve_repo,
|
||||
)
|
||||
from push_gitea import compute_push_plan, execute_push
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="List, create, and comment on Gitea pull requests."
|
||||
)
|
||||
parser.add_argument("--repo-url", help="Explicit target repo URL.")
|
||||
parser.add_argument("--repo", help="Shorthand owner/repo. Requires GITEA_BASE_URL.")
|
||||
parser.add_argument("--remote", default="origin", help="Git remote name. Default: origin")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
list_parser = subparsers.add_parser("list", help="List pull requests.")
|
||||
list_parser.add_argument(
|
||||
"--state",
|
||||
default="open",
|
||||
choices=("open", "closed", "all"),
|
||||
help="Pull request state filter.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--limit",
|
||||
default=20,
|
||||
type=int,
|
||||
help="Maximum number of pull requests to return.",
|
||||
)
|
||||
|
||||
create_parser = subparsers.add_parser("create", help="Create a pull request.")
|
||||
create_parser.add_argument("--base", help="Base branch. Default: remote default branch.")
|
||||
create_parser.add_argument("--head", help="Head branch. Default: current branch.")
|
||||
create_parser.add_argument("--title", required=True, help="Pull request title.")
|
||||
create_parser.add_argument("--body", default="", help="Pull request body.")
|
||||
|
||||
comment_parser = subparsers.add_parser("comment", help="Comment on a pull request.")
|
||||
comment_parser.add_argument("pr", type=int, help="Pull request number.")
|
||||
comment_parser.add_argument("--body", required=True, help="Comment body.")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def summarize_pull_request(item: dict) -> dict:
|
||||
user = item.get("user") or {}
|
||||
head = item.get("head") or {}
|
||||
base = item.get("base") or {}
|
||||
return {
|
||||
"number": item.get("number"),
|
||||
"title": item.get("title"),
|
||||
"state": item.get("state"),
|
||||
"html_url": item.get("html_url"),
|
||||
"author": user.get("full_name") or user.get("login"),
|
||||
"head": head.get("ref"),
|
||||
"base": base.get("ref"),
|
||||
"created_at": item.get("created_at"),
|
||||
"updated_at": item.get("updated_at"),
|
||||
}
|
||||
|
||||
|
||||
def list_pull_requests(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
url = f"{api_base(context)}/pulls?state={args.state}&page=1&limit={args.limit}"
|
||||
data = request_json(url, token)
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit("Unexpected pull request list response.")
|
||||
return {
|
||||
"action": "list",
|
||||
"repo_url": context.repo_url,
|
||||
"state": args.state,
|
||||
"limit": args.limit,
|
||||
"count": len(data),
|
||||
"pull_requests": [summarize_pull_request(item) for item in data],
|
||||
}
|
||||
|
||||
|
||||
def create_pull_request(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
head = args.head or current_branch()
|
||||
base = args.base or remote_default_branch(args.remote)
|
||||
|
||||
push_plan = compute_push_plan(
|
||||
repo_url=args.repo_url,
|
||||
repo=args.repo,
|
||||
remote=args.remote,
|
||||
branch=head,
|
||||
force=False,
|
||||
)
|
||||
push_result = None
|
||||
if push_plan["needs_push"]:
|
||||
push_result = execute_push(push_plan, force=False)
|
||||
elif push_plan["status"] == "blocked":
|
||||
raise SystemExit(
|
||||
"Cannot create PR because the current branch has diverged from the remote branch."
|
||||
)
|
||||
|
||||
payload = {"base": base, "head": head, "title": args.title, "body": args.body}
|
||||
created = request_json(f"{api_base(context)}/pulls", token, method="POST", payload=payload)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected pull request creation response.")
|
||||
return {
|
||||
"action": "create",
|
||||
"repo_url": context.repo_url,
|
||||
"base": base,
|
||||
"head": head,
|
||||
"push": push_result,
|
||||
"pull_request": summarize_pull_request(created),
|
||||
}
|
||||
|
||||
|
||||
def comment_pull_request(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
payload = {"body": args.body}
|
||||
created = request_json(
|
||||
f"{api_base(context)}/issues/{args.pr}/comments",
|
||||
token,
|
||||
method="POST",
|
||||
payload=payload,
|
||||
)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected PR comment response.")
|
||||
user = created.get("user") or {}
|
||||
return {
|
||||
"action": "comment",
|
||||
"repo_url": context.repo_url,
|
||||
"pull_request": args.pr,
|
||||
"comment": {
|
||||
"id": created.get("id"),
|
||||
"html_url": created.get("html_url"),
|
||||
"author": user.get("full_name") or user.get("login"),
|
||||
"created_at": created.get("created_at"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.command == "list":
|
||||
payload = list_pull_requests(args)
|
||||
elif args.command == "create":
|
||||
payload = create_pull_request(args)
|
||||
else:
|
||||
payload = comment_pull_request(args)
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,236 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preflight and execute git push with optional Gitea token fallback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from common import (
|
||||
build_authenticated_push_url,
|
||||
current_branch,
|
||||
ensure_git_repo,
|
||||
get_current_user_login,
|
||||
get_upstream,
|
||||
is_auth_error,
|
||||
load_token,
|
||||
mask_url,
|
||||
remote_branch_sha,
|
||||
resolve_repo,
|
||||
worktree_changes,
|
||||
ahead_behind,
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Preflight and optionally push the current branch to Gitea."
|
||||
)
|
||||
parser.add_argument("--repo-url", help="Explicit target repo URL.")
|
||||
parser.add_argument("--repo", help="Shorthand owner/repo. Requires GITEA_BASE_URL.")
|
||||
parser.add_argument("--remote", default="origin", help="Git remote name. Default: origin")
|
||||
parser.add_argument("--branch", help="Branch to push. Default: current branch")
|
||||
parser.add_argument(
|
||||
"--execute",
|
||||
action="store_true",
|
||||
help="Run git push after preflight instead of only printing the plan.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Allow force push. Uses --force-with-lease under the hood.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def build_push_command(target: str, branch: str, force: bool) -> list[str]:
|
||||
command = ["git", "push"]
|
||||
if force:
|
||||
command.append("--force-with-lease")
|
||||
command.extend([target, f"{branch}:{branch}"])
|
||||
return command
|
||||
|
||||
|
||||
def command_text(command: list[str]) -> str:
|
||||
return " ".join(shlex.quote(part) for part in command)
|
||||
|
||||
|
||||
def compute_push_plan(
|
||||
repo_url: str | None = None,
|
||||
repo: str | None = None,
|
||||
remote: str = "origin",
|
||||
branch: str | None = None,
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
ensure_git_repo()
|
||||
context = resolve_repo(repo_url=repo_url, repo=repo, remote=remote)
|
||||
branch_name = branch or current_branch()
|
||||
dirty_lines = worktree_changes()
|
||||
upstream = get_upstream(branch_name)
|
||||
compare_ref = upstream
|
||||
remote_exists = True
|
||||
ahead = 0
|
||||
behind = 0
|
||||
|
||||
if upstream:
|
||||
behind, ahead = ahead_behind(upstream)
|
||||
else:
|
||||
remote_sha = remote_branch_sha(remote, branch_name)
|
||||
if remote_sha:
|
||||
compare_ref = f"{remote}/{branch_name}"
|
||||
behind, ahead = ahead_behind(remote_sha)
|
||||
else:
|
||||
remote_exists = False
|
||||
compare_ref = None
|
||||
ahead = None
|
||||
behind = 0
|
||||
|
||||
diverged = bool(remote_exists and behind > 0 and (ahead or 0) > 0)
|
||||
behind_only = bool(remote_exists and behind > 0 and (ahead or 0) == 0)
|
||||
needs_push = (not remote_exists) or ((ahead or 0) > 0)
|
||||
|
||||
status = "ready"
|
||||
if diverged and not force:
|
||||
status = "blocked"
|
||||
elif not needs_push:
|
||||
status = "noop"
|
||||
|
||||
messages: list[str] = []
|
||||
if dirty_lines:
|
||||
messages.append("Working tree is dirty; push will include only committed changes.")
|
||||
if not remote_exists:
|
||||
messages.append("Remote branch does not exist yet; push will create it.")
|
||||
if behind_only:
|
||||
messages.append("Local branch is behind the remote branch; there are no local commits to push.")
|
||||
if diverged and not force:
|
||||
messages.append("Branch has diverged from remote; rerun with explicit force only if that is intended.")
|
||||
|
||||
push_command = build_push_command(remote, branch_name, force)
|
||||
fallback_command = None
|
||||
token_available = bool(load_token(required=False))
|
||||
if token_available:
|
||||
try:
|
||||
token = load_token(required=False)
|
||||
login = get_current_user_login(context.origin, token)
|
||||
auth_url = build_authenticated_push_url(context.repo_url, login, token)
|
||||
fallback_command = build_push_command(auth_url, branch_name, force)
|
||||
except SystemExit as exc:
|
||||
messages.append(f"Failed to prepare HTTPS token fallback: {exc}")
|
||||
|
||||
return {
|
||||
"repo_url": context.repo_url,
|
||||
"origin": context.origin,
|
||||
"owner": context.owner,
|
||||
"repo": context.repo,
|
||||
"remote": remote,
|
||||
"branch": branch_name,
|
||||
"upstream": upstream,
|
||||
"compare_ref": compare_ref,
|
||||
"remote_branch_exists": remote_exists,
|
||||
"dirty": bool(dirty_lines),
|
||||
"dirty_paths": dirty_lines,
|
||||
"ahead": ahead,
|
||||
"behind": behind,
|
||||
"diverged": diverged,
|
||||
"behind_only": behind_only,
|
||||
"needs_push": needs_push,
|
||||
"status": status,
|
||||
"messages": messages,
|
||||
"push_command": command_text(push_command),
|
||||
"fallback_push_command": command_text(
|
||||
[
|
||||
*(fallback_command[:-2] if fallback_command else []),
|
||||
mask_url(fallback_command[-2]) if fallback_command else "",
|
||||
*(fallback_command[-1:] if fallback_command else []),
|
||||
]
|
||||
)
|
||||
if fallback_command
|
||||
else None,
|
||||
}
|
||||
|
||||
|
||||
def run_command(command: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(command, capture_output=True, text=True)
|
||||
|
||||
|
||||
def execute_push(plan: dict, force: bool = False) -> dict:
|
||||
if plan["status"] == "noop":
|
||||
return {
|
||||
"status": "noop",
|
||||
"executed": False,
|
||||
"reason": "nothing_to_push",
|
||||
"messages": plan["messages"],
|
||||
}
|
||||
if plan["diverged"] and not force:
|
||||
raise SystemExit(
|
||||
"Branch has diverged from remote. Refusing to push without explicit --force."
|
||||
)
|
||||
|
||||
primary_command = build_push_command(plan["remote"], plan["branch"], force)
|
||||
primary_result = run_command(primary_command)
|
||||
if primary_result.returncode == 0:
|
||||
return {
|
||||
"status": "pushed",
|
||||
"executed": True,
|
||||
"method": "remote",
|
||||
"command": command_text(primary_command),
|
||||
"stdout": primary_result.stdout.strip(),
|
||||
"stderr": primary_result.stderr.strip(),
|
||||
}
|
||||
|
||||
primary_stderr = primary_result.stderr.strip() or primary_result.stdout.strip()
|
||||
if not is_auth_error(primary_stderr):
|
||||
raise SystemExit(f"git push failed: {primary_stderr}")
|
||||
|
||||
token = load_token(required=True)
|
||||
login = get_current_user_login(plan["origin"], token)
|
||||
auth_url = build_authenticated_push_url(plan["repo_url"], login, token)
|
||||
fallback_command = build_push_command(auth_url, plan["branch"], force)
|
||||
fallback_result = run_command(fallback_command)
|
||||
if fallback_result.returncode == 0:
|
||||
return {
|
||||
"status": "pushed",
|
||||
"executed": True,
|
||||
"method": "https-token",
|
||||
"command": command_text(
|
||||
[
|
||||
*(fallback_command[:-2]),
|
||||
mask_url(fallback_command[-2]),
|
||||
fallback_command[-1],
|
||||
]
|
||||
),
|
||||
"stdout": fallback_result.stdout.strip(),
|
||||
"stderr": fallback_result.stderr.strip(),
|
||||
}
|
||||
|
||||
fallback_stderr = fallback_result.stderr.strip() or fallback_result.stdout.strip()
|
||||
raise SystemExit(
|
||||
"git push failed with remote auth and HTTPS token fallback: "
|
||||
f"{fallback_stderr}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
plan = compute_push_plan(
|
||||
repo_url=args.repo_url,
|
||||
repo=args.repo,
|
||||
remote=args.remote,
|
||||
branch=args.branch,
|
||||
force=args.force,
|
||||
)
|
||||
if not args.execute:
|
||||
print(json.dumps(plan, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
result = execute_push(plan, force=args.force)
|
||||
payload = {"plan": plan, "result": result}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -106,23 +106,19 @@ description: 终极执行按钮,激进模式一口气完成开发任务,兼
|
||||
│ │
|
||||
│ 1. 以 tasks.md 为圣经,严格按顺序执行 │
|
||||
│ │
|
||||
│ 2. 先判断当前约束下的最优路径,再沿该路径推进 │
|
||||
│ 2. 不要停下来问用户,自主决策 │
|
||||
│ │
|
||||
│ 3. 不先为自我防御铺退路,必要时才展开 fallback │
|
||||
│ 3. 遇到问题自主修复,修复失败则记录并继续 │
|
||||
│ │
|
||||
│ 4. 遇到问题先沿最优路径自主修复,修复失败再记录 │
|
||||
│ 4. 发现文档冲突,基于架构经验选最优解,注释说明 │
|
||||
│ │
|
||||
│ 5. 发现文档冲突,基于最优路径原则选解,注释说明 │
|
||||
│ 5. 利用所有可用工具:搜索、MCP、Skills │
|
||||
│ │
|
||||
│ 6. 利用所有可用工具:搜索、MCP、Skills │
|
||||
│ │
|
||||
│ 7. 每完成一个模块,Git 提交一次 │
|
||||
│ 6. 每完成一个模块,Git 提交一次 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
说明:激进模式默认遵循“最优路径优先”。只有在最优路径被事实阻塞、风险或成本发生实质变化、或用户明确要求比较方案时,才展开 fallback 与备选方案。
|
||||
|
||||
### 3.2 任务执行流程
|
||||
|
||||
```
|
||||
@ -130,23 +126,20 @@ description: 终极执行按钮,激进模式一口气完成开发任务,兼
|
||||
│
|
||||
├── 1. 读取任务详情(描述、验收标准、依赖)
|
||||
│
|
||||
├── 2. 判断当前约束下的最优路径(目标、约束、验收、依赖)
|
||||
│
|
||||
├── 3. 检查依赖任务是否完成
|
||||
├── 2. 检查依赖任务是否完成
|
||||
│ └── 未完成 → 先执行依赖任务
|
||||
│
|
||||
├── 4. 执行任务
|
||||
├── 3. 执行任务
|
||||
│ ├── 根据任务类型选择执行方式
|
||||
│ ├── 编写代码 / 配置 / 测试
|
||||
│ └── 验证验收标准
|
||||
│
|
||||
├── 5. 遇到问题?
|
||||
│ ├── 先沿最优路径尝试自主修复(最多 3 次)
|
||||
│ ├── 最优路径被阻塞/风险成本变化/用户明确要求比较 → 再评估 fallback
|
||||
├── 4. 遇到问题?
|
||||
│ ├── 尝试自主修复(最多 3 次)
|
||||
│ ├── 修复成功 → 继续
|
||||
│ └── 修复失败 → 记录问题,跳过,继续下一个
|
||||
│
|
||||
└── 6. 标记任务完成,更新 tasks.md
|
||||
└── 5. 标记任务完成,更新 tasks.md
|
||||
```
|
||||
|
||||
### 3.3 自主修复策略
|
||||
@ -157,7 +150,7 @@ description: 终极执行按钮,激进模式一口气完成开发任务,兼
|
||||
| 类型错误 | 检查类型定义,修复类型 |
|
||||
| 依赖缺失 | 安装依赖包 |
|
||||
| 测试失败 | 修复功能代码使测试通过 |
|
||||
| 文档冲突 | 基于最优路径原则选解,并在注释中说明 |
|
||||
| 文档冲突 | 基于架构经验选最优解 |
|
||||
| 未知错误 | 搜索解决方案,尝试修复 |
|
||||
|
||||
## 4. Git 提交规则
|
||||
|
||||
@ -1,172 +0,0 @@
|
||||
---
|
||||
name: issue-drive
|
||||
description: 归集证据并把问题拆成 1 到多张 Gitea issue,支持从当前仓库 origin 自动识别仓库或用户显式指定,并通过环境变量配置的 Gitea API 批量创建工单。
|
||||
---
|
||||
|
||||
# Issue Drive - 通用 Gitea 问题拆单与创建
|
||||
|
||||
> **定位**:不是“查看 issue”,而是把一个真实问题沉淀成可执行的 Gitea issue。适合“线上出 bug 了”“要把一个需求拆成工单”“需要补齐工程治理任务”这类场景。
|
||||
>
|
||||
> 如果用户只是想查 issue 列表或详情,用 `issue`。如果用户想通过统一入口完成 issue / push / PR 组合操作,用 `gitea`。
|
||||
|
||||
当用户调用 `/issue-drive`、`$issue-drive`,或自然语言要求“拆 issue”“提 issue”“把问题沉淀成 Gitea 工单”时,执行以下步骤。
|
||||
|
||||
## 1. 先确定目标仓库
|
||||
|
||||
目标仓库按以下优先级确定:
|
||||
|
||||
1. 用户显式给出的完整仓库 URL:`https://git.example.com/owner/repo`
|
||||
2. 用户显式给出的仓库简写:`owner/repo`,此时需要 `GITEA_BASE_URL`
|
||||
3. 当前项目的 `git remote get-url origin`
|
||||
|
||||
解析规则:
|
||||
|
||||
- 支持 `https://host[/prefix]/owner/repo`
|
||||
- 支持 `git@host:owner/repo.git`
|
||||
- 支持 `git@host:prefix/owner/repo.git`
|
||||
- 支持 `ssh://git@host/owner/repo.git`
|
||||
- 当前仓库 `origin` 是 SSH 地址时:
|
||||
- 优先使用 `GITEA_BASE_URL`
|
||||
- 否则退回 `https://host`
|
||||
|
||||
如果参数和当前仓库都无法确定目标仓库,停止并提示:
|
||||
|
||||
```bash
|
||||
❌ 无法确定目标仓库
|
||||
|
||||
请显式指定仓库,例如:
|
||||
/issue-drive https://git.example.com/owner/repo
|
||||
|
||||
或配置:
|
||||
export GITEA_BASE_URL=https://git.example.com
|
||||
```
|
||||
|
||||
## 2. 检查环境变量
|
||||
|
||||
先读取环境变量:
|
||||
|
||||
- `GITEA_TOKEN`:必需,创建 issue 时使用
|
||||
- `GITEA_BASE_URL`:可选;仓库简写或 SSH origin 时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,输出:
|
||||
|
||||
```bash
|
||||
❌ 缺少 GITEA_TOKEN
|
||||
|
||||
请先在当前 shell 或 .env 中配置:
|
||||
export GITEA_TOKEN=your_gitea_token
|
||||
```
|
||||
|
||||
然后停止,不继续创建 issue。
|
||||
|
||||
## 3. 先读仓库基线和证据
|
||||
|
||||
不要先写 issue。先确认事实、范围、影响和证据入口。
|
||||
|
||||
优先读取以下文件;只读存在的那些:
|
||||
|
||||
1. `docs/ISSUE_WORKFLOW.md`
|
||||
2. `.gitea/ISSUE_TEMPLATE/`
|
||||
3. `.github/ISSUE_TEMPLATE/`
|
||||
4. `README.md`
|
||||
5. `doc/`, `docs/` 下与当前问题直接相关的说明、审计、回归、报表文档
|
||||
|
||||
如果仓库里没有现成 issue 基建,就直接基于用户描述、代码、日志、测试结果和已有文档整理事实,不要因为“模板不完整”而停住。
|
||||
|
||||
## 4. 判断该用哪种 issue
|
||||
|
||||
按下面规则分类:
|
||||
|
||||
- **缺陷 / 异常报告**:已有事实证据,目标是排查、修复、回归
|
||||
- **业务需求 / 功能请求**:目标是交付用户价值,需要产品化描述
|
||||
- **工程任务 / 重构 / 基建**:目标是稳定性、可观测性、测试、流程、治理
|
||||
|
||||
标签策略:
|
||||
|
||||
- 优先复用仓库远端已有标签
|
||||
- 如果远端没有对应标签,不要中断;用标题前缀和正文结构表达类型
|
||||
- 不要为了建 issue 先去重构整套标签体系
|
||||
|
||||
## 5. 拆单规则
|
||||
|
||||
不要默认一张大工单。优先按“可独立修复、可独立验证、可独立关闭”拆。
|
||||
|
||||
优先拆开的情况:
|
||||
|
||||
- 现象修复 和 口径 / 文案 / 导出字段修复 是两件事
|
||||
- 业务缺陷 和 工程可观测性补齐 是两件事
|
||||
- 一个问题需要不同 owner、不同验证方式或不同发布时间
|
||||
|
||||
## 6. 写 issue 前的最小检查
|
||||
|
||||
创建 issue 前必须确认:
|
||||
|
||||
- 标题是否直接表达现象或交付物
|
||||
- 正文是否写明:现象、期望、影响范围、证据入口、初步判断、完成标准
|
||||
- 事实数字是否已经从本地数据、日志、代码或用户描述中复核
|
||||
- 引用的仓库内链接是否已经存在于默认分支
|
||||
|
||||
如果本次要新建或更新以下文件:
|
||||
|
||||
- `.gitea/ISSUE_TEMPLATE/*`
|
||||
- `.github/ISSUE_TEMPLATE/*`
|
||||
- `.gitea/PULL_REQUEST_TEMPLATE.md`
|
||||
- `docs/ISSUE_WORKFLOW.md`
|
||||
- issue 正文会引用的证据文档
|
||||
|
||||
先做最小提交并推送到默认分支,再创建 issue。只提交与 issue 基建或证据直接相关的文件,不要顺手带上无关改动。
|
||||
|
||||
## 7. 用脚本批量创建 issue
|
||||
|
||||
优先使用本 skill 附带脚本,并按当前平台选择路径:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json --dry-run
|
||||
python3 .codex/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json --dry-run
|
||||
python3 .claude/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json
|
||||
```
|
||||
|
||||
JSON 结构固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"repo_url": "https://git.example.com/owner/repo",
|
||||
"issues": [
|
||||
{
|
||||
"title": "[缺陷][登录] 示例标题",
|
||||
"body": "Issue 正文",
|
||||
"labels": ["P1", "bug"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `repo_url` 可省略;脚本会按 `--repo-url` > `spec.repo_url` > `spec.repo` > 当前仓库 `origin` 的顺序解析
|
||||
- `spec.repo` 允许写 `owner/repo`,但需要 `GITEA_BASE_URL`
|
||||
- Token 固定读取 `GITEA_TOKEN`
|
||||
- `labels` 写标签名,脚本会自动解析成远端 label id
|
||||
- 远端不存在的标签会告警并自动跳过,不中断整个批次
|
||||
- 先跑 `--dry-run`,确认 payload 和目标仓库无误后再正式创建
|
||||
|
||||
## 8. 输出结果
|
||||
|
||||
创建成功后,输出:
|
||||
|
||||
- 已推送的提交信息(如果本次为了 issue 基建或证据先推了提交)
|
||||
- 新建 issue 的编号、标题、URL
|
||||
- 哪些标签成功命中,哪些因远端不存在被跳过
|
||||
- 如果拆成多张 issue,要说明每张工单分别驱动什么工作
|
||||
|
||||
## 9. 行为边界
|
||||
|
||||
- 默认以中文写 issue;用户明确要求英文时再切换
|
||||
- 不把一个大问题硬塞进一张工单
|
||||
- 不在 issue 里写超长散文,优先写清单、事实和完成标准
|
||||
- 不因为缺少完美自动化复现就拒绝提单;现网证据或稳定复现路径成立时,可以先提
|
||||
- 创建 issue 后,不自动继续写修复代码,除非用户明确要求
|
||||
@ -1,268 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create one or more Gitea issues from a JSON spec."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
SSH_RE = re.compile(r"^git@(?P<host>[^:]+):(?P<path>.+?)(?:\.git)?/?$")
|
||||
REPO_PATH_RE = re.compile(r"^(?P<owner>[^/]+)/(?P<repo>[^/]+)$")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Batch-create Gitea issues from a JSON spec."
|
||||
)
|
||||
parser.add_argument("spec", help="Path to JSON spec file")
|
||||
parser.add_argument(
|
||||
"--repo-url",
|
||||
help=(
|
||||
"Override target repo. Accepts full https URL, SSH git origin, "
|
||||
"or owner/repo with GITEA_BASE_URL."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repo",
|
||||
help="Shorthand owner/repo. Requires GITEA_BASE_URL.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Validate and print the payload without creating issues",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_token(required: bool = True) -> str:
|
||||
token = os.getenv("GITEA_TOKEN")
|
||||
if not token and required:
|
||||
raise SystemExit(
|
||||
"Missing GITEA_TOKEN. Export the token before creating issues."
|
||||
)
|
||||
return token or ""
|
||||
|
||||
|
||||
def load_spec(path: str) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
if not isinstance(data, dict) or not isinstance(data.get("issues"), list):
|
||||
raise SystemExit("Spec must be an object with an 'issues' array.")
|
||||
if not data["issues"]:
|
||||
raise SystemExit("Spec contains no issues.")
|
||||
return data
|
||||
|
||||
|
||||
def normalize_base_url(base_url: str) -> str:
|
||||
return base_url.rstrip("/")
|
||||
|
||||
|
||||
def parse_http_repo_url(repo_url: str) -> tuple[str, str, str]:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
if prefix:
|
||||
origin = f"{origin}/{prefix}"
|
||||
return origin, owner, repo
|
||||
|
||||
|
||||
def parse_repo_target(repo_target: str, base_url: str | None = None) -> tuple[str, str, str, str]:
|
||||
value = repo_target.strip()
|
||||
if not value:
|
||||
raise SystemExit("Repo target cannot be empty.")
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
origin, owner, repo = parse_http_repo_url(value)
|
||||
repo_url = f"{origin}/{owner}/{repo}"
|
||||
return origin, owner, repo, repo_url
|
||||
|
||||
if value.startswith("ssh://"):
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid SSH repo URL. Expected ssh://git@host/owner/repo.git")
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
host = parsed.hostname
|
||||
if not host:
|
||||
raise SystemExit("Invalid SSH repo URL. Missing host.")
|
||||
origin = normalize_base_url(base_url or f"https://{host}")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
ssh_match = SSH_RE.match(value)
|
||||
if ssh_match:
|
||||
path = ssh_match.group("path").rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid SSH repo target. Expected git@host:owner/repo.git")
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
origin = normalize_base_url(base_url or f"https://{ssh_match.group('host')}")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
path_match = REPO_PATH_RE.match(value)
|
||||
if path_match:
|
||||
if not base_url:
|
||||
raise SystemExit(
|
||||
"Repo shorthand owner/repo requires GITEA_BASE_URL or --repo-url."
|
||||
)
|
||||
origin = normalize_base_url(base_url)
|
||||
owner = path_match.group("owner")
|
||||
repo = path_match.group("repo")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
raise SystemExit(
|
||||
"Invalid repo target. Use https://host/owner/repo, git@host:owner/repo.git, "
|
||||
"ssh://git@host/owner/repo.git, or owner/repo with GITEA_BASE_URL."
|
||||
)
|
||||
|
||||
|
||||
def get_origin_repo_target() -> str:
|
||||
try:
|
||||
return subprocess.check_output(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
text=True,
|
||||
stderr=subprocess.STDOUT,
|
||||
).strip()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise SystemExit(f"Failed to read git origin: {exc.output.strip()}") from exc
|
||||
|
||||
|
||||
def resolve_repo(spec: dict, args: argparse.Namespace) -> tuple[str, str, str, str]:
|
||||
base_url = os.getenv("GITEA_BASE_URL")
|
||||
repo_target = (
|
||||
args.repo_url
|
||||
or args.repo
|
||||
or spec.get("repo_url")
|
||||
or spec.get("repo")
|
||||
or get_origin_repo_target()
|
||||
)
|
||||
return parse_repo_target(repo_target, base_url=base_url)
|
||||
|
||||
|
||||
def request_json(
|
||||
url: str,
|
||||
token: str,
|
||||
method: str = "GET",
|
||||
payload: dict | None = None,
|
||||
) -> dict | list:
|
||||
data = None
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace")
|
||||
raise SystemExit(
|
||||
f"Gitea API request failed: {exc.code} {exc.reason} | {body}"
|
||||
) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise SystemExit(f"Failed to reach Gitea API: {exc.reason}") from exc
|
||||
|
||||
|
||||
def fetch_label_map(api_base: str, token: str) -> dict[str, int]:
|
||||
data = request_json(f"{api_base}/labels?page=1&limit=100", token)
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit("Unexpected labels API response.")
|
||||
return {
|
||||
item["name"]: item["id"]
|
||||
for item in data
|
||||
if "name" in item and "id" in item
|
||||
}
|
||||
|
||||
|
||||
def resolve_label_ids(label_names: list[str], label_map: dict[str, int]) -> tuple[list[int], list[str]]:
|
||||
ids: list[int] = []
|
||||
missing: list[str] = []
|
||||
for name in label_names:
|
||||
if name in label_map:
|
||||
ids.append(label_map[name])
|
||||
else:
|
||||
missing.append(name)
|
||||
return ids, missing
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
token = load_token(required=not args.dry_run)
|
||||
spec = load_spec(args.spec)
|
||||
origin, owner, repo, repo_url = resolve_repo(spec, args)
|
||||
api_base = f"{origin}/api/v1/repos/{owner}/{repo}"
|
||||
label_map = fetch_label_map(api_base, token) if token else {}
|
||||
|
||||
issues = spec["issues"]
|
||||
for issue in issues:
|
||||
if not isinstance(issue, dict):
|
||||
raise SystemExit("Each issue entry must be an object.")
|
||||
if not issue.get("title") or not issue.get("body"):
|
||||
raise SystemExit("Each issue must include non-empty 'title' and 'body'.")
|
||||
|
||||
for issue in issues:
|
||||
label_names = issue.get("labels") or []
|
||||
if not isinstance(label_names, list) or not all(
|
||||
isinstance(name, str) for name in label_names
|
||||
):
|
||||
raise SystemExit("'labels' must be an array of strings.")
|
||||
|
||||
label_ids, missing_labels = resolve_label_ids(label_names, label_map)
|
||||
payload = {
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
"labels": label_ids,
|
||||
}
|
||||
|
||||
if args.dry_run:
|
||||
print(f"repo_url={repo_url}")
|
||||
print(f"DRY-RUN\t{issue['title']}")
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
if missing_labels:
|
||||
print(f"missing_labels={missing_labels}")
|
||||
if not token:
|
||||
print("warning\tno token provided, remote label validation skipped")
|
||||
continue
|
||||
|
||||
created = request_json(f"{api_base}/issues", token, method="POST", payload=payload)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected issue creation response.")
|
||||
print(
|
||||
f"#{created.get('number')}\t{created.get('title')}\t{created.get('html_url')}"
|
||||
)
|
||||
if missing_labels:
|
||||
print(f"warning\tmissing labels skipped: {', '.join(missing_labels)}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,222 +0,0 @@
|
||||
---
|
||||
name: issue
|
||||
description: 查看当前仓库或任意 Gitea 仓库的 issue 列表和单条详情,支持自动识别 git origin、用户指定仓库、状态筛选和格式化输出。
|
||||
---
|
||||
|
||||
# Issue - 通用 Gitea Issue 查看
|
||||
|
||||
> **定位**:这是只读 skill。用于查看当前仓库或指定仓库的 issue 列表、单条详情和评论,不负责拆单、创建工单、push 或 PR。
|
||||
>
|
||||
> 如果用户要做的是“把问题拆成 issue 并实际创建工单”,改用 `issue-drive`。如果用户想用统一入口处理 Gitea 相关任务,改用 `gitea`。
|
||||
|
||||
当用户调用 `/issue`、`$issue`,或自然语言要求“查看当前仓库 issue”“看某个 Gitea 仓库的 issue”“查 issue #17”时,执行以下步骤。
|
||||
|
||||
## 1. 解析输入
|
||||
|
||||
支持以下几种目标仓库写法:
|
||||
|
||||
- 不传仓库参数:默认使用当前项目的 `git remote get-url origin`
|
||||
- 完整仓库 URL:`https://git.example.com/owner/repo`
|
||||
- 仓库简写:`owner/repo`
|
||||
- 当前仓库 + 单条 issue:`/issue 17`
|
||||
- 指定仓库 + 单条 issue:`/issue owner/repo 17` 或 `/issue https://git.example.com/owner/repo 17`
|
||||
|
||||
同时支持:
|
||||
|
||||
- `--state=open|closed|all`,默认 `open`
|
||||
- `--limit=<N>`,默认 `50`
|
||||
|
||||
解析规则:
|
||||
|
||||
- 第二个位置参数如果是纯数字,视为 `issue-number`,进入详情模式
|
||||
- 如果第一个位置参数是纯数字,则视为“当前仓库的 issue 编号”
|
||||
- `owner/repo` 这种简写依赖 `GITEA_BASE_URL`
|
||||
- 如果没有显式仓库参数,就读取当前仓库的 `origin`
|
||||
|
||||
规范化仓库目标时,接受以下输入:
|
||||
|
||||
- `https://host[/prefix]/owner/repo`
|
||||
- `git@host:owner/repo.git`
|
||||
- `git@host:prefix/owner/repo.git`
|
||||
- `ssh://git@host/owner/repo.git`
|
||||
- `owner/repo`
|
||||
|
||||
提取结果必须包含:
|
||||
|
||||
- `origin`:例如 `https://git.example.com`,如果 Gitea 部署在子路径下,保留前缀,例如 `https://git.example.com/gitea`
|
||||
- `owner`
|
||||
- `repo`
|
||||
- `repo_path`:`owner/repo`
|
||||
|
||||
如果无法从参数或当前仓库推断出目标仓库,明确提示:
|
||||
|
||||
```bash
|
||||
❌ 无法确定目标仓库
|
||||
|
||||
请显式传入:
|
||||
/issue https://git.example.com/owner/repo
|
||||
|
||||
或先配置:
|
||||
export GITEA_BASE_URL=https://git.example.com
|
||||
```
|
||||
|
||||
## 2. 检查环境变量
|
||||
|
||||
先读取环境变量:
|
||||
|
||||
- `GITEA_TOKEN`:必需,读取 issue 时使用
|
||||
- `GITEA_BASE_URL`:可选;当仓库参数是 `owner/repo`,或当前仓库 `origin` 是 SSH 地址时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,输出:
|
||||
|
||||
```bash
|
||||
❌ 缺少 GITEA_TOKEN
|
||||
|
||||
请先在当前 shell 或 .env 中配置:
|
||||
export GITEA_TOKEN=your_gitea_token
|
||||
```
|
||||
|
||||
然后停止,不继续请求 API。
|
||||
|
||||
## 3. 解析当前仓库 origin
|
||||
|
||||
当没有显式传仓库参数时,执行:
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
```
|
||||
|
||||
处理规则:
|
||||
|
||||
- 如果是 `https://host[/prefix]/owner/repo(.git)`,直接使用
|
||||
- 如果是 `git@host:owner/repo(.git)` 或 `ssh://git@host/owner/repo(.git)`:
|
||||
- 优先用 `GITEA_BASE_URL` 作为 API/Web 基地址
|
||||
- 否则退回 `https://host`
|
||||
- 如果当前目录不是 git 仓库,或没有 `origin`,停止并提示用户显式传仓库
|
||||
|
||||
不要为了查 issue 再向用户追问仓库 URL;只有在当前项目和参数都无法推断时才提示。
|
||||
|
||||
## 4. 调用 Gitea API
|
||||
|
||||
不要调用仓库元信息接口,避免依赖额外 scope。仓库标题直接使用 `repo_path`。
|
||||
|
||||
### 4.1 列表模式
|
||||
|
||||
请求:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issues.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues?state=${state}&limit=${limit}"
|
||||
```
|
||||
|
||||
### 4.2 详情模式
|
||||
|
||||
先请求 issue 详情:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issue_detail.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues/${issue_number}"
|
||||
```
|
||||
|
||||
再请求评论:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issue_comments.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues/${issue_number}/comments"
|
||||
```
|
||||
|
||||
### 4.3 错误处理
|
||||
|
||||
根据 HTTP 状态码给出简短、直接的提示:
|
||||
|
||||
- `401`:`GITEA_TOKEN` 无效或未生效
|
||||
- `403`:token scope 不足,或当前用户无权访问该仓库 / issue
|
||||
- `404`:仓库不存在,或该 issue 编号不存在
|
||||
- 其他非 `2xx`:输出状态码和响应中的 `message`
|
||||
|
||||
如果列表接口返回项里存在 `pull_request` 且非空,排除这些项,只保留 issue。
|
||||
|
||||
## 5. 格式化输出
|
||||
|
||||
优先使用 `jq` 解析 JSON;如果环境没有 `jq`,再退回模型手工整理,但输出结构保持一致。
|
||||
|
||||
### 5.1 列表模式
|
||||
|
||||
输出结构固定为:
|
||||
|
||||
```markdown
|
||||
📋 {repo_path} Issues
|
||||
|
||||
状态: {state} | 数量: {count} | 限制: {limit}
|
||||
|
||||
| # | 标题 | 优先级 | 标签 | 状态 | 提出人 |
|
||||
|---|------|--------|------|------|--------|
|
||||
| 35 | 小红书笔记状态查询 | P:紧急 | P:紧急, 需求 | open | zhangsan |
|
||||
```
|
||||
|
||||
字段规则:
|
||||
|
||||
- `优先级`:从 labels 中提取首个匹配 `^P[::]` 的 label,没有则填 `-`
|
||||
- `标签`:保留全部 label 原文,用 `, ` 连接;没有则填 `-`
|
||||
- `提出人`:优先显示 `user.full_name`,没有则显示 `user.login`
|
||||
- `状态`:直接显示 `open` 或 `closed`
|
||||
|
||||
如果过滤后没有任何 issue,明确输出“无符合条件的 issue”。
|
||||
|
||||
### 5.2 详情模式
|
||||
|
||||
输出结构固定为:
|
||||
|
||||
```markdown
|
||||
## #{number} {title}
|
||||
|
||||
- 仓库: {repo_path}
|
||||
- 状态: {state}
|
||||
- 标签: {labels}
|
||||
- 提出人: {author}
|
||||
- 创建时间: {created_at}
|
||||
- 更新时间: {updated_at}
|
||||
|
||||
### 正文
|
||||
|
||||
{body 或 “无正文”}
|
||||
|
||||
### 评论
|
||||
|
||||
- {author} | {created_at} | {1-2 句摘要}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 正文为空时写“无正文”
|
||||
- 评论按时间顺序输出
|
||||
- 每条评论只保留 1 到 2 句摘要;不要整段照抄超长评论
|
||||
- 没有评论时明确写“无评论”
|
||||
|
||||
## 6. 行为边界
|
||||
|
||||
- 默认只做列表和单条详情,不主动做主题归纳、epic 合并、优先级建议
|
||||
- 用户后续如果要求摘要、优先级排序、相似 issue 合并,再基于已拉取的数据继续分析
|
||||
- 不要求用户额外配置固定仓库 URL;优先从当前项目推断
|
||||
- 当前仓库 origin 与 Web 域名不一致时,再使用 `GITEA_BASE_URL`
|
||||
|
||||
## 7. 用法示例
|
||||
|
||||
```bash
|
||||
/issue
|
||||
/issue 17
|
||||
/issue owner/repo
|
||||
/issue owner/repo --state=all --limit=20
|
||||
/issue https://git.example.com/owner/repo
|
||||
/issue https://git.example.com/owner/repo 17
|
||||
|
||||
$issue
|
||||
$issue 17
|
||||
$issue owner/repo
|
||||
$issue owner/repo --state=all --limit=20
|
||||
$issue https://git.example.com/owner/repo
|
||||
$issue https://git.example.com/owner/repo 17
|
||||
```
|
||||
@ -1,93 +0,0 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="display: block;" viewBox="0 0 2048 887" width="1000" height="433">
|
||||
<defs>
|
||||
<linearGradient id="Gradient1" gradientUnits="userSpaceOnUse" x1="210.508" y1="105.269" x2="586.128" y2="792.383">
|
||||
<stop class="stop0" offset="0" stop-opacity="1" stop-color="rgb(228,24,73)"/>
|
||||
<stop class="stop1" offset="1" stop-opacity="1" stop-color="rgb(253,56,71)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path transform="translate(0,0)" fill="url(#Gradient1)" d="M 376.146 54.9048 C 394.494 52.0082 450.721 54.6372 467.605 60.9459 C 476.746 62.3192 486.198 63.3892 495.173 65.5997 C 515.789 70.6779 564.525 86.5296 576.81 103.019 C 582.859 111.139 585.986 121.422 584.272 131.496 C 582.52 141.792 577.15 152.234 568.387 158.191 C 559.717 164.085 547.095 166.763 536.819 164.74 C 522.557 161.932 508.608 154.389 494.635 150.13 C 458.749 139.194 420.04 135.901 382.699 137.271 L 382.087 137.409 C 377.052 138.507 371.716 138.434 366.594 139.112 C 353.633 140.827 340.292 142.838 327.621 146.09 C 253.423 165.138 184.133 214.131 142.088 278.203 C 136.733 286.364 128.204 304.126 123.434 309.932 C 111.073 334.935 104.381 360.26 95.8454 386.549 L 95.7672 387.361 C 94.7432 397.501 91.6381 407.384 90.5873 417.508 C 89.7119 425.942 89.9836 434.794 89.8723 443.272 C 89.4501 475.416 91.5586 497.422 98.6317 528.874 C 100.45 536.927 102.824 544.844 105.737 552.569 C 107.655 557.772 110.601 563.483 111.543 568.893 C 117.587 584.712 125.65 599.216 133.531 614.15 C 143.94 630.512 155.472 646.586 168.527 660.964 C 173.349 666.275 188.564 678.675 191.256 683.236 C 251.557 733.221 311.796 756.819 390.291 761.391 C 394.05 760.783 398.822 761.571 402.708 761.517 C 415.486 761.341 428.664 761.316 441.345 759.599 C 485.651 753.598 530.394 737.673 568.043 713.473 C 575.81 708.481 583.428 703.049 590.788 697.474 C 594.071 694.988 596.979 692.019 600.533 689.912 L 601.084 689.592 C 616.553 677.289 631.234 662.355 643.509 646.888 C 644.812 642.001 654.107 632.73 657.41 628.311 C 676.308 596.768 687.36 578.365 698.997 542.562 C 702.091 535.055 703.233 526.973 705.666 519.255 C 705.725 507.623 709.936 495.566 711.264 483.918 C 713.169 467.214 712.726 450.024 712.977 433.224 L 712.92 431.802 C 711.531 426.337 711.557 419.947 710.818 414.282 C 709.582 404.813 707.787 395.546 705.774 386.213 C 700.989 351.447 677.646 305.278 659.181 275.386 C 653.455 266.116 644.933 257.388 640.642 247.468 C 637.876 241.072 637.34 233.314 637.61 226.417 C 637.996 216.532 640.733 207.138 648.335 200.309 C 656.928 192.59 671.867 189.783 683.178 190.458 C 692.932 191.041 701.105 194.806 707.559 202.061 L 714.51 211.387 C 737.619 243.545 757.234 275.572 771.319 312.816 C 774.839 322.124 777.095 331.789 780.403 341.131 C 797.083 399.915 800.154 457.173 790.602 517.54 C 788.234 532.501 785.885 549.101 779.732 563.007 C 768.839 611.398 725.068 690.424 687.592 724.592 C 658.332 752.202 618.553 787.704 581.018 802.842 C 562.995 813.263 542.695 820.874 522.909 827.195 C 447.846 851.214 367.429 852.958 291.395 832.215 C 275.181 827.889 259.267 822.031 243.62 816.005 C 220.843 805.593 199.356 794.344 178.44 780.521 C 91.1047 722.799 33.0984 630.627 12.3333 528.81 C 11.7547 526.892 11.3367 524.926 10.9201 522.968 C 6.97496 504.429 5.54503 484.947 4.99442 466.026 C 2.49649 384.006 25.2737 303.201 70.2396 234.56 C 110.525 174.202 165.964 125.49 231.002 93.3037 C 244.395 86.5531 259.95 78.4536 274.682 75.364 C 288.042 70.2143 301.587 66.3748 315.565 63.2966 C 322.289 61.7913 329.104 59.9829 335.973 59.355 C 347.591 55.8138 364.002 55.0317 376.146 54.9048 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 705.774 386.213 C 700.989 351.447 677.646 305.278 659.181 275.386 C 653.455 266.116 644.933 257.388 640.642 247.468 C 637.876 241.072 637.34 233.314 637.61 226.417 C 637.996 216.532 640.733 207.138 648.335 200.309 C 656.928 192.59 671.867 189.783 683.178 190.458 C 692.932 191.041 701.105 194.806 707.559 202.061 L 714.51 211.387 C 737.619 243.545 757.234 275.572 771.319 312.816 C 774.839 322.124 777.095 331.789 780.403 341.131 C 797.083 399.915 800.154 457.173 790.602 517.54 C 788.234 532.501 785.885 549.101 779.732 563.007 C 768.839 611.398 725.068 690.424 687.592 724.592 C 658.332 752.202 618.553 787.704 581.018 802.842 C 562.995 813.263 542.695 820.874 522.909 827.195 C 447.846 851.214 367.429 852.958 291.395 832.215 C 275.181 827.889 259.267 822.031 243.62 816.005 C 220.843 805.593 199.356 794.344 178.44 780.521 C 91.1047 722.799 33.0984 630.627 12.3333 528.81 L 13.647 527.729 C 14.402 529.837 15.0932 534.929 16.7981 535.785 C 17.5141 533.357 15.8765 529.94 15.0764 527.549 L 15.9 527.418 C 18.6345 534.095 20.0034 541.498 21.9989 548.453 C 26.9283 565.633 31.6172 583.045 38.2114 599.677 C 64.9422 667.101 116.207 732.766 177.003 772.666 C 189.614 780.943 202.698 789.795 216.058 796.759 C 224.712 801.271 234.424 804.33 242.789 809.289 C 250.667 812.637 258.854 815.525 266.905 818.433 C 349.42 848.238 444.177 848.559 527.411 820.818 C 545.994 814.624 563.998 805.904 581.717 797.604 C 603.127 783.645 625.24 771.395 645.477 755.591 C 658.557 745.377 670.301 733.195 682.386 721.844 C 728.545 674.13 761.29 615.077 777.312 550.652 C 797.154 486.362 792.669 418.058 778.475 353.134 C 767.793 317.922 754.532 281.526 735.293 249.969 C 728.138 238.235 719.461 227.302 711.837 215.843 C 708.68 211.913 705.464 208.03 702.19 204.197 C 693.73 197.534 684.075 194.889 673.357 196.197 C 665.575 197.147 657.47 200.047 650.684 203.95 C 644.436 214.061 640.809 227.514 643.576 239.394 C 646.334 251.238 671.063 283.842 678.471 297.084 C 693.576 324.081 703.652 353.558 710.477 383.619 C 713.023 393.568 715.018 403.747 715.532 414.027 L 717.071 423.777 C 720.72 442.779 716.413 511.912 708.485 528.149 C 706.525 537.703 703.771 546.858 700.269 555.961 C 697.102 566.785 686.883 593.093 679.438 600.774 C 684.103 591.158 688.576 581.632 692.348 571.622 C 685.985 582.691 667.071 624.486 657.41 628.311 C 676.308 596.768 687.36 578.365 698.997 542.562 C 702.091 535.055 703.233 526.973 705.666 519.255 C 705.725 507.623 709.936 495.566 711.264 483.918 C 713.169 467.214 712.726 450.024 712.977 433.224 L 712.92 431.802 C 711.531 426.337 711.557 419.947 710.818 414.282 C 709.582 404.813 707.787 395.546 705.774 386.213 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 89.8436 390.816 C 92.8902 370.901 99.7863 351.425 106.984 332.654 C 110.049 324.66 121.333 295.736 127.989 292.773 L 129.293 294.957 C 125.28 299.606 124.047 303.884 123.434 309.932 C 111.073 334.935 104.381 360.26 95.8454 386.549 L 95.7672 387.361 C 94.7432 397.501 91.6381 407.384 90.5873 417.508 C 89.7119 425.942 89.9836 434.794 89.8723 443.272 C 89.4501 475.416 91.5586 497.422 98.6317 528.874 C 100.45 536.927 102.824 544.844 105.737 552.569 C 107.655 557.772 110.601 563.483 111.543 568.893 C 117.587 584.712 125.65 599.216 133.531 614.15 C 129.444 612.078 127.432 605.723 123.651 602.599 C 123.042 605.188 129.828 614.104 130.748 617.464 C 119.822 604.665 113.472 587.447 107.114 572.034 C 106.864 571.666 106.835 571.641 106.596 571.18 C 84.8375 529.334 79.3733 455.273 86.371 409.569 C 87.2906 403.563 87.7619 396.511 89.8436 390.816 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 467.605 60.9459 C 476.746 62.3192 486.198 63.3892 495.173 65.5997 C 515.789 70.6779 564.525 86.5296 576.81 103.019 C 582.859 111.139 585.986 121.422 584.272 131.496 C 582.52 141.792 577.15 152.234 568.387 158.191 C 559.717 164.085 547.095 166.763 536.819 164.74 C 522.557 161.932 508.608 154.389 494.635 150.13 C 458.749 139.194 420.04 135.901 382.699 137.271 C 401.38 126.952 478.879 143.57 501.374 150.331 C 513.555 153.993 525.503 160.481 537.856 163.083 C 547.196 165.051 558.991 161.578 566.89 156.404 C 575.585 150.709 579.423 143.326 581.522 133.4 C 583.575 123.696 581.557 112.369 575.994 104.046 C 573.49 100.3 570.487 98.0368 566.074 97.1358 L 565.314 97.7949 C 569.437 99.0598 571.952 101.348 574.235 104.996 C 580.205 114.537 580.949 124.563 578.348 135.333 C 576.615 142.508 573.838 150.994 567.234 155.03 L 566.34 154.256 C 566.994 151.676 568.333 150.62 570.467 149.029 C 570.722 148.838 570.982 148.652 571.227 148.448 C 575.662 144.773 577.981 132.345 578.447 126.705 C 579.116 118.618 577.833 111.784 572.383 105.569 C 558.678 89.9432 495.022 68.0075 474.068 66.5128 C 471.389 64.8806 469.466 63.4925 467.605 60.9459 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 191.256 683.236 C 251.557 733.221 311.796 756.819 390.291 761.391 C 384.048 764.932 370.279 761.634 362.687 762.291 C 361.888 762.36 362.274 762.212 361.617 762.991 C 367.962 764.537 376.21 766.043 382.625 764.548 C 382.899 764.484 383.171 764.412 383.444 764.344 L 384.048 765.039 C 383.31 765.409 382.77 765.691 381.943 765.815 C 373.428 767.101 362.837 765.155 354.356 763.843 C 317.586 758.155 264.109 743.033 234.667 719.807 L 241.003 722.723 C 233.991 716.274 224.813 711.666 216.97 706.228 C 211.114 702.167 192.413 689.79 191.256 683.236 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(241,25,50)" d="M 376.146 54.9048 C 394.494 52.0082 450.721 54.6372 467.605 60.9459 C 469.466 63.4925 471.389 64.8806 474.068 66.5128 C 462.177 64.3163 450.083 61.7173 438.022 60.8079 C 415.361 59.0993 392.468 60.3754 369.865 58.5625 C 373.126 56.6957 426.662 57.683 431.679 59.027 C 432.503 59.2478 433.394 58.6279 434.177 58.2957 C 425.546 51.9762 387.515 60.0202 376.146 54.9048 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(241,25,50)" d="M 315.565 63.2966 C 322.289 61.7913 329.104 59.9829 335.973 59.355 L 335.213 61.0976 C 331.824 61.9908 328.07 62.6586 325.211 64.7655 L 330.836 63.1888 C 323.47 66.8573 315.681 68.5767 307.736 70.4894 C 295.905 73.9845 287.012 75.9701 274.682 75.364 C 288.042 70.2143 301.587 66.3748 315.565 63.2966 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(241,25,50)" d="M 601.084 689.592 C 616.553 677.289 631.234 662.355 643.509 646.888 C 642.944 649.312 642.433 651.632 641.348 653.888 C 642.715 652.59 643.982 651.396 645.593 650.391 C 643.168 657.971 615.843 685.786 608.409 689.944 C 607.817 690.275 607.352 690.377 606.696 690.521 L 610.446 685.035 C 607.305 687.498 605.104 689.098 601.084 689.592 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1610.11 53.6333 C 1610.18 47.1911 1610.04 40.7484 1609.67 34.3162 C 1612.92 32.1096 1622.54 32.521 1626.83 32.421 C 1701.22 30.6842 1776.03 32.2737 1850.46 32.2788 L 1942.68 32.293 C 1961.78 32.2982 1981.13 31.6609 2000.17 33.0634 C 2002.29 59.7016 2000.77 87.3161 2000.5 114.078 C 2000.18 145.397 2000.71 176.761 2000.65 208.089 L 2000.53 275.627 C 2000.59 287.904 2001.86 301.31 2000.39 313.448 C 1999.08 314.796 1998.19 315.254 1996.36 315.657 C 1983.81 313.542 1969.62 314.801 1956.84 314.86 L 1885.1 315.187 C 1843.56 314.96 1802.02 314.957 1760.49 315.178 C 1737.73 315.239 1714.92 314.515 1692.21 316.146 C 1682.97 316.356 1673.71 316.127 1664.47 316.03 C 1647.62 319.103 1627.49 316.171 1610.18 316.817 C 1609.42 279.846 1609.91 242.781 1609.91 205.802 L 1610.11 53.6333 z M 1931 253.687 C 1934.04 247.761 1931.57 214.273 1932.58 204.615 C 1914.48 203.829 1896.3 204.085 1878.18 204.045 C 1864.82 204.016 1851.24 203.501 1837.9 204.271 C 1837.75 219.838 1836.4 236.461 1838.1 251.9 C 1840.18 253.488 1842.92 253.076 1845.51 253.213 L 1901.24 253.025 C 1910.95 253.041 1921.4 252.368 1931 253.687 z M 1770.88 142.098 C 1771.08 126.574 1770.3 110.547 1772.02 95.1281 C 1753 96.0183 1693.67 98.7431 1678.07 94.8681 C 1678.09 111.235 1678.34 127.63 1678.14 143.994 C 1708.74 143.936 1739.89 145.247 1770.43 143.809 L 1770.88 142.098 z M 1934.72 92.3052 C 1911.33 88.9068 1886.2 90.9728 1862.62 91.3784 C 1853.2 91.5404 1843.25 90.4708 1833.95 92.0614 C 1833.84 102.176 1831.75 140.452 1835.62 146.951 C 1843.68 147.053 1854.75 146.124 1862.56 144.157 C 1885.64 143.39 1908.85 144.079 1931.95 143.907 C 1931.95 138.803 1932.1 133.689 1932.2 128.585 C 1935.08 117.315 1933.3 104.041 1934.72 92.3052 z M 1677.37 253.007 C 1708.52 253.373 1739.72 253.078 1770.88 253.105 L 1771.3 225.345 C 1771.16 218.243 1771.08 211.133 1770.85 204.034 C 1759.02 205.292 1746.28 204.266 1734.35 204.189 C 1715.7 204.069 1697.03 204.4 1678.38 204.082 C 1678.04 215.578 1680.36 244.069 1677.37 253.007 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1610.11 53.6333 C 1610.18 47.1911 1610.04 40.7484 1609.67 34.3162 C 1612.92 32.1096 1622.54 32.521 1626.83 32.421 C 1701.22 30.6842 1776.03 32.2737 1850.46 32.2788 L 1942.68 32.293 C 1961.78 32.2982 1981.13 31.6609 2000.17 33.0634 C 2002.29 59.7016 2000.77 87.3161 2000.5 114.078 C 2000.18 145.397 2000.71 176.761 2000.65 208.089 L 2000.53 275.627 C 2000.59 287.904 2001.86 301.31 2000.39 313.448 C 1999.08 314.796 1998.19 315.254 1996.36 315.657 C 1993.64 303.906 1995.65 250.184 1995.69 234.585 C 1995.92 168.649 1995.78 102.714 1995.29 36.7799 C 1925.27 37.7276 1855.17 36.83 1785.15 36.817 C 1728.68 36.8065 1672.04 37.9569 1615.6 36.5773 C 1614.95 43.6695 1612.3 47.175 1610.11 53.6333 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1664.47 316.03 C 1666.5 314.805 1667.46 314.503 1668.45 312.273 L 1665.67 311.474 C 1671.97 311.086 1686.89 308.672 1692.15 311.634 L 1690.91 313.897 L 1692.21 316.146 C 1682.97 316.356 1673.71 316.127 1664.47 316.03 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1845.51 253.213 C 1844.99 254.36 1844.2 255.349 1843.47 256.378 C 1840.74 257.034 1839.14 257.385 1836.42 256.348 C 1834.75 254.769 1834.05 253.306 1833.8 251.037 C 1833.38 247.235 1834.13 243.089 1834.11 239.233 C 1834.04 230.355 1831.04 208.119 1837.09 201.151 C 1839.3 198.598 1842.79 197.701 1846.04 197.531 C 1859.1 196.843 1873.03 198.612 1886.2 198.769 C 1900.33 198.938 1915.64 197.011 1929.62 198.925 C 1932.66 199.342 1933.85 199.862 1936.11 201.931 L 1934.68 204.411 L 1932.58 204.615 C 1914.48 203.829 1896.3 204.085 1878.18 204.045 C 1864.82 204.016 1851.24 203.501 1837.9 204.271 C 1837.75 219.838 1836.4 236.461 1838.1 251.9 C 1840.18 253.488 1842.92 253.076 1845.51 253.213 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" fill-opacity="0.988235" d="M 1833.95 92.0614 C 1843.25 90.4708 1853.2 91.5404 1862.62 91.3784 C 1886.2 90.9728 1911.33 88.9068 1934.72 92.3052 C 1933.3 104.041 1935.08 117.315 1932.2 128.585 C 1932.13 117.541 1931.83 106.443 1932.31 95.4083 C 1900.97 94.8136 1869.11 97.621 1837.92 95.6443 C 1837.75 111.765 1837.75 127.888 1837.91 144.009 C 1846.11 144.051 1854.36 143.908 1862.56 144.157 C 1854.75 146.124 1843.68 147.053 1835.62 146.951 C 1831.75 140.452 1833.84 102.176 1833.95 92.0614 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1677.37 253.007 C 1676.01 249.029 1675.36 206.342 1676.51 201.588 C 1676.74 200.659 1676.51 200.948 1677.41 200.433 C 1683.58 196.905 1762.47 197.876 1771.89 200.114 C 1775.4 203.794 1775.91 210.315 1776.2 215.219 C 1776.32 217.185 1776.54 220.848 1775.2 222.344 C 1774.64 216.979 1774.18 211.604 1773.84 206.222 C 1770.93 210.228 1774.69 219.912 1771.3 225.345 C 1771.16 218.243 1771.08 211.133 1770.85 204.034 C 1759.02 205.292 1746.28 204.266 1734.35 204.189 C 1715.7 204.069 1697.03 204.4 1678.38 204.082 C 1678.04 215.578 1680.36 244.069 1677.37 253.007 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1936.11 201.931 C 1938.65 208.234 1937.95 246.218 1935.33 252.21 C 1935.07 252.803 1934.76 253.362 1934.43 253.919 L 1932.49 254.612 L 1931 253.687 C 1934.04 247.761 1931.57 214.273 1932.58 204.615 L 1934.68 204.411 L 1936.11 201.931 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1006.6 45.4499 C 1031.54 43.1833 1056.7 37.1747 1081.15 31.9075 L 1166.87 13.5267 C 1184.98 9.6218 1204.58 3.5952 1223.1 2.70382 C 1227.92 13.2229 1243.3 49.5666 1243.48 59.1636 C 1241.48 63.1086 1237.17 64.3833 1233.13 65.7144 C 1213.13 72.2883 1183.43 75.7795 1165.67 83.6946 C 1165.77 96.0882 1166.22 108.972 1165.32 121.308 C 1165.5 131.861 1165.51 142.417 1165.35 152.971 C 1185.9 153.481 1206.51 152.376 1227.03 153.395 L 1229.57 154.32 C 1231.82 159.785 1230.47 209.232 1228.99 217.22 C 1221.36 221.118 1176.66 218.927 1165.12 218.805 L 1165.69 233.003 C 1186.68 256.95 1208.07 280.214 1228.61 304.623 C 1233.71 310.689 1239.48 316.447 1244.29 322.719 C 1236.36 331.171 1208.23 368.286 1200.38 369.973 C 1196.71 368.94 1195.46 367.551 1193.56 364.221 C 1185.87 350.734 1178.82 336.778 1171.16 323.225 C 1169.29 319.923 1167.83 315.741 1165.57 312.761 L 1161.87 313.52 L 1159.86 313.826 C 1157.91 317.848 1159.13 490.857 1159.14 509.002 C 1145.76 510.751 1115.26 511.795 1102.82 509.487 C 1101.45 507.065 1101.81 504.262 1101.87 501.573 C 1101.28 499.136 1100.77 496.759 1100.43 494.27 L 1098.89 498.558 L 1098.39 503.214 L 1097.35 503.129 L 1095.92 499.799 C 1095.86 471.761 1095.12 443.548 1096.09 415.536 C 1095.38 399.262 1096.02 382.718 1096.5 366.436 C 1095.43 354.843 1098.31 342.422 1096.3 331.133 C 1094.22 332.644 1093.54 335.68 1092.65 338.015 C 1086.5 355.983 1076.78 383.433 1064.1 397.913 L 1060.97 393.867 C 1057.86 398.825 1054.89 404.149 1051.36 408.806 L 1055.69 410.177 C 1050.14 417.984 1039.76 431.194 1031.55 435.802 C 1030.6 436.331 1029.46 435.666 1028.45 435.368 C 1025.23 430.779 1025.33 423.663 1023.19 418.389 C 1017.87 405.294 1004.74 390.662 1003.89 376.48 C 1003.53 370.47 1008.7 365.943 1012.39 361.839 C 1015.56 356.263 1020.27 350.809 1023.91 345.415 C 1031.48 334.187 1038.5 322.905 1045.51 311.328 C 1052.16 296.668 1060.7 282.29 1068.14 267.958 C 1073.98 251.756 1082.05 236.285 1087.94 219.978 C 1064.75 219.246 1041.57 219.542 1018.39 218.85 C 1018.39 197.742 1020.26 175.702 1016.45 154.876 C 1036.44 153.75 1080.04 150.789 1098.35 154.211 C 1096.51 136.91 1101.28 111.579 1097.41 95.7053 L 1095.49 94.8062 L 1094.39 97.2745 C 1080.68 100.429 1036.63 107.213 1023.9 106.893 C 1019.48 88.4613 1013.94 62.5029 1006.6 45.4499 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1006.6 45.4499 C 1031.54 43.1833 1056.7 37.1747 1081.15 31.9075 L 1166.87 13.5267 C 1184.98 9.6218 1204.58 3.5952 1223.1 2.70382 C 1227.92 13.2229 1243.3 49.5666 1243.48 59.1636 C 1241.48 63.1086 1237.17 64.3833 1233.13 65.7144 C 1213.13 72.2883 1183.43 75.7795 1165.67 83.6946 C 1165.77 96.0882 1166.22 108.972 1165.32 121.308 C 1165.17 108.312 1161.23 93.7619 1162.46 81.3738 C 1167.31 72.6395 1224.87 64.7892 1237.57 59.0255 L 1238.01 58.8212 C 1234.92 42.6656 1226.92 21.5456 1218.09 7.64251 C 1183.03 16.4377 1147.04 22.5421 1111.67 29.9825 C 1081.42 36.346 1044.19 46.3595 1014.09 48.6933 C 1017.42 66.1257 1021.73 84.9866 1027.72 101.699 C 1037.27 99.5806 1047.53 99.321 1057.25 97.9256 C 1067.59 96.4403 1078.76 93.28 1089.14 93.0457 C 1091.63 92.9897 1093.37 93.5455 1095.49 94.8062 L 1094.39 97.2745 C 1080.68 100.429 1036.63 107.213 1023.9 106.893 C 1019.48 88.4613 1013.94 62.5029 1006.6 45.4499 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1227.03 153.395 L 1229.57 154.32 C 1231.82 159.785 1230.47 209.232 1228.99 217.22 C 1221.36 221.118 1176.66 218.927 1165.12 218.805 L 1165.69 233.003 C 1186.68 256.95 1208.07 280.214 1228.61 304.623 C 1233.71 310.689 1239.48 316.447 1244.29 322.719 C 1236.36 331.171 1208.23 368.286 1200.38 369.973 C 1196.71 368.94 1195.46 367.551 1193.56 364.221 C 1185.87 350.734 1178.82 336.778 1171.16 323.225 C 1169.29 319.923 1167.83 315.741 1165.57 312.761 L 1161.87 313.52 C 1162.79 312.227 1163.93 311.067 1164.99 309.887 L 1166.66 309.967 C 1176.31 318.59 1194.1 354.305 1200.74 367.374 C 1211.15 351.42 1225.19 336.886 1237.77 322.592 C 1232.06 314.885 1224.74 307.856 1218.47 300.554 C 1206.08 286.135 1194.14 271.427 1181.47 257.237 C 1175.55 250.609 1166.98 243.796 1162.42 236.366 C 1160.81 233.731 1160.19 229.324 1159.97 226.307 C 1159.71 222.718 1159.8 217.944 1162.42 215.168 C 1165.18 212.253 1216.51 213.835 1224.21 213.874 C 1224.9 199.546 1221.88 165.12 1227.03 153.395 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1016.45 154.876 C 1036.44 153.75 1080.04 150.789 1098.35 154.211 L 1098.86 157.509 C 1093.04 162.702 1033.68 159.729 1021.95 160.202 C 1022.35 178.003 1023.23 196.081 1021.96 213.849 C 1035.1 214.169 1048.2 215.267 1061.34 215.599 C 1068.9 215.79 1077.33 214.713 1084.77 215.904 C 1086.92 216.25 1088.38 217.037 1090.04 218.449 C 1091.77 221.73 1091.02 224.48 1089.98 227.878 C 1085.85 241.274 1077.5 253.557 1073.43 266.75 L 1068.14 267.958 C 1073.98 251.756 1082.05 236.285 1087.94 219.978 C 1064.75 219.246 1041.57 219.542 1018.39 218.85 C 1018.39 197.742 1020.26 175.702 1016.45 154.876 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1096.3 331.133 L 1098.37 332.127 L 1100.45 329.983 C 1102.99 334.17 1102.09 344.035 1102.13 349.091 C 1101.85 376.406 1101.72 403.722 1101.74 431.038 C 1101.9 454.468 1102.81 478.159 1101.87 501.573 C 1101.28 499.136 1100.77 496.759 1100.43 494.27 L 1098.89 498.558 L 1098.39 503.214 L 1097.35 503.129 L 1095.92 499.799 C 1095.86 471.761 1095.12 443.548 1096.09 415.536 C 1095.38 399.262 1096.02 382.718 1096.5 366.436 C 1095.43 354.843 1098.31 342.422 1096.3 331.133 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1098.09 397.344 C 1103.18 430.466 1098.83 465.213 1098.89 498.558 L 1098.39 503.214 L 1097.35 503.129 L 1095.92 499.799 C 1095.86 471.761 1095.12 443.548 1096.09 415.536 C 1096.79 409.475 1097.46 403.411 1098.09 397.344 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1096.3 331.133 L 1098.37 332.127 L 1100.45 329.983 C 1102.99 334.17 1102.09 344.035 1102.13 349.091 C 1101.25 350.375 1099.94 351.819 1099.24 353.183 C 1097.54 356.483 1099.89 371.953 1097.7 373.359 C 1097.32 371.052 1096.98 368.724 1096.5 366.436 C 1095.43 354.843 1098.31 342.422 1096.3 331.133 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1055.69 410.177 C 1050.14 417.984 1039.76 431.194 1031.55 435.802 C 1030.6 436.331 1029.46 435.666 1028.45 435.368 C 1025.23 430.779 1025.33 423.663 1023.19 418.389 C 1017.87 405.294 1004.74 390.662 1003.89 376.48 C 1003.53 370.47 1008.7 365.943 1012.39 361.839 C 1012 367.193 1010.01 370.558 1007.25 375.007 C 1015.56 392.717 1024.97 410.345 1031.75 428.703 C 1038.97 422.747 1045.27 415.897 1051.36 408.806 L 1055.69 410.177 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1060.97 393.867 C 1069.61 382.495 1075.33 368.04 1080.97 354.989 C 1082.91 350.497 1084.77 344.948 1087.38 340.887 C 1088.86 338.592 1090.22 338.608 1092.65 338.015 C 1086.5 355.983 1076.78 383.433 1064.1 397.913 L 1060.97 393.867 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1045.51 311.328 C 1052.16 296.668 1060.7 282.29 1068.14 267.958 L 1073.43 266.75 C 1068.67 277.741 1064.11 289.546 1056.19 298.684 L 1056.92 297.011 C 1053.86 301.211 1050 309.144 1045.51 311.328 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1024.1 600.841 C 1047.97 600.009 1071.69 601.581 1095.51 600.771 C 1099.64 604.33 1170.28 752.401 1171.95 760.294 C 1176.61 751.322 1180.84 742.243 1184.93 732.998 C 1191.38 719.083 1197.76 705.128 1205.41 691.816 C 1205.98 686.662 1243.62 609.471 1248.51 603.039 C 1259.03 598.373 1283.35 601.001 1295.44 601.178 C 1301.24 601.05 1315.75 599.339 1320.16 602.87 C 1319.94 639.745 1319.9 676.621 1320.06 713.496 C 1320.06 731.326 1320.78 749.534 1319.91 767.322 C 1319.01 759.086 1318.34 750.784 1316.68 742.659 C 1315.21 769.119 1316.01 795.856 1316.08 822.353 C 1316.12 834.41 1317.27 848.465 1315.16 860.225 L 1314.9 871.753 C 1301.12 871.207 1287.28 871.195 1273.48 870.972 C 1271.7 825.968 1273.02 780.543 1273.06 735.492 C 1273.08 716.382 1273.89 696.863 1272.4 677.824 L 1267.99 676.603 L 1267.63 675.295 L 1266.67 675.836 C 1264.44 681.942 1261.74 687.639 1258.79 693.429 C 1245.24 722.308 1231.21 750.965 1216.72 779.388 C 1206.98 798.855 1196.52 817.923 1188.53 838.222 C 1177.64 838.068 1166.43 838.44 1155.58 837.607 C 1145.89 810.774 1132.07 786.333 1119.58 760.765 C 1111.04 746.688 1105.01 731.328 1097.53 716.692 C 1089.74 705.324 1083.36 685.16 1075.1 671.059 C 1074.43 686.924 1075.9 703.244 1075.93 719.184 C 1076.03 771.654 1076.95 824.35 1075.59 876.794 C 1062.17 877.872 1037.93 878.989 1025.08 876.684 C 1021.51 865.099 1023.86 801.739 1023.86 784.074 C 1023.85 723.089 1022.53 661.788 1024.1 600.841 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1257.38 685.941 C 1258.15 683.808 1258.89 681.379 1260.01 679.406 C 1261.53 676.728 1263.97 676.529 1266.67 675.836 C 1264.44 681.942 1261.74 687.639 1258.79 693.429 C 1245.24 722.308 1231.21 750.965 1216.72 779.388 C 1206.98 798.855 1196.52 817.923 1188.53 838.222 C 1177.64 838.068 1166.43 838.44 1155.58 837.607 C 1145.89 810.774 1132.07 786.333 1119.58 760.765 L 1124.51 760.253 C 1131.96 774.613 1139.34 789.102 1146.23 803.735 C 1150.72 813.264 1154.39 823.749 1160.18 832.525 C 1168.3 832.727 1176.37 832.42 1184.47 832.109 C 1191.31 822.822 1199.47 801.76 1204.97 790.568 C 1222.11 755.658 1239.16 720.284 1257.38 685.941 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1095.51 600.771 C 1099.64 604.33 1170.28 752.401 1171.95 760.294 L 1169.41 762.968 C 1164.11 755.23 1160.62 746.15 1156.61 737.705 L 1139.57 702.304 C 1125.34 672.546 1108.39 642.438 1097.1 611.494 C 1095.78 607.861 1094.88 604.678 1095.51 600.771 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1295.44 601.178 C 1301.24 601.05 1315.75 599.339 1320.16 602.87 C 1319.94 639.745 1319.9 676.621 1320.06 713.496 C 1320.06 731.326 1320.78 749.534 1319.91 767.322 C 1319.01 759.086 1318.34 750.784 1316.68 742.659 C 1315.21 769.119 1316.01 795.856 1316.08 822.353 C 1316.12 834.41 1317.27 848.465 1315.16 860.225 C 1313.6 841.169 1315.01 820.666 1315 801.472 C 1314.97 750.692 1316.1 699.905 1315.32 649.126 C 1315.09 633.754 1316.66 617.607 1314.96 602.396 C 1308.48 602.278 1301.81 602.444 1295.44 601.178 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1184.93 732.998 C 1191.38 719.083 1197.76 705.128 1205.41 691.816 C 1205.06 700.163 1194.49 725.473 1189.98 733.031 C 1186.31 741.754 1181.62 758.633 1173.98 764.009 L 1171.41 764.502 L 1169.41 762.968 L 1171.95 760.294 C 1176.61 751.322 1180.84 742.243 1184.93 732.998 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1100.83 713.389 C 1109.2 728.732 1116.18 744.852 1124.51 760.253 L 1119.58 760.765 C 1111.04 746.688 1105.01 731.328 1097.53 716.692 L 1100.83 713.389 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1075.1 671.059 L 1075.12 668.605 L 1076.38 667.77 C 1085.98 673.264 1094.56 702.882 1100.83 713.389 L 1097.53 716.692 C 1089.74 705.324 1083.36 685.16 1075.1 671.059 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1862.25 670.478 C 1880.93 667.773 1896.95 668.992 1915.11 674.171 C 1939.98 682.89 1958.96 697.763 1970.61 721.844 C 1974.85 730.544 1977.65 739.876 1978.9 749.474 C 1979.65 762.044 1982.27 780.333 1977.85 792.266 C 1965.44 793.497 1952.36 792.896 1939.89 792.928 L 1868.62 792.706 C 1859.85 792.641 1842.58 790.411 1835.05 793.02 C 1835.06 794.206 1835.12 795.382 1835.26 796.561 C 1836.75 808.376 1843.6 819.298 1852.87 826.583 C 1870.62 840.532 1902.35 844.521 1923.16 835.886 C 1932.33 832.08 1942.21 824.064 1952.03 822.909 C 1961.27 830.284 1969.55 841.111 1977.55 849.937 C 1977.46 850.379 1977.35 850.82 1977.23 851.254 C 1975.77 856.368 1968.02 860.401 1963.63 862.842 C 1930.23 881.449 1894.48 889.16 1856.87 878.407 C 1827.99 870.151 1807.38 854.607 1792.81 828.361 C 1777.42 800.661 1777.72 765.932 1786.24 736.177 C 1789.08 728.564 1792.41 721.458 1796.62 714.498 C 1812 689.073 1834.13 677.333 1862.25 670.478 z M 1832.77 750.534 L 1832.6 753.744 C 1837.03 756.175 1919.83 755.14 1931.45 754.943 L 1932.41 754.175 C 1932.45 753.78 1932.48 753.383 1932.48 752.986 C 1932.63 742.832 1926.78 730.587 1919.78 723.426 C 1909.85 713.275 1898.11 709.974 1884.23 709.793 C 1869.9 710.091 1857.17 712.692 1846.88 723.466 C 1839.09 731.624 1837.4 740.852 1832.77 750.534 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1977.85 792.266 C 1965.44 793.497 1952.36 792.896 1939.89 792.928 L 1868.62 792.706 C 1859.85 792.641 1842.58 790.411 1835.05 793.02 C 1835.06 794.206 1835.12 795.382 1835.26 796.561 C 1836.75 808.376 1843.6 819.298 1852.87 826.583 C 1870.62 840.532 1902.35 844.521 1923.16 835.886 C 1932.33 832.08 1942.21 824.064 1952.03 822.909 C 1961.27 830.284 1969.55 841.111 1977.55 849.937 C 1977.46 850.379 1977.35 850.82 1977.23 851.254 C 1975.77 856.368 1968.02 860.401 1963.63 862.842 C 1930.23 881.449 1894.48 889.16 1856.87 878.407 C 1827.99 870.151 1807.38 854.607 1792.81 828.361 C 1777.42 800.661 1777.72 765.932 1786.24 736.177 C 1787.98 750.767 1786.53 764.692 1786.75 779.273 C 1787.18 807.634 1796.6 836.528 1819.59 854.608 C 1845.01 874.601 1879.25 880.104 1910.71 876.175 C 1933.33 873.35 1953.2 863.533 1972.08 851.205 C 1964.55 843.368 1956.75 835.676 1950.16 827.017 C 1927.95 841.131 1903.52 849.225 1877.18 843.435 C 1861.67 840.028 1846.84 831.35 1838.17 817.8 C 1835.25 813.243 1828.18 798.871 1829.17 793.863 C 1829.7 791.142 1831.65 789.098 1833.82 787.559 C 1842.07 786.012 1851.78 786.992 1860.2 787.011 L 1909.58 787.109 C 1930 787.139 1954.01 785.182 1973.85 787.28 C 1975.72 788.666 1976.68 790.318 1977.85 792.266 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1915.11 674.171 C 1939.98 682.89 1958.96 697.763 1970.61 721.844 C 1974.85 730.544 1977.65 739.876 1978.9 749.474 C 1976.96 750.058 1976.53 750.472 1974.6 749.698 C 1969.64 741.386 1968.35 730.085 1964.59 721.074 C 1956.37 701.386 1935.75 687.009 1916.71 679.202 L 1915.11 674.171 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1862.25 670.478 C 1880.93 667.773 1896.95 668.992 1915.11 674.171 L 1916.71 679.202 C 1910.32 678.039 1904.34 675.455 1897.84 674.535 C 1885.79 672.831 1873.74 675.126 1862.25 670.478 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1832.77 750.534 L 1829.62 750.529 L 1828.67 748.633 C 1829.3 737.861 1837.01 726.372 1844.52 718.98 C 1856.99 706.716 1869.9 703.755 1887 703.878 C 1884.87 705.53 1882.91 706.77 1880.45 707.863 C 1881.61 709.367 1882.43 709.3 1884.23 709.793 C 1869.9 710.091 1857.17 712.692 1846.88 723.466 C 1839.09 731.624 1837.4 740.852 1832.77 750.534 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1887 703.878 C 1898.99 703.387 1912.03 709.519 1920.7 717.574 C 1927.81 724.175 1934.63 734.806 1934.93 744.727 C 1935.03 748.027 1933.13 751.045 1932.41 754.175 C 1932.45 753.78 1932.48 753.383 1932.48 752.986 C 1932.63 742.832 1926.78 730.587 1919.78 723.426 C 1909.85 713.275 1898.11 709.974 1884.23 709.793 C 1882.43 709.3 1881.61 709.367 1880.45 707.863 C 1882.91 706.77 1884.87 705.53 1887 703.878 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1509.72 674.522 C 1524.52 673.337 1539.79 674.077 1554.65 674.261 C 1557.15 674.728 1557.81 674.604 1559.48 676.666 C 1558.01 694.465 1559.82 713.654 1559.82 731.649 C 1559.8 780.374 1560.59 829.246 1558.87 877.94 C 1547.23 877.733 1536.18 877.872 1524.56 878.672 C 1520.77 878.933 1516.8 879.432 1513.78 876.608 C 1508.56 871.724 1510.06 853.707 1509.97 846.797 L 1509.79 847.192 C 1502.71 862.277 1488.37 872.827 1472.93 878.369 C 1453.48 885.349 1426.75 884.654 1408.04 875.717 C 1392.29 868.197 1378.44 855.365 1372.67 838.624 C 1370 830.902 1368.77 822.571 1368.28 814.438 C 1367.6 803.093 1366.65 679.902 1368.75 675.904 C 1369.14 675.169 1372 674.526 1372.89 674.222 L 1418.14 674.141 C 1420.83 717.929 1417.54 762.305 1418.98 806.2 C 1422.11 817.652 1425.69 826.327 1436.65 832.502 C 1445.66 837.578 1455.88 838.986 1466.05 839.365 C 1477.2 837.184 1488.17 833.768 1496.58 825.776 C 1501.63 820.979 1505.09 815.435 1506.6 808.578 C 1510.4 791.304 1508.06 751.908 1508.06 732.246 C 1508.06 712.947 1509.13 693.796 1509.72 674.522 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1554.65 674.261 C 1557.15 674.728 1557.81 674.604 1559.48 676.666 C 1558.01 694.465 1559.82 713.654 1559.82 731.649 C 1559.8 780.374 1560.59 829.246 1558.87 877.94 C 1547.23 877.733 1536.18 877.872 1524.56 878.672 C 1520.77 878.933 1516.8 879.432 1513.78 876.608 C 1508.56 871.724 1510.06 853.707 1509.97 846.797 L 1509.79 847.192 C 1502.71 862.277 1488.37 872.827 1472.93 878.369 C 1453.48 885.349 1426.75 884.654 1408.04 875.717 C 1392.29 868.197 1378.44 855.365 1372.67 838.624 C 1370 830.902 1368.77 822.571 1368.28 814.438 C 1367.6 803.093 1366.65 679.902 1368.75 675.904 C 1369.14 675.169 1372 674.526 1372.89 674.222 C 1373.97 706.292 1373.32 738.434 1373.14 770.517 C 1373.03 788.732 1371.13 809.125 1374.92 826.979 C 1377.5 839.125 1382.79 850.808 1391.94 859.405 C 1406.05 872.651 1426.3 877.804 1445.22 877.145 C 1464.85 876.461 1482.26 871.621 1495.84 856.903 C 1499.93 852.472 1503.37 847.412 1509.7 846.33 C 1511.94 847.139 1512.87 847.422 1513.89 849.756 C 1516.34 855.324 1514.82 864.949 1514.82 871.08 C 1527.54 871.567 1540.27 871.176 1552.99 870.937 C 1553.87 824.628 1554.2 778.311 1553.97 731.995 C 1554.02 713.159 1551.42 692.76 1554.65 674.261 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1466.05 839.365 C 1460.8 840.484 1454.13 841.156 1448.72 841.827 C 1443.07 842.527 1432.96 836.371 1428.56 832.871 C 1422.64 828.169 1416.78 821.805 1416.07 813.947 C 1415.74 810.314 1416.8 808.888 1418.98 806.2 C 1422.11 817.652 1425.69 826.327 1436.65 832.502 C 1445.66 837.578 1455.88 838.986 1466.05 839.365 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1686.26 344.071 C 1707.65 342.909 1729.57 343.183 1750.96 344.27 C 1750.64 367.963 1751.27 391.664 1750.83 415.359 C 1750.86 424.891 1749.2 442.359 1756.24 449.563 C 1766.3 459.867 1812.95 456.58 1827.95 456.475 C 1841.65 456.38 1870.45 459.419 1880.46 449.032 C 1888.5 440.687 1885.34 417.979 1885.04 407.161 C 1904.77 411.367 1923.75 420.442 1943.77 422.862 C 1942.66 447.472 1945.63 480.992 1925.66 499.182 C 1921.62 502.861 1916.86 505.713 1912.23 508.577 C 1899.91 512.096 1887.55 513.952 1874.72 513.965 C 1852.29 515.323 1829.28 514.138 1806.77 514.038 C 1782.8 513.932 1756.97 515.717 1733.3 512.005 C 1723.94 510.537 1716.17 508.099 1708.33 502.759 C 1706.03 501.144 1703.64 499.314 1701.61 497.37 C 1692.41 488.551 1687.08 471.178 1686.84 458.827 C 1683.92 449.183 1686.41 362.882 1686.26 344.071 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1885.04 407.161 C 1904.77 411.367 1923.75 420.442 1943.77 422.862 C 1942.66 447.472 1945.63 480.992 1925.66 499.182 C 1921.62 502.861 1916.86 505.713 1912.23 508.577 L 1911.28 504.136 C 1915.44 502.702 1919.3 499.244 1922.45 496.226 C 1940.11 479.268 1937.38 449.249 1937.84 426.814 C 1922.1 423.748 1906.09 418.259 1890.65 413.853 C 1890.07 425.165 1891.37 437.638 1887.43 448.4 C 1886.72 449.133 1886 449.858 1885.28 450.574 C 1880.75 455.058 1876.08 457.795 1869.89 459.539 C 1852.13 464.546 1813.56 462.296 1793.97 462.144 C 1780.89 462.042 1761.44 462.112 1751.64 451.774 C 1745.35 445.138 1746.06 434.717 1746.3 426.279 C 1746.93 431.815 1747.5 437.616 1748.83 443.028 C 1749.44 445.512 1750.21 448.166 1752.52 449.568 C 1753.18 446.212 1750.35 441.943 1749.6 438.52 C 1748.06 431.552 1748.69 422.103 1750.83 415.359 C 1750.86 424.891 1749.2 442.359 1756.24 449.563 C 1766.3 459.867 1812.95 456.58 1827.95 456.475 C 1841.65 456.38 1870.45 459.419 1880.46 449.032 C 1888.5 440.687 1885.34 417.979 1885.04 407.161 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1686.84 458.827 C 1688.01 461.379 1688.99 465.914 1690.92 467.655 C 1691.87 466.271 1691.48 463.959 1691.43 462.325 C 1694.33 473.282 1696.57 485.677 1704.41 494.304 C 1707.04 497.198 1709.82 498.543 1708.33 502.759 C 1706.03 501.144 1703.64 499.314 1701.61 497.37 C 1692.41 488.551 1687.08 471.178 1686.84 458.827 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1874.72 513.965 C 1884.8 507.63 1899.8 506.316 1911.28 504.136 L 1912.23 508.577 C 1899.91 512.096 1887.55 513.952 1874.72 513.965 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1334.48 13.9338 C 1356.82 11.7735 1380.56 13.0312 1403.03 13.3648 L 1403.28 178.728 C 1403.28 195.197 1405.61 341.541 1400.41 346.659 C 1395.43 351.552 1351.55 348.949 1342.66 348.814 C 1339.99 348.69 1337.1 348.93 1334.83 347.359 C 1332.39 340.545 1334.27 288.507 1334.28 277.413 L 1334.35 117.816 C 1334.4 83.3054 1332.82 48.3826 1334.48 13.9338 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1334.48 13.9338 C 1356.82 11.7735 1380.56 13.0312 1403.03 13.3648 L 1403.28 178.728 C 1401.91 176.692 1400.72 174.599 1400.4 172.133 C 1398.71 159.197 1399.79 144.773 1399.73 131.669 C 1399.57 94.1965 1401.33 56.787 1399.74 19.3588 C 1379.58 18.9091 1359.2 20.014 1339.11 19.094 L 1339.07 238.008 L 1339.09 306.158 C 1339.09 317.838 1338.29 330.152 1339.46 341.758 C 1339.75 344.669 1340.69 346.635 1342.66 348.814 C 1339.99 348.69 1337.1 348.93 1334.83 347.359 C 1332.39 340.545 1334.27 288.507 1334.28 277.413 L 1334.35 117.816 C 1334.4 83.3054 1332.82 48.3826 1334.48 13.9338 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1437.81 285.832 C 1450.19 288.726 1462.02 297.18 1473.33 302.966 C 1482.13 307.466 1491.19 311.454 1500.05 315.823 C 1468.93 380.537 1435.5 424.255 1374.31 463.085 C 1372.58 462.337 1371.34 461.017 1369.98 459.756 C 1363.85 463.458 1357.79 467.159 1352.21 471.672 C 1349.51 473.859 1347.67 476.765 1344.74 478.628 L 1345.07 480.114 C 1328.25 490.37 1289 509.674 1270.37 513.707 C 1261.26 516.702 1250.77 518.647 1241.24 519.589 C 1238.19 514.057 1211.75 461.266 1211.64 459.456 C 1216.22 456.162 1224.83 455.283 1230.37 453.943 C 1241.44 450.161 1253.02 447.622 1263.92 443.393 C 1315.24 423.471 1374.73 386.65 1407.33 341.82 C 1414.57 329.092 1422.18 316.586 1429.37 303.818 C 1432.11 297.845 1434.63 291.579 1437.81 285.832 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1437.81 285.832 C 1450.19 288.726 1462.02 297.18 1473.33 302.966 C 1482.13 307.466 1491.19 311.454 1500.05 315.823 C 1468.93 380.537 1435.5 424.255 1374.31 463.085 C 1372.58 462.337 1371.34 461.017 1369.98 459.756 C 1426.79 425.832 1465.42 376.585 1493.43 317.476 C 1475.76 310.947 1456.81 301.346 1440.7 291.567 C 1438.7 295.452 1436.95 299.465 1435.16 303.447 L 1429.37 303.818 C 1432.11 297.845 1434.63 291.579 1437.81 285.832 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1230.37 453.943 C 1228.42 456.645 1218.11 459.493 1218.02 460.316 L 1218.7 460.176 C 1221.03 459.699 1223.16 459.584 1225.54 459.633 L 1217.79 462.212 C 1225.83 479.691 1234.34 497.962 1244.02 514.565 C 1249.63 513.139 1263.55 508.21 1268.53 510.972 L 1270.37 513.707 C 1261.26 516.702 1250.77 518.647 1241.24 519.589 C 1238.19 514.057 1211.75 461.266 1211.64 459.456 C 1216.22 456.162 1224.83 455.283 1230.37 453.943 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1429.37 303.818 L 1435.16 303.447 C 1430.04 313.293 1419.07 335.18 1410.87 341.762 C 1409.54 342.046 1408.67 342.252 1407.33 341.82 C 1414.57 329.092 1422.18 316.586 1429.37 303.818 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1660.73 669.673 C 1691.27 663.698 1723.37 673.239 1750.64 686.914 C 1748.74 693.578 1747.37 700.914 1744.12 707.051 C 1741.44 712.983 1740.14 719.451 1736.87 725.075 C 1724.35 720.183 1708.64 716.616 1697.19 711.593 C 1686.82 709.514 1677.27 709.645 1666.76 710.499 C 1659.37 713.565 1653.87 717.201 1647.56 722.098 C 1647.48 726.491 1646.52 732.962 1648.38 736.963 C 1655.47 743.461 1668.99 746.473 1678.22 748.439 L 1702.01 755.475 C 1720 759.434 1741.31 768.689 1751.6 784.857 C 1759.88 797.869 1761.62 814.771 1758.3 829.632 C 1754.46 846.773 1744.67 860.635 1729.81 869.99 C 1703.68 886.436 1670.98 886.434 1641.75 879.628 C 1626.4 876.056 1609.07 868.968 1595.67 860.538 C 1597.39 852.348 1601.78 829.076 1608.58 824.321 C 1610.45 823.011 1612.66 822.913 1614.82 823.39 C 1625.26 825.698 1636.36 832.834 1646.67 836.633 C 1662.24 842.154 1679.68 845.475 1695.84 840.318 C 1704.38 835.618 1707.96 831.049 1711.52 822.188 C 1711.02 819.924 1710.55 817.503 1709.64 815.365 C 1702.17 797.866 1651.68 791.019 1633.95 784.064 C 1621.36 779.127 1608.54 770.922 1603.04 757.996 C 1596.23 741.965 1598.23 720.557 1604.61 704.766 C 1605.41 703.433 1606.23 702.119 1607.08 700.824 C 1620.56 680.411 1637.68 674.379 1660.73 669.673 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1662.99 706.269 C 1665.64 707.659 1665.67 707.712 1666.76 710.499 C 1659.37 713.565 1653.87 717.201 1647.56 722.098 C 1647.48 726.491 1646.52 732.962 1648.38 736.963 C 1655.47 743.461 1668.99 746.473 1678.22 748.439 L 1702.01 755.475 C 1720 759.434 1741.31 768.689 1751.6 784.857 C 1759.88 797.869 1761.62 814.771 1758.3 829.632 C 1754.46 846.773 1744.67 860.635 1729.81 869.99 C 1703.68 886.436 1670.98 886.434 1641.75 879.628 C 1626.4 876.056 1609.07 868.968 1595.67 860.538 C 1597.39 852.348 1601.78 829.076 1608.58 824.321 C 1610.45 823.011 1612.66 822.913 1614.82 823.39 C 1625.26 825.698 1636.36 832.834 1646.67 836.633 C 1643.63 836.776 1640.71 836.742 1637.67 836.503 C 1636.84 836.919 1637.22 836.76 1636.53 837.011 C 1627.33 835.457 1620.88 831.105 1613.15 826.317 C 1607.95 836.107 1604.22 846.187 1601.04 856.792 C 1609.69 862.988 1619.7 867.705 1629.82 870.931 C 1657.33 879.709 1695.82 882.263 1722.23 868.386 C 1735.23 861.558 1746.87 849.806 1751.25 835.565 C 1755.74 820.97 1755.24 802.618 1747.98 788.976 C 1737.89 770.007 1715.12 764.503 1696.12 758.726 C 1689.65 758.071 1682.91 754.848 1676.34 753.58 C 1665.89 750.175 1650.3 748.155 1644.27 737.694 C 1640.53 731.202 1642.44 725 1644.3 718.319 C 1650.11 712.622 1655.37 709.153 1662.99 706.269 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1604.61 704.766 C 1604.65 704.931 1604.69 705.095 1604.72 705.261 C 1606.57 714.069 1604.59 723.13 1604.06 732 C 1603.15 747.367 1608.62 763.736 1621.8 772.532 C 1642.73 786.497 1709.86 790.767 1715.08 816.492 C 1715.38 817.993 1715.52 819.528 1715.65 821.052 C 1715.64 826.168 1714.21 831.196 1710.47 834.895 C 1705.89 839.433 1702.14 840.336 1695.84 840.318 C 1704.38 835.618 1707.96 831.049 1711.52 822.188 C 1711.02 819.924 1710.55 817.503 1709.64 815.365 C 1702.17 797.866 1651.68 791.019 1633.95 784.064 C 1621.36 779.127 1608.54 770.922 1603.04 757.996 C 1596.23 741.965 1598.23 720.557 1604.61 704.766 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1660.73 669.673 C 1691.27 663.698 1723.37 673.239 1750.64 686.914 C 1748.74 693.578 1747.37 700.914 1744.12 707.051 C 1743.86 704.044 1743.45 701.754 1742.18 699.003 L 1744.59 692.183 C 1743.42 686.139 1736.45 684.167 1731.19 682.4 C 1722.53 679.487 1713.68 676.986 1704.63 675.613 C 1694.02 674.002 1668.52 675.896 1660.73 669.673 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1742.18 699.003 C 1743.45 701.754 1743.86 704.044 1744.12 707.051 C 1741.44 712.983 1740.14 719.451 1736.87 725.075 C 1724.35 720.183 1708.64 716.616 1697.19 711.593 L 1698.34 710.376 L 1697.85 706.372 C 1709.78 708.728 1723.46 713.101 1733.88 719.387 C 1736.54 712.553 1739.41 705.79 1742.18 699.003 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1662.99 706.269 C 1669.26 704.599 1691.65 703.746 1697.85 706.372 L 1698.34 710.376 L 1697.19 711.593 C 1686.82 709.514 1677.27 709.645 1666.76 710.499 C 1665.67 707.712 1665.64 707.659 1662.99 706.269 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1258.68 97.9654 C 1258.78 95.7311 1258.73 95.0922 1259.93 93.2493 C 1269.17 89.7092 1284.92 93.6455 1294.73 94.3416 C 1302.83 94.6407 1310.94 95.0189 1319.05 94.9453 C 1318.84 158.821 1313.68 223.32 1298.94 285.621 C 1297.27 292.678 1295.18 307.259 1289.98 312.386 C 1288.65 313.7 1287.75 313.835 1285.97 313.726 C 1280.92 313.417 1274.6 310.454 1270.19 308.075 L 1248.85 298.083 C 1242.9 295.861 1234.86 292.165 1230.6 287.398 C 1230.17 283.138 1243.32 235.723 1245.03 225.775 C 1252.33 183.217 1253.42 140.652 1258.68 97.9654 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1258.68 97.9654 C 1258.78 95.7311 1258.73 95.0922 1259.93 93.2493 C 1269.17 89.7092 1284.92 93.6455 1294.73 94.3416 C 1302.83 94.6407 1310.94 95.0189 1319.05 94.9453 C 1318.84 158.821 1313.68 223.32 1298.94 285.621 C 1297.27 292.678 1295.18 307.259 1289.98 312.386 C 1288.65 313.7 1287.75 313.835 1285.97 313.726 C 1280.92 313.417 1274.6 310.454 1270.19 308.075 L 1248.85 298.083 C 1242.9 295.861 1234.86 292.165 1230.6 287.398 C 1230.17 283.138 1243.32 235.723 1245.03 225.775 C 1252.33 183.217 1253.42 140.652 1258.68 97.9654 C 1259.26 107.744 1260.44 117.631 1260.36 127.422 C 1259.9 180.228 1248.47 234.324 1236.2 285.407 C 1240.84 288.244 1245.46 291.176 1250.18 293.872 C 1258.28 297.285 1266.98 300.327 1274.73 304.453 C 1278.62 306.181 1282.38 308.059 1286.15 310.04 C 1293.78 288.631 1298.19 265.477 1301.85 243.09 C 1304.24 228.47 1307.34 213.733 1308.91 199.016 C 1312.41 166.45 1312.91 132.73 1314.04 99.9949 L 1303.44 98.1334 C 1288.55 98.2177 1273.57 98.4942 1258.68 97.9654 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1463.42 274.843 C 1461.03 275.192 1458.49 275.875 1456.27 274.663 C 1446.55 255.916 1438.37 217.855 1432.83 196.518 C 1424.73 165.393 1416.04 134.213 1408.98 102.85 C 1411.91 100.732 1427.26 99.1802 1431.79 98.0583 C 1443.09 95.2586 1459.4 86.9186 1470.22 86.6587 C 1478.75 106.288 1520.34 240.83 1518.4 258.035 C 1516.64 260.62 1515.26 260.835 1512.43 262.046 C 1503.51 261.363 1473.57 271.946 1463.42 274.843 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1463.42 274.843 C 1461.03 275.192 1458.49 275.875 1456.27 274.663 C 1446.55 255.916 1438.37 217.855 1432.83 196.518 C 1424.73 165.393 1416.04 134.213 1408.98 102.85 C 1411.91 100.732 1427.26 99.1802 1431.79 98.0583 C 1443.09 95.2586 1459.4 86.9186 1470.22 86.6587 C 1478.75 106.288 1520.34 240.83 1518.4 258.035 C 1516.64 260.62 1515.26 260.835 1512.43 262.046 L 1511.19 258.062 C 1511.9 257.421 1513.47 256.424 1513.54 255.487 C 1514.08 247.799 1510.44 236.888 1508.55 229.285 C 1501.38 200.343 1492.38 171.486 1483.16 143.133 C 1477.82 126.685 1470.04 107.797 1467.23 90.9897 C 1449.17 98.559 1433.96 103.983 1414.18 103.679 C 1419.68 129.612 1450.58 261.478 1463.42 274.843 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1968.55 328.412 C 1970.72 327.76 1972.59 326.613 1974.74 327.692 C 1982.4 331.543 2038.58 442.052 2045.53 454.665 C 2031.84 463.552 2003.54 483.539 1988.69 488.08 C 1970.75 451.652 1951.21 415.038 1930.27 380.217 C 1926.46 373.882 1912.12 358.194 1912.72 351.658 C 1915.95 349.185 1920.14 348.408 1924 347.357 C 1931.55 344.442 1939.23 341.687 1946.42 337.945 C 1953.84 334.875 1961.22 331.697 1968.55 328.412 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1968.55 328.412 C 1970.72 327.76 1972.59 326.613 1974.74 327.692 C 1982.4 331.543 2038.58 442.052 2045.53 454.665 C 2031.84 463.552 2003.54 483.539 1988.69 488.08 C 1970.75 451.652 1951.21 415.038 1930.27 380.217 C 1926.46 373.882 1912.12 358.194 1912.72 351.658 C 1915.95 349.185 1920.14 348.408 1924 347.357 C 1923.4 351.102 1920.9 352.233 1919.88 355.655 C 1946.18 396.818 1966.33 440.802 1991.23 482.623 C 2007.43 473.201 2023.75 463.749 2038.88 452.656 C 2034.41 442.441 2028.21 432.519 2022.97 422.638 C 2013.67 405.102 2004.65 387.312 1994.85 370.049 C 1988.47 358.801 1979.43 346.546 1975.41 334.321 C 1975.25 333.524 1975.07 332.574 1974.65 331.869 C 1973.18 329.373 1971.09 329.179 1968.55 328.412 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1968.55 328.412 C 1971.09 329.179 1973.18 329.373 1974.65 331.869 C 1975.07 332.574 1975.25 333.524 1975.41 334.321 C 1970.94 332.417 1956.18 339.404 1946.42 337.945 C 1953.84 334.875 1961.22 331.697 1968.55 328.412 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1604.08 358.446 C 1604.2 355.183 1604.99 351.131 1605.79 347.956 C 1606.33 345.823 1607.65 343.985 1609.59 342.928 C 1617.1 338.853 1631.66 345.014 1639.53 347.12 C 1650.47 349.158 1661.25 351.703 1672.06 354.334 C 1659.76 406.552 1656.12 437.8 1624.18 483.834 C 1621.36 487.894 1617.44 496.537 1613.36 498.899 C 1611.54 499.952 1609.99 499.306 1608.16 498.645 C 1603.31 496.888 1598.26 493.639 1593.87 490.9 C 1586.6 488.498 1579.26 482.7 1572.86 478.484 C 1568.23 475.515 1561.73 472.898 1558.37 468.516 C 1557.44 462.974 1574.21 440.015 1577.66 433.243 C 1585.54 417.755 1593.09 399.948 1597.92 383.226 C 1600.29 375.042 1601.68 366.634 1604.08 358.446 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1639.53 347.12 C 1650.47 349.158 1661.25 351.703 1672.06 354.334 C 1659.76 406.552 1656.12 437.8 1624.18 483.834 C 1621.36 487.894 1617.44 496.537 1613.36 498.899 C 1611.54 499.952 1609.99 499.306 1608.16 498.645 C 1603.31 496.888 1598.26 493.639 1593.87 490.9 L 1598.37 487.952 C 1602.29 490.158 1605.83 492.375 1609.33 495.236 C 1641.87 461.602 1658.84 402.8 1665.94 357.572 C 1660.05 356.21 1654.15 354.947 1648.31 353.383 C 1648.99 353.187 1649.53 353.034 1650.25 353.021 C 1655.32 352.932 1662.26 354.092 1666.08 357.482 C 1670.45 369.742 1649.92 426.867 1645.26 439.899 L 1645.85 439.779 C 1656.11 420.323 1660.79 398.671 1665.53 377.343 C 1666.83 371.515 1668.82 366.079 1668.95 360.046 C 1669.16 349.844 1645.67 353.48 1639.53 347.12 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1572.86 478.484 C 1568.23 475.515 1561.73 472.898 1558.37 468.516 C 1557.44 462.974 1574.21 440.015 1577.66 433.243 C 1585.54 417.755 1593.09 399.948 1597.92 383.226 C 1600.29 375.042 1601.68 366.634 1604.08 358.446 C 1604.74 365.667 1603.07 373.348 1602.05 380.505 C 1603.03 376.337 1603.99 372.161 1604.91 367.98 C 1604.27 387.324 1587.87 422.435 1578.43 439.752 C 1573.38 449.017 1567.03 458.018 1563.52 467.998 C 1567.23 470.098 1570.92 472.314 1574.71 474.262 L 1572.86 478.484 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1574.71 474.262 C 1581.13 478.09 1593.25 483.61 1598.37 487.952 L 1593.87 490.9 C 1586.6 488.498 1579.26 482.7 1572.86 478.484 L 1574.71 474.262 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 210.512 303.979 C 212.896 298.215 218.021 292.777 223.161 289.438 C 231.868 283.781 241.066 282.421 251.159 284.58 C 261.104 285.013 270.184 290.877 276.803 298.138 C 284.456 306.532 286.804 316.158 286.192 327.235 C 285.513 339.537 281.7 352.043 272.213 360.438 C 264.162 367.562 253.037 370.669 242.407 369.901 C 231.592 369.102 221.546 364.008 214.51 355.756 C 206.988 346.862 204.102 334.645 205.078 323.189 C 205.629 316.717 207.414 309.696 210.512 303.979 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 251.159 284.58 C 261.104 285.013 270.184 290.877 276.803 298.138 C 284.456 306.532 286.804 316.158 286.192 327.235 C 285.513 339.537 281.7 352.043 272.213 360.438 C 264.162 367.562 253.037 370.669 242.407 369.901 C 231.592 369.102 221.546 364.008 214.51 355.756 C 206.988 346.862 204.102 334.645 205.078 323.189 C 205.629 316.717 207.414 309.696 210.512 303.979 C 212.863 320.548 205.226 334.056 216.582 349.813 C 221.998 357.329 230.707 362.055 239.76 363.524 C 248.677 364.971 259.591 364.14 266.981 358.575 C 274.844 352.655 280.299 336.764 281.372 327.351 C 282.807 314.767 274.521 299.523 263.095 293.7 C 259.094 291.661 249.84 290.221 247.204 286.888 C 247.737 286.785 248.002 286.693 248.523 286.719 C 252.723 286.934 268.875 294.67 271.819 297.659 C 281.419 307.404 282.844 319.203 282.731 332.276 L 283.397 332.191 L 283.491 330.446 C 284.594 307.901 280.538 298.119 259.393 288.233 C 256.682 286.966 253.455 287.344 251.435 284.945 C 251.336 284.829 251.251 284.702 251.159 284.58 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 405.775 585.787 C 402.286 584 398.737 582.435 396.1 579.441 L 396.173 578.049 C 414.175 571.945 434.199 570.435 447.739 555.563 C 451.705 551.207 467.115 525.303 469.081 524.849 C 469.427 524.769 470.483 525.078 470.886 525.144 C 472.583 548.13 480.476 567.719 498.098 582.985 C 502.139 586.486 517.009 594.541 518.474 597.064 C 517.594 599.336 515.543 600.338 513.436 601.275 C 505.224 604.927 496.149 606.07 487.796 609.663 C 475.159 616.204 462.947 628.68 456.767 641.508 C 454.428 646.362 452.452 652.908 448.461 656.58 L 446.773 656.483 C 444.116 650.832 444.886 641.677 443.524 635.347 C 438.787 613.324 424.282 597.46 405.775 585.787 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 405.775 585.787 C 409.338 585.087 411.516 586.974 414.456 588.922 C 422.805 594.453 429.948 602.779 435.358 611.129 C 436.234 612.482 437.263 615.281 438.812 615.161 C 438.445 608.42 425.972 595.687 420.978 591.23 C 417.107 587.775 413.126 585.155 408.536 582.754 L 408.555 582.048 C 422.566 586.434 434.168 601.621 440.723 614.215 C 445.105 622.632 444.938 633.874 449.711 641.576 C 453.927 639.297 461.966 613.967 483.317 608.424 L 478.55 610.815 L 478.805 611.142 C 481.476 610.248 484.154 609.375 486.839 608.523 L 487.796 609.663 C 475.159 616.204 462.947 628.68 456.767 641.508 C 454.428 646.362 452.452 652.908 448.461 656.58 L 446.773 656.483 C 444.116 650.832 444.886 641.677 443.524 635.347 C 438.787 613.324 424.282 597.46 405.775 585.787 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1803.05 318.144 C 1816.9 328.396 1829.2 342.719 1841.34 354.945 C 1850.51 364.185 1860.98 373.401 1869.13 383.536 C 1864.21 392.856 1841.35 416.739 1831.82 420.308 C 1828.69 419.912 1826.84 418.064 1824.55 415.972 C 1814.44 406.707 1805.51 394.919 1796.47 384.558 C 1788.37 375.28 1779.6 366.52 1772.04 356.799 C 1770.51 354.836 1767.08 351.135 1767.46 348.594 C 1767.69 347.041 1769.03 345.378 1769.89 344.09 C 1771.44 342.481 1773.17 341.022 1774.83 339.526 C 1783.59 331.421 1793.08 324.646 1803.05 318.144 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1803.05 318.144 C 1816.9 328.396 1829.2 342.719 1841.34 354.945 C 1850.51 364.185 1860.98 373.401 1869.13 383.536 C 1864.21 392.856 1841.35 416.739 1831.82 420.308 C 1828.69 419.912 1826.84 418.064 1824.55 415.972 C 1814.44 406.707 1805.51 394.919 1796.47 384.558 C 1788.37 375.28 1779.6 366.52 1772.04 356.799 C 1770.51 354.836 1767.08 351.135 1767.46 348.594 C 1767.69 347.041 1769.03 345.378 1769.89 344.09 C 1770.78 347.369 1772.03 350.104 1774.02 352.875 C 1779.13 359.973 1785.71 366.103 1791.54 372.605 C 1804.73 387.32 1818.42 402.335 1830.22 418.183 C 1841.2 406.851 1853.01 396.325 1863.99 384.947 C 1854.41 374.027 1843.29 363.941 1833.02 353.635 C 1822.96 343.541 1813.04 332.114 1801.79 323.366 C 1797.29 327.355 1792.79 331.646 1787.93 335.178 C 1783.82 338.163 1780 340.314 1774.83 339.526 C 1783.59 331.421 1793.08 324.646 1803.05 318.144 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 445.897 250.388 C 449.325 246.881 456.128 244.104 460.925 242.921 C 469.145 240.895 475.293 241.862 482.484 246.097 C 491.384 250.018 498.032 255.541 501.663 264.868 C 505.5 274.725 503.67 286.246 499.378 295.673 C 495.288 304.656 488.547 311.581 479.176 315.066 C 470.634 318.243 460.901 318.567 452.537 314.674 C 444.78 311.064 437.485 303.083 434.372 295.102 C 430.932 288.26 430.416 281.74 431.633 274.237 C 433.149 264.886 438.193 255.998 445.897 250.388 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 472.668 248.788 C 488.812 251.718 499.974 266.59 498.277 282.91 C 496.579 299.23 482.596 311.487 466.194 311.032 C 449.793 310.577 436.511 297.563 435.721 281.174 C 435.287 272.158 438.756 263.392 445.242 257.115 C 452.508 250.083 462.718 246.983 472.668 248.788 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(241,25,50)" d="M 445.897 250.388 C 445.973 253.142 442.428 256.106 443.685 257.49 L 445.242 257.115 C 438.756 263.392 435.287 272.158 435.721 281.174 L 434.494 279.664 C 433.38 284.252 433.671 290.428 434.372 295.102 C 430.932 288.26 430.416 281.74 431.633 274.237 C 433.149 264.886 438.193 255.998 445.897 250.388 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 376.604 379.194 C 384.836 378.003 394.172 379.707 401.018 384.463 C 407.857 389.215 412.169 396.668 413.657 404.791 C 415.561 415.181 414.546 427.19 408.255 435.969 C 402.123 444.527 393.347 448.061 383.349 449.728 C 374.452 449.598 366.757 448.063 359.368 442.715 C 351.971 437.36 346.912 429.624 345.634 420.523 C 344.299 411.009 346.208 400.522 352.162 392.843 C 358.046 385.253 367.162 380.35 376.604 379.194 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 245.278 481.533 C 257.539 479.362 270.02 483.955 277.95 493.555 C 285.88 503.155 288.032 516.279 283.585 527.909 C 279.138 539.54 268.778 547.878 256.466 549.738 C 237.761 552.563 220.261 539.826 217.199 521.159 C 214.137 502.492 226.651 484.832 245.278 481.533 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 581.532 430.797 C 583.737 426.223 585.763 420.476 589.684 417.162 L 591.653 417.292 C 594.088 420.177 593.57 425.309 594.04 428.969 C 596.145 445.356 610.271 455.983 623.899 463.083 C 606.858 469.748 593.857 473.295 582.916 489.14 C 581.766 491.553 581.517 494.399 580.229 496.771 C 579.253 498.57 578.644 499.099 576.752 499.681 C 573.484 495.743 574.503 486.104 572.779 481.118 C 571.347 476.976 567.817 473.984 566.688 469.622 C 562.444 466.354 558.851 462.241 554.464 458.93 C 552.165 457.194 548.388 454.735 547.974 451.776 C 551.501 446.938 564.993 444.86 570.638 441.417 C 574.943 438.791 577.163 433.481 581.532 430.797 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 581.532 430.797 C 583.737 426.223 585.763 420.476 589.684 417.162 L 591.653 417.292 C 594.088 420.177 593.57 425.309 594.04 428.969 C 596.145 445.356 610.271 455.983 623.899 463.083 C 606.858 469.748 593.857 473.295 582.916 489.14 C 581.766 491.553 581.517 494.399 580.229 496.771 C 579.253 498.57 578.644 499.099 576.752 499.681 C 573.484 495.743 574.503 486.104 572.779 481.118 C 571.347 476.976 567.817 473.984 566.688 469.622 C 571.173 471.572 572.524 477.478 575.983 480.956 L 574.541 477.616 C 577.199 480.049 578.267 482.89 579.609 486.173 C 581.592 483.925 582.987 480.98 584.97 478.61 C 591.857 470.376 600.56 466.217 610.139 461.923 C 604.823 456.132 598.001 450.067 593.691 443.548 C 591.471 440.19 590.468 436.192 588.242 432.738 L 585.264 432.437 C 585.278 431.088 585.42 429.746 585.536 428.403 C 584.408 429.65 583.607 430.186 581.984 430.67 C 581.834 430.715 581.683 430.755 581.532 430.797 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" fill-opacity="0.988235" d="M 1268.53 876.747 C 1267.01 839.046 1267.99 800.953 1268.02 763.211 C 1268.04 734.464 1266.89 705.284 1267.99 676.603 L 1272.4 677.824 C 1273.89 696.863 1273.08 716.382 1273.06 735.492 C 1273.02 780.543 1271.7 825.968 1273.48 870.972 C 1287.28 871.195 1301.12 871.207 1314.9 871.753 L 1315.16 860.225 C 1317.27 848.465 1316.12 834.41 1316.08 822.353 C 1316.01 795.856 1315.21 769.119 1316.68 742.659 C 1318.34 750.784 1319.01 759.086 1319.91 767.322 C 1320.82 777.589 1320.18 788.461 1320.16 798.785 C 1320.1 824.831 1319.18 851.171 1320.3 877.184 C 1303.1 877.483 1285.72 877.43 1268.53 876.747 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" fill-opacity="0.988235" d="M 1161.87 313.52 L 1165.57 312.761 C 1163.16 322.307 1165.36 349.256 1165.36 360.697 C 1165.34 411.592 1166.05 462.605 1165.23 513.484 C 1146.99 516.978 1114.16 518.771 1096.1 514.8 L 1095.92 499.799 L 1097.35 503.129 L 1098.39 503.214 L 1098.89 498.558 L 1100.43 494.27 C 1100.77 496.759 1101.28 499.136 1101.87 501.573 C 1101.81 504.262 1101.45 507.065 1102.82 509.487 C 1115.26 511.795 1145.76 510.751 1159.14 509.002 C 1159.13 490.857 1157.91 317.848 1159.86 313.826 L 1161.87 313.52 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" fill-opacity="0.984314" d="M 1345.07 480.114 L 1344.74 478.628 C 1347.67 476.765 1349.51 473.859 1352.21 471.672 C 1357.79 467.159 1363.85 463.458 1369.98 459.756 C 1371.34 461.017 1372.58 462.337 1374.31 463.085 C 1365.98 469.573 1354.87 476.027 1345.07 480.114 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" fill-opacity="0.984314" d="M 1051.36 408.806 C 1054.89 404.149 1057.86 398.825 1060.97 393.867 L 1064.1 397.913 C 1061.59 402.37 1058.98 406.26 1055.69 410.177 L 1051.36 408.806 z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 58 KiB |
@ -1,56 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
把这个文件放在项目根目录,用来把已安装的 Codex skills 注册给 Codex。项目级约束可以追加在文末的 `Project notes` 一节。
|
||||
|
||||
## Coding 宪法
|
||||
|
||||
### 最优路径优先
|
||||
|
||||
- 先基于当前目标、约束和上下文判断最优路径,再沿着这条路径推进。
|
||||
- 默认先收敛到一个最优方案,不先为了自我防御同时铺退路、保守版、兼容版或降级版。
|
||||
- 只有在最优路径被事实阻塞、风险或成本发生实质变化、或用户明确要求方案比较时,才展开 fallback 与备选方案。
|
||||
- 提问和澄清只用于解决会改变最优路径判断的问题,不能把提问当成免责动作。
|
||||
|
||||
## Skills
|
||||
|
||||
### Available skills
|
||||
|
||||
- `rr`: 评审 RequirementsDoc.md,检查需求文档的完整性、清晰度和可执行性,输出结构化评审报告。 (file: `./.codex/skills/rr/SKILL.md`)
|
||||
- `rp`: 评审 PRD.md,对比 RequirementsDoc 检查一致性,输出结构化评审报告。 (file: `./.codex/skills/rp/SKILL.md`)
|
||||
- `rf`: 评审 FeatureSummary.md,对比 PRD 检查一致性,输出结构化评审报告。 (file: `./.codex/skills/rf/SKILL.md`)
|
||||
- `rd`: 评审 DevelopmentPlan.md,检查技术可行性和与上游文档一致性,输出结构化评审报告。 (file: `./.codex/skills/rd/SKILL.md`)
|
||||
- `ru`: 评审 UIDesign.md,对比 DevelopmentPlan 检查设计一致性,输出结构化评审报告。 (file: `./.codex/skills/ru/SKILL.md`)
|
||||
- `rt`: 评审 tasks.md,检查任务完整性和与上游文档一致性,输出结构化评审报告。 (file: `./.codex/skills/rt/SKILL.md`)
|
||||
- `wp`: 从 RequirementsDoc.md 生成 PRD.md,将需求文档转化为结构化的产品需求文档。 (file: `./.codex/skills/wp/SKILL.md`)
|
||||
- `wf`: 从 RequirementsDoc.md 和 PRD.md 生成 FeatureSummary.md,提供功能全貌概览。 (file: `./.codex/skills/wf/SKILL.md`)
|
||||
- `wd`: 从上游文档生成 DevelopmentPlan.md,包含技术方案和开发排期。 (file: `./.codex/skills/wd/SKILL.md`)
|
||||
- `wu`: 从上游文档生成 UIDesign.md,覆盖所有用户界面设计。 (file: `./.codex/skills/wu/SKILL.md`)
|
||||
- `wt`: 从上游文档生成 tasks.md,创建可直接执行的任务列表。 (file: `./.codex/skills/wt/SKILL.md`)
|
||||
- `mr`: 增量修改 RequirementsDoc.md,根据用户指令在现有内容基础上更新需求文档。 (file: `./.codex/skills/mr/SKILL.md`)
|
||||
- `mp`: 增量修改 PRD.md,根据用户指令在现有内容基础上更新产品需求文档。 (file: `./.codex/skills/mp/SKILL.md`)
|
||||
- `mf`: 增量修改 FeatureSummary.md,根据用户指令在现有内容基础上更新功能摘要。 (file: `./.codex/skills/mf/SKILL.md`)
|
||||
- `md`: 增量修改 DevelopmentPlan.md,根据用户指令在现有内容基础上更新开发计划。 (file: `./.codex/skills/md/SKILL.md`)
|
||||
- `mu`: 增量修改 UIDesign.md,根据用户指令在现有内容基础上更新 UI 设计文档。 (file: `./.codex/skills/mu/SKILL.md`)
|
||||
- `mt`: 增量修改 tasks.md,根据用户指令在现有内容基础上更新任务列表。 (file: `./.codex/skills/mt/SKILL.md`)
|
||||
- `go`: 终极执行按钮,激进模式一口气完成开发任务,兼容 0->1 和 1->100 场景。 (file: `./.codex/skills/go/SKILL.md`)
|
||||
- `iter`: 迭代变更入口,调研问题后更新 PRD.md 和 tasks.md,支持 Bug 修复、功能迭代、技术重构。 (file: `./.codex/skills/iter/SKILL.md`)
|
||||
- `doc`: 渐进式文档生成器。首次只写精炼梗概(≤300字),后续通过迭代不断完善。 (file: `./.codex/skills/doc/SKILL.md`)
|
||||
- `capture`: 复刻一次成功任务的经验,输出到用户指定目录。调用方式 `/capture <目录>` 或 `$capture <目录>`,生成只含 fenced YAML 的 Markdown 记录。 (file: `./.codex/skills/capture/SKILL.md`)
|
||||
- `update`: 收集用户反馈并更新最近使用的 skill。别名:`up`。 (file: `./.codex/skills/up/SKILL.md`)
|
||||
- `deploy`: Drone CI + 服务器 CD 全流程引导:从基础设施检查到生成配置文件到验证部署,交互式完成。 (file: `./.codex/skills/deploy/SKILL.md`)
|
||||
- `gitea`: 统一 Gitea 总入口,支持 issue 查询、issue 拆单创建、git push 和 PR 基础操作,优先从当前仓库 origin 自动识别目标仓库。 (file: `./.codex/skills/gitea/SKILL.md`)
|
||||
- `issue`: 查看当前仓库或任意 Gitea 仓库的 issue 列表和单条详情,支持自动识别 git origin、用户指定仓库和格式化输出。 (file: `./.codex/skills/issue/SKILL.md`)
|
||||
- `issue-drive`: 归集证据并把问题拆成 1 到多张 Gitea issue,支持从当前仓库 origin 自动识别仓库或用户显式指定。 (file: `./.codex/skills/issue-drive/SKILL.md`)
|
||||
- `changelog`: 一键发版:生成更新日志 → commit → 打 tag,全流程自动化。 (file: `./.codex/skills/changelog/SKILL.md`)
|
||||
|
||||
### How to use skills
|
||||
|
||||
- Discovery: 以上列表就是当前项目注册给 Codex 的 skills。
|
||||
- Trigger rules: 如果用户显式提到 skill 名称(例如 `/rr`、`$rr`、`rr skill`、`用 rr 评审`),或任务明显匹配 skill 描述,优先使用对应 skill。
|
||||
- Codex usage: 在 Codex 中优先使用 `/skill-name`;兼容历史 `$skill-name` 写法,也支持自然语言触发。
|
||||
- Missing/blocked: 如果某个 skill 文件不存在或无法读取,简短说明并回退到普通实现方式。
|
||||
- Context hygiene: 只按需打开 `SKILL.md`,不要一次性加载整个 skill 仓库。
|
||||
|
||||
## Project notes
|
||||
|
||||
- 在这里补充项目特定约束,例如技术栈、测试命令、代码风格、提交流程。
|
||||
@ -1 +0,0 @@
|
||||
just empety...
|
||||
@ -1,139 +0,0 @@
|
||||
---
|
||||
name: capture
|
||||
description: 复刻一次成功任务的经验,输出到用户指定目录。调用方式 `/capture <目录>` 或 `$capture <目录>`,生成只含 fenced YAML 的 Markdown 记录。
|
||||
---
|
||||
|
||||
# Capture - 成功经验复刻
|
||||
|
||||
> **定位**:把当前会话里已经跑通、值得复用的做法沉淀成结构化记录,供其他仓库直接复刻。
|
||||
|
||||
当用户调用 `capture` skill、`/capture <target_dir>`、`$capture <target_dir>`,或自然语言要求“把这次成功经验沉淀/复刻到某个目录”时,执行以下步骤。
|
||||
|
||||
## 1. 锁定输出目录
|
||||
|
||||
- 如果用户给了目录,直接使用。
|
||||
- 绝对路径原样使用;相对路径相对当前仓库根目录。
|
||||
- 如果没给目录,只追问一次:`要保存到哪个目录?`
|
||||
- 目录不存在就创建。
|
||||
|
||||
## 2. 收集事实
|
||||
|
||||
只从这些来源取材:
|
||||
|
||||
- 当前会话里的目标、决策、问题、修复、验证结果
|
||||
- 当前任务产物、相关文件、diff、日志
|
||||
- 明确发生过且已经验证的修复
|
||||
|
||||
不要做这些事:
|
||||
|
||||
- 不复述用户原需求
|
||||
- 不编造未知信息
|
||||
- 不把“计划中但没发生”的内容写成事实
|
||||
|
||||
## 3. 生成记录
|
||||
|
||||
### 3.1 命名
|
||||
|
||||
- `task` 用任务本质的短名字,避免 `misc`、`update-doc` 这类空名。
|
||||
- 文件名:`YYYY-MM-DD-task-slug.md`
|
||||
- 若重名,依次追加 `-2`、`-3`
|
||||
|
||||
### 3.2 内容规则
|
||||
|
||||
- 文件内容只能有一个 fenced code block,语言标记固定为 `yaml`
|
||||
- code block 外不要写任何文字
|
||||
- `raw_request` 只写归一化摘要;拿不准就留空
|
||||
- 空标量字段留空
|
||||
- 空列表字段写 `[]`,不要保留空 `-`
|
||||
- 每个 bullet 尽量控制在两行内
|
||||
- 重点写:目标、输入输出、必须正确项、风险、关键决策、问题修复、成功原因、可复用模式
|
||||
- `why_it_worked` 只写真正起作用的因素
|
||||
- `reusable_pattern` 必须可执行,优先写先做什么、看什么、按什么顺序复用
|
||||
- `problems_and_fixes` 只写实际发生且已验证的项
|
||||
|
||||
### 3.3 固定模板
|
||||
|
||||
~~~~markdown
|
||||
```yaml
|
||||
task: <task_name>
|
||||
|
||||
goal: >
|
||||
<一句话目标>
|
||||
|
||||
context:
|
||||
scene:
|
||||
trigger:
|
||||
audience:
|
||||
deadline:
|
||||
constraints:
|
||||
-
|
||||
|
||||
input:
|
||||
raw_request:
|
||||
known:
|
||||
-
|
||||
unknown:
|
||||
-
|
||||
dependencies:
|
||||
-
|
||||
|
||||
output:
|
||||
deliverable:
|
||||
consumer:
|
||||
usage:
|
||||
acceptance:
|
||||
-
|
||||
|
||||
must_be_right:
|
||||
-
|
||||
|
||||
can_be_rough:
|
||||
-
|
||||
|
||||
risks:
|
||||
-
|
||||
|
||||
decisions:
|
||||
- choice:
|
||||
reason:
|
||||
|
||||
execution:
|
||||
-
|
||||
|
||||
problems_and_fixes:
|
||||
- problem:
|
||||
symptom:
|
||||
cause:
|
||||
fix:
|
||||
status:
|
||||
|
||||
why_it_worked:
|
||||
-
|
||||
|
||||
reusable_pattern:
|
||||
first_step:
|
||||
checkpoints:
|
||||
-
|
||||
reusable_steps:
|
||||
-
|
||||
anti_patterns:
|
||||
-
|
||||
|
||||
one_line_summary: >
|
||||
<一句话总结为什么成功>
|
||||
```
|
||||
~~~~
|
||||
|
||||
如果某个列表字段没有内容,改成 `[]`。如果某个标量字段没有内容,保持为空,不要删字段。
|
||||
|
||||
## 4. 保存和回执
|
||||
|
||||
- 保存到目标目录,不覆盖同名旧文件
|
||||
- 默认不把全文再贴回对话,除非用户明确要求
|
||||
- 回执只说三件事:保存路径、任务名、提醒主人还应该准备一个单独“保存味道”的地方
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 本 skill 只沉淀记录,不修改业务代码
|
||||
- 信息不足时允许留空,但不要跳过关键字段
|
||||
- 如果任务结果并不成功或还没稳定,不要硬写成“成功经验”
|
||||
@ -1,109 +0,0 @@
|
||||
---
|
||||
name: changelog
|
||||
description: 一键发版:生成更新日志 → commit → 打 tag,全流程自动化。
|
||||
---
|
||||
|
||||
# Changelog - 一键发版
|
||||
|
||||
> **定位**:发版全流程。`/changelog 1.0.0227.1` 一个命令搞定日志生成 + commit + tag。
|
||||
|
||||
## 用法
|
||||
|
||||
```
|
||||
/changelog <version> # 例: /changelog 1.0.0227.1
|
||||
/changelog # 不传版本号则自动推断
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 1. 确定版本号
|
||||
|
||||
**有参数**:直接使用用户传入的版本号。
|
||||
|
||||
**无参数**:自动推断。读取最新 tag,bump build 号 +1。例如当前最新 `v1.1.0211.1` → 推断为 `1.1.0211.2`。若无 tag 则提示用户输入。
|
||||
|
||||
版本格式:`{major}.{minor}.{MMDD}.{build}`
|
||||
|
||||
### 2. 获取 Commits
|
||||
|
||||
```bash
|
||||
# 找到上一个 tag
|
||||
git tag --sort=-creatordate
|
||||
|
||||
# 读取 commits(从上一个 tag 到 HEAD)
|
||||
git log {prev_tag}..HEAD --oneline --no-merges
|
||||
```
|
||||
|
||||
如果没有上一个 tag,则从初始 commit 开始。
|
||||
|
||||
### 3. AI 总结
|
||||
|
||||
将 commit 列表总结为用户友好的中文更新说明:
|
||||
|
||||
| type | emoji | 说明 |
|
||||
|------|-------|------|
|
||||
| feat | ✨ | 新功能 / 新 Skill |
|
||||
| fix | 🐛 | Bug 修复 |
|
||||
| refactor | ♻️ | 代码重构 |
|
||||
| docs | 📝 | 文档更新 |
|
||||
| chore | 🔧 | 杂项维护 |
|
||||
|
||||
规则:
|
||||
- 每条说明 1 句话,简洁明了
|
||||
- 合并相关的小 commit 为一条
|
||||
- 面向用户描述(不要出现技术细节如文件名、函数名)
|
||||
- summary:一句话概括本次更新的核心主题
|
||||
|
||||
### 4. 展示草稿,等待确认
|
||||
|
||||
```
|
||||
📦 v{version} · {date}
|
||||
{summary}
|
||||
|
||||
✨ 新增 /go 执行按钮 Skill...
|
||||
📝 更新 README 文档格式...
|
||||
🐛 修复仓库 URL 配置...
|
||||
```
|
||||
|
||||
**必须等用户确认或修改后才继续**。
|
||||
|
||||
### 5. 写入文件
|
||||
|
||||
用户确认后,更新 `CHANGELOG.md`:
|
||||
|
||||
- 文件不存在时创建,头部加 `# Changelog` 标题
|
||||
- 将新条目插入已有内容**头部**(标题行之后,最新版本在前),保持现有条目不变
|
||||
- 条目格式:
|
||||
|
||||
```markdown
|
||||
## v{version} ({YYYY-MM-DD})
|
||||
|
||||
{summary}
|
||||
|
||||
- ✨ xxx
|
||||
- 🐛 xxx
|
||||
- ♻️ xxx
|
||||
```
|
||||
|
||||
### 6. Commit + Tag
|
||||
|
||||
```bash
|
||||
# Stage 变更文件
|
||||
git add CHANGELOG.md
|
||||
|
||||
# Commit
|
||||
git commit -m "release: v{version}"
|
||||
|
||||
# 打 tag
|
||||
git tag v{version}
|
||||
```
|
||||
|
||||
### 7. 完成提示
|
||||
|
||||
```
|
||||
✅ 发版完成!
|
||||
- 📝 CHANGELOG.md: 新增 v{version} 条目
|
||||
- 🏷️ git tag: v{version}
|
||||
|
||||
需要 push 到远程吗?(git push && git push --tags)
|
||||
```
|
||||
@ -1,316 +0,0 @@
|
||||
---
|
||||
name: deploy
|
||||
description: Drone CI + 服务器 CD 全流程引导:从基础设施检查到生成配置文件到验证部署,交互式完成。
|
||||
---
|
||||
|
||||
# Deploy - CI/CD 全流程部署引导
|
||||
|
||||
> **定位**:基于公司 Drone CI + 私有 Docker Registry + Docker Compose 的自动化部署方案,交互式引导用户从零完成 CI/CD 接入。
|
||||
|
||||
当用户调用 `/deploy` 或 `/deploy <指令>` 时,执行以下步骤:
|
||||
|
||||
## 1. 收集项目信息
|
||||
|
||||
快速了解项目情况(已知的不重复问):
|
||||
|
||||
| 项目 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 项目名称 | 用于镜像命名 | `douyin`, `crm`, `blog` |
|
||||
| 需构建的服务 | 每个服务对应一个镜像 | `backend`, `frontend` |
|
||||
| 各服务的 Dockerfile 路径 | Docker build context | `./backend`, `./frontend` |
|
||||
| 生产服务器 SSH 端口 | 默认 22 | `22`, `3141` |
|
||||
| 部署目录 | 生产服务器上的路径 | `/opt/docker/myproject` |
|
||||
| 数据库迁移命令 | 如有 | `alembic upgrade head`, `npx prisma migrate deploy` |
|
||||
| 健康检查方式 | 三选一 | python / curl / host |
|
||||
| 健康检查 URL | 容器内地址 | `http://127.0.0.1:8000/health` |
|
||||
| 通知 Webhook | 可选,不配则跳过 | 企业微信/钉钉/飞书 |
|
||||
|
||||
确认后进入下一步。
|
||||
|
||||
## 2. 基础设施检查
|
||||
|
||||
输出 Checklist,让用户逐项确认(首次接入需全部完成,后续项目可跳过):
|
||||
|
||||
```
|
||||
□ 基础设施(一次性,已完成则跳过)
|
||||
□ Drone CI Server + Runner 已部署运行
|
||||
□ 私有 Docker Registry 已运行(默认 :5000)
|
||||
□ insecure-registries 已配置(Drone CI 服务器 + 生产服务器)
|
||||
□ SSH 密钥已配置(Drone CI 服务器 → 生产服务器免密登录)
|
||||
□ 生产服务器用户已加入 docker 组
|
||||
```
|
||||
|
||||
如果用户表示基础设施未就绪,输出对应的一次性搭建指引(参见下方「附录:基础设施搭建」)。
|
||||
|
||||
## 3. 生成配置文件
|
||||
|
||||
基于收集到的信息,生成以下文件:
|
||||
|
||||
### 3.1 `.drone.yml`
|
||||
|
||||
核心原则(踩坑总结,不可违反):
|
||||
1. 使用 `docker:27-cli` + 宿主机 Docker socket,**不用** `plugins/docker` DinD
|
||||
2. 使用 `environment: { VAR: { from_secret: name } }` 注入密钥,**不用** `secrets:` 字段
|
||||
3. 使用 `${DRONE_TAG:-latest}` 作为镜像 tag,**不自定义中间变量**(Drone 变量替换冲突)
|
||||
4. 触发条件只用 `event: [tag, cron]`,**不叠加** `cron: [name]`(AND 运算陷阱)
|
||||
|
||||
生成内容包括:
|
||||
- `trigger`: tag + cron
|
||||
- `volumes`: 挂载宿主机 Docker socket
|
||||
- 每个服务的 `build-<service>` step
|
||||
- `deploy` step(使用 `appleboy/drone-ssh`)
|
||||
- `notify-success` / `notify-failure` step(如配置了 Webhook)
|
||||
|
||||
### 3.2 `scripts/deploy-remote.sh`
|
||||
|
||||
部署脚本要点:
|
||||
- `set -euo pipefail` 严格模式
|
||||
- 部署锁(PID 文件防并发)
|
||||
- 同时 export `IMAGE_TAG` 和 `VERSION`(兼容不同 compose 变量命名)
|
||||
- 按顺序:pull → 停 beat → 更新核心服务 → 健康检查 → 数据库迁移 → 启动剩余服务 → 最终健康检查
|
||||
- 健康检查根据用户选择的方式生成(python / curl / host)
|
||||
|
||||
### 3.3 生成后展示
|
||||
|
||||
```
|
||||
已生成:
|
||||
📄 .drone.yml — Drone CI 流水线配置
|
||||
📄 scripts/deploy-remote.sh — 远程部署脚本
|
||||
|
||||
确认写入?[Y/n]
|
||||
```
|
||||
|
||||
用户确认后写入文件。
|
||||
|
||||
## 4. Drone 面板配置引导
|
||||
|
||||
生成文件后,输出需要在 Drone 面板手动配置的清单:
|
||||
|
||||
### 4.1 仓库设置
|
||||
|
||||
```
|
||||
在 Drone 面板完成以下配置:
|
||||
|
||||
1. 激活仓库:SYNC → 找到仓库 → ACTIVATE
|
||||
2. 开启 Trusted:Settings → General → Project Settings → 勾选 Trusted
|
||||
```
|
||||
|
||||
### 4.2 Secrets 配置
|
||||
|
||||
根据收集到的信息,输出具体的 Secret 列表:
|
||||
|
||||
```
|
||||
在 Drone 面板 → 仓库 Settings → Secrets 添加:
|
||||
|
||||
| Secret 名称 | 填写内容 |
|
||||
|-------------------|----------------------------------------------------|
|
||||
| backend_repo | docker.internal.intelligrow.cn:5000/{project}-backend |
|
||||
| frontend_repo | docker.internal.intelligrow.cn:5000/{project}-frontend |
|
||||
| deploy_host | {生产服务器 IP} |
|
||||
| deploy_user | {SSH 用户} |
|
||||
| deploy_ssh_key | cat ~/.ssh/drone_deploy 的完整内容 |
|
||||
| deploy_path | {部署目录} |
|
||||
| wecom_webhook | {Webhook URL}(如已配置) |
|
||||
```
|
||||
|
||||
### 4.3 Cron 配置(可选)
|
||||
|
||||
```
|
||||
如需定时构建,在 Settings → Cron Jobs 添加:
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|----------|------------------|-------------------------|
|
||||
| Name | nightly-build | 任务名称 |
|
||||
| Branch | main | 构建分支 |
|
||||
| Schedule | 0 16 * * * | UTC 16:00 = 北京 00:00 |
|
||||
```
|
||||
|
||||
## 5. 生产服务器配置引导
|
||||
|
||||
```
|
||||
在生产服务器上确认:
|
||||
|
||||
1. 部署目录结构:
|
||||
{deploy_path}/
|
||||
├── docker-compose.prod.yml
|
||||
├── .env
|
||||
└── scripts/
|
||||
└── deploy-remote.sh ← 需从代码仓库复制
|
||||
|
||||
2. .env 至少包含:
|
||||
DOCKER_REGISTRY=docker.internal.intelligrow.cn:5000
|
||||
(其他数据库密码等生产配置)
|
||||
|
||||
3. docker-compose.prod.yml 中镜像引用格式:
|
||||
image: ${DOCKER_REGISTRY}/{project}-backend:${VERSION:-latest}
|
||||
|
||||
⚠️ 变量一致性:Drone Secret 的镜像地址前缀 = .env 的 DOCKER_REGISTRY = compose 中的镜像名拼接结果
|
||||
```
|
||||
|
||||
## 6. 验证
|
||||
|
||||
自己执行可执行的验证,不能远程执行的给出命令让用户确认结果:
|
||||
|
||||
### 6.1 本地验证(自己执行)
|
||||
|
||||
```bash
|
||||
# 检查 .drone.yml 语法合法性(YAML 解析)
|
||||
# 检查 deploy-remote.sh 语法(bash -n)
|
||||
# 检查文件是否已正确写入
|
||||
```
|
||||
|
||||
### 6.2 远程验证引导(输出命令,让用户在服务器上执行并反馈结果)
|
||||
|
||||
```bash
|
||||
# 推送 Tag 触发首次构建
|
||||
git tag v{version}
|
||||
git push origin v{version}
|
||||
|
||||
# 观察 Drone 面板 pipeline 状态
|
||||
|
||||
# 生产服务器检查
|
||||
ssh -p {port} {user}@{host} "cd {deploy_path} && docker compose -f docker-compose.prod.yml ps"
|
||||
```
|
||||
|
||||
## 7. 完成输出
|
||||
|
||||
```
|
||||
✅ CI/CD 接入完成!
|
||||
|
||||
📄 生成的文件:
|
||||
- .drone.yml
|
||||
- scripts/deploy-remote.sh
|
||||
|
||||
🔧 Drone 面板配置(需手动):
|
||||
- [x] 仓库已激活
|
||||
- [x] Trusted 已开启
|
||||
- [x] Secrets 已添加
|
||||
- [ ] Cron Job(可选)
|
||||
|
||||
🖥️ 生产服务器:
|
||||
- [ ] .env 已配置
|
||||
- [ ] deploy-remote.sh 已复制
|
||||
- [ ] 首次部署成功
|
||||
|
||||
📖 回滚方案:
|
||||
方式1: ssh 到生产服务器执行 bash scripts/deploy-remote.sh {旧版本tag}
|
||||
方式2: git tag {旧版本}-rollback {旧版本} && git push origin {旧版本}-rollback
|
||||
|
||||
主人,用不用我沉淀 or git 提交?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:基础设施搭建
|
||||
|
||||
当用户表示基础设施未就绪时,按需输出以下指引:
|
||||
|
||||
### A. Drone CI 部署
|
||||
|
||||
在 Drone CI 服务器创建 `~/drone/docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
drone-server:
|
||||
image: drone/drone:2
|
||||
container_name: drone-server
|
||||
restart: always
|
||||
ports:
|
||||
- "3080:80"
|
||||
environment:
|
||||
- DRONE_GITEA_SERVER=https://<your-gitea-domain>
|
||||
- DRONE_GITEA_CLIENT_ID=<gitea-oauth-client-id>
|
||||
- DRONE_GITEA_CLIENT_SECRET=<gitea-oauth-client-secret>
|
||||
- DRONE_SERVER_HOST=<your-drone-domain>
|
||||
- DRONE_SERVER_PROTO=https
|
||||
- DRONE_RPC_SECRET=<openssl rand -hex 16 生成>
|
||||
- DRONE_USER_CREATE=username:<gitea用户名>,admin:true
|
||||
volumes:
|
||||
- ./data:/data
|
||||
|
||||
drone-runner:
|
||||
image: drone/drone-runner-docker:1
|
||||
container_name: drone-runner
|
||||
restart: always
|
||||
depends_on:
|
||||
- drone-server
|
||||
environment:
|
||||
- DRONE_RPC_PROTO=http
|
||||
- DRONE_RPC_HOST=drone-server
|
||||
- DRONE_RPC_SECRET=<与 server 相同>
|
||||
- DRONE_RUNNER_CAPACITY=2
|
||||
- DRONE_RUNNER_NAME=drone-runner-1
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
```
|
||||
|
||||
关键注意:
|
||||
- `DRONE_RPC_PROTO=http`:Runner 走 Docker 内网直连,不走 HTTPS
|
||||
- `DRONE_USER_CREATE` 的 username 必须与 Gitea **登录用户名**完全一致(不是邮箱)
|
||||
|
||||
### B. 私有 Registry
|
||||
|
||||
```bash
|
||||
docker run -d --name registry \
|
||||
-p 5000:5000 \
|
||||
-v /opt/registry-data:/var/lib/registry \
|
||||
--restart always \
|
||||
registry:2
|
||||
```
|
||||
|
||||
### C. insecure-registries 配置
|
||||
|
||||
在 Drone CI 服务器和生产服务器的 `/etc/docker/daemon.json` 添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"insecure-registries": ["<registry-host>:5000"]
|
||||
}
|
||||
```
|
||||
|
||||
**不要带 `http://` 前缀**,直接写 `host:port`。修改后 `sudo systemctl restart docker`。
|
||||
|
||||
### D. SSH 免密
|
||||
|
||||
```bash
|
||||
# Drone CI 服务器上生成密钥
|
||||
ssh-keygen -t ed25519 -C "drone-ci-deploy" -f ~/.ssh/drone_deploy -N ""
|
||||
|
||||
# 将公钥添加到生产服务器
|
||||
ssh-copy-id -i ~/.ssh/drone_deploy.pub -p <port> <user>@<production-ip>
|
||||
|
||||
# 验证
|
||||
ssh -i ~/.ssh/drone_deploy -p <port> <user>@<production-ip> "echo ok"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 踩坑清单(生成配置时必须规避)
|
||||
|
||||
| # | 坑 | 正确做法 |
|
||||
|---|-----|---------|
|
||||
| 1 | `insecure-registries` 带 `http://` 前缀 | 直接写 `host:port` |
|
||||
| 2 | Drone `${VAR}` 与 shell 变量冲突 | 直接用 `${DRONE_TAG:-latest}`,不赋中间变量 |
|
||||
| 3 | 用 `secrets:` 字段注入 secret | 用 `environment: { VAR: { from_secret: name } }` |
|
||||
| 4 | `plugins/docker` DinD 启动失败 | 用 `docker:27-cli` + 挂载 Docker socket |
|
||||
| 5 | `DRONE_USER_CREATE` 填邮箱 | 必须填 Gitea 登录用户名 |
|
||||
| 6 | `event + cron` 触发条件互斥 | 只用 `event: [tag, cron]`,不加 `cron:` 过滤 |
|
||||
| 7 | Registry 地址不一致(IP vs 域名) | Drone Secret、`.env`、compose 三处统一 |
|
||||
| 8 | SSH 端口不对 | `appleboy/drone-ssh` 显式指定 `port` |
|
||||
| 9 | Docker 权限不足 | `sudo usermod -aG docker <user>` 后重新登录 |
|
||||
| 10 | `daemon.json` 被覆盖 | 修改前先 cat 查看,合并内容 |
|
||||
|
||||
---
|
||||
|
||||
## 故障排查速查表
|
||||
|
||||
| 现象 | 检查方向 |
|
||||
|------|---------|
|
||||
| Pipeline 不触发 | Gitea Webhook 是否勾选"创建"事件;`.drone.yml` trigger |
|
||||
| Step 一直 pending | Runner 是否连通 Server;仓库是否 Trusted |
|
||||
| 构建报 secret 为空 | `environment: from_secret` 而非 `secrets:` |
|
||||
| Docker push 失败 (HTTPS) | 两台服务器 `insecure-registries` 配置 |
|
||||
| SSH 部署超时 | 密钥是否正确;端口是否匹配;Docker 权限 |
|
||||
| 镜像名 invalid reference | `.env` 的 `DOCKER_REGISTRY` 变量是否正确 |
|
||||
| 数据库迁移失败 | `docker compose logs -f <service>` |
|
||||
| 健康检查超时 | 增大 `MAX_ATTEMPTS`;检查服务启动日志 |
|
||||
@ -1,101 +0,0 @@
|
||||
---
|
||||
name: doc
|
||||
description: 渐进式文档生成器。首次只写精炼梗概(≤300字),后续通过迭代不断完善。
|
||||
---
|
||||
|
||||
# Doc - 渐进式文档生成器
|
||||
|
||||
> **核心理念**:文档是缝缝补补长出来的,不是一步到位写出来的。首次只写最重要的梗概,后续通过讨论和迭代逐步完善。
|
||||
|
||||
当用户调用 `doc` skill、`/doc`、`$doc`,或自然语言要求“用 doc 写文档”时,执行以下步骤:
|
||||
|
||||
## 1. 理解需求
|
||||
|
||||
如果用户提供了参数,使用该描述。否则询问要写什么文档。
|
||||
|
||||
快速确认(已知的不重复问):
|
||||
|
||||
| 项目 | 默认值 |
|
||||
|------|--------|
|
||||
| 文档主题 | 用户指令 |
|
||||
| 输出路径 | 询问用户 |
|
||||
| 作者署名 | 询问用户 |
|
||||
|
||||
简短讨论文档边界:列出你理解的覆盖范围,标注不确定的点,让用户拍板。
|
||||
|
||||
> 你是执笔人,不是决策者。"写什么、不写什么"由用户决定。
|
||||
|
||||
## 2. 快速调研
|
||||
|
||||
聚焦调研,不求面面俱到:
|
||||
- 扫描相关代码和现有文档
|
||||
- 提取核心概念、关键接口、主要数据流
|
||||
- 了解项目现有文档风格(命名、格式)
|
||||
|
||||
调研完成后,用 2-3 句话告诉用户你发现了什么,有什么存疑的点。**不需要正式的大纲确认环节**,直接写梗概,快速拿到反馈比完美大纲更重要。
|
||||
|
||||
## 3. 生成梗概
|
||||
|
||||
### 3.1 写作原则
|
||||
|
||||
**首次写作(默认模式)**:
|
||||
- **言简意赅**,建议控制在 300 字以内
|
||||
- 只写最重要的骨架:是什么、为什么、怎么用
|
||||
- 留白是刻意的,后续迭代会填充细节
|
||||
- 不需要面面俱到,抓住核心价值
|
||||
|
||||
**迭代补充**(用户再次调用 `doc` 指向同一文件时):
|
||||
- 读取现有内容,在此基础上增量补充
|
||||
- 递增版本号,更新 `updated` 日期
|
||||
- 每次迭代聚焦一个方面,不要一次补太多
|
||||
|
||||
### 3.2 文档头部
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: {文档标题}
|
||||
version: v1.0
|
||||
created: {YYYY-MM-DD}
|
||||
updated: {YYYY-MM-DD}
|
||||
author: {作者署名}
|
||||
---
|
||||
```
|
||||
|
||||
### 3.3 内容要求
|
||||
|
||||
- **准确**:基于实际代码,不编造
|
||||
- **精炼**:一句话能说清的不用两句
|
||||
- **实用**:面向读者,提供可操作的信息
|
||||
- 善用表格、代码块、ASCII 图示(但首次不强求)
|
||||
|
||||
## 4. 保存并输出
|
||||
|
||||
保存到用户指定路径。如果文件已存在,先询问是覆盖还是增量更新。
|
||||
|
||||
输出摘要:
|
||||
|
||||
```text
|
||||
文档: {标题} | 版本: v1.0 | 路径: {path}
|
||||
字数: ~{N}字(首版梗概)
|
||||
|
||||
后续可以通过 /doc 继续补充完善。
|
||||
主人,用不用我沉淀 or git 提交?
|
||||
```
|
||||
|
||||
## 工作流总览
|
||||
|
||||
```text
|
||||
/doc <指令>
|
||||
│
|
||||
├── 1. 理解需求(简短确认主题、路径、署名)
|
||||
├── 2. 快速调研(聚焦核心,不求全面)
|
||||
├── 3. 生成梗概(≤300字,抓骨架)
|
||||
└── 4. 保存输出(鼓励后续迭代)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **少即是多**:首版宁短勿长,300 字是指导建议而非硬限制
|
||||
- **鼓励迭代**:每次 `doc` 都是一次对话机会,文档在讨论中成长
|
||||
- **不做代码改动**:本 skill 只生成文档
|
||||
- **风格一致**:与项目已有文档风格保持一致
|
||||
@ -1,144 +0,0 @@
|
||||
---
|
||||
name: gitea
|
||||
description: 统一 Gitea 总入口,支持 issue 查询、issue 拆单创建、git push 和 PR 基础操作,优先从当前仓库 origin 自动识别目标仓库。
|
||||
---
|
||||
|
||||
# Gitea - 统一入口
|
||||
|
||||
> **定位**:这是 Gitea 总入口。适合“看 issue”“拆 issue”“把提交 push 上去”“给当前分支开 PR”“在 PR 里留言”这类场景。
|
||||
>
|
||||
> `issue` 和 `issue-drive` 继续保留:`issue` 负责只读查看,`issue-drive` 负责拆单和创建工单;`gitea` 负责统一路由和 push / PR 能力。
|
||||
|
||||
当用户调用 `/gitea`、`$gitea`,或自然语言要求“处理 Gitea / push / PR / issue”时,按以下规则执行。
|
||||
|
||||
## 1. 先路由意图
|
||||
|
||||
先从用户输入里判断目标动作:
|
||||
|
||||
- 包含“看 / 查 / list / open / closed / #123 / issue”,且没有“拆 / 建 / 提 / 创建工单”语义:走 `issue`
|
||||
- 包含“拆 issue / 提 issue / 创建工单 / 沉淀问题”语义:走 `issue-drive`
|
||||
- 包含“push / 推送 / tag 推上去 / 推当前分支”语义:走 push 流程
|
||||
- 包含“PR / pull request / 拉请求 / 合并请求 / 评论 PR”语义:走 PR 流程
|
||||
|
||||
如果一句话里同时包含多个动作,先按用户描述顺序执行;默认不要自作主张串更多步骤。
|
||||
|
||||
## 2. 环境变量
|
||||
|
||||
统一使用:
|
||||
|
||||
- `GITEA_TOKEN`:必需
|
||||
- `GITEA_BASE_URL`:可选;当传 `owner/repo` 或当前仓库 `origin` 是 SSH 地址时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,先提示用户配置后停止,不继续执行 API、push fallback 或 PR 操作。
|
||||
|
||||
## 3. Issue 路由
|
||||
|
||||
如果判定为只读 issue:
|
||||
|
||||
- 直接执行 `issue` skill 的流程
|
||||
- 保持它的输入规则:当前仓库自动识别、支持 `owner/repo`、支持完整仓库 URL、支持单条 issue 编号
|
||||
- 不在 `gitea` 里重复抄写 `issue` 的长说明
|
||||
|
||||
如果判定为 issue 拆单 / 创建:
|
||||
|
||||
- 直接执行 `issue-drive` 的流程
|
||||
- 先整理事实和证据,再创建 issue
|
||||
- 创建时统一使用 `GITEA_TOKEN`
|
||||
|
||||
## 4. Push 流程
|
||||
|
||||
Push 一律先跑预检,再决定是否执行:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/gitea/scripts/push_gitea.py
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 默认推当前分支到同名远端分支
|
||||
- `ahead=0` 时直接告诉用户“没有可推送提交”
|
||||
- `dirty=true` 时明确提示“只会推送已提交内容”
|
||||
- 分支 diverged 时停止,不自动 rebase,也不自动 force
|
||||
- 只有用户明确要求“强推 / force push”时,才带 `--force`
|
||||
|
||||
真正执行时:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py --execute
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/gitea/scripts/push_gitea.py --execute
|
||||
```
|
||||
|
||||
若用户明确要求强推:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/gitea/scripts/push_gitea.py --execute --force
|
||||
```
|
||||
|
||||
执行策略:
|
||||
|
||||
- 先尝试 `git push origin branch:branch`
|
||||
- 若远端鉴权失败,再使用 `GITEA_TOKEN` 调 `/api/v1/user` 获取登录名,构造一次性 HTTPS token URL 进行 fallback
|
||||
- 不改写本地 `origin`
|
||||
|
||||
## 5. PR 流程
|
||||
|
||||
PR 只做三件事:`list`、`create`、`comment`。
|
||||
|
||||
### 5.1 看 PR
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py list --state=open --limit=20
|
||||
```
|
||||
|
||||
### 5.2 开 PR
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py create --base main --title "标题" --body "正文"
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `head` 默认当前分支
|
||||
- `base` 默认远端默认分支;识别不到时再显式传 `--base`
|
||||
- 如果当前分支还没推送到远端,脚本会先走 push 流程,再创建 PR
|
||||
- v1 只支持同仓库 PR,不处理 fork 间 PR
|
||||
|
||||
### 5.3 在 PR 里留言
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/gitea/scripts/pr_gitea.py comment 12 --body "已完成回归"
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- PR 评论走 issue comment API,因为 Gitea PR 底层复用 issue 线程
|
||||
|
||||
## 6. 输出要求
|
||||
|
||||
- issue 查询:沿用 `issue` skill 的固定 Markdown 输出
|
||||
- issue 创建:沿用 `issue-drive` 的创建结果输出
|
||||
- push:输出预检 JSON 或执行结果 JSON,至少包含 `status`、`branch`、`ahead/behind`、是否执行、执行方式
|
||||
- PR:输出 PR 列表或单条创建 / 评论结果 JSON,至少包含编号、标题、URL、状态
|
||||
|
||||
## 7. 用法示例
|
||||
|
||||
```bash
|
||||
/gitea 看当前仓库 open issues
|
||||
/gitea 查 issue 17
|
||||
/gitea 把这个 bug 拆成两张 issue
|
||||
/gitea push
|
||||
/gitea 推送当前分支到远端
|
||||
/gitea 给当前分支开 PR 到 main
|
||||
/gitea 看当前仓库 open PR
|
||||
/gitea 在 PR 12 留言:已完成回归
|
||||
```
|
||||
@ -1,334 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared helpers for Gitea skill scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
|
||||
SSH_RE = re.compile(r"^git@(?P<host>[^:]+):(?P<path>.+?)(?:\.git)?/?$")
|
||||
REPO_PATH_RE = re.compile(r"^(?P<owner>[^/]+)/(?P<repo>[^/]+)$")
|
||||
|
||||
|
||||
class GitCommandError(RuntimeError):
|
||||
"""Raised when a git command fails."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepoContext:
|
||||
origin: str
|
||||
owner: str
|
||||
repo: str
|
||||
repo_url: str
|
||||
remote_name: str = "origin"
|
||||
remote_url: str | None = None
|
||||
|
||||
|
||||
def normalize_base_url(base_url: str) -> str:
|
||||
return base_url.rstrip("/")
|
||||
|
||||
|
||||
def ensure_git_repo() -> None:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--is-inside-work-tree"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0 or result.stdout.strip() != "true":
|
||||
raise SystemExit("Current directory is not a git repository.")
|
||||
|
||||
|
||||
def run_git(args: list[str], cwd: str | None = None, check: bool = True) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
detail = result.stderr.strip() or result.stdout.strip() or "unknown git error"
|
||||
raise GitCommandError(f"git {' '.join(args)} failed: {detail}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def parse_http_repo_url(repo_url: str) -> tuple[str, str, str, str]:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
if prefix:
|
||||
origin = f"{origin}/{prefix}"
|
||||
normalized_repo_url = f"{origin}/{owner}/{repo}"
|
||||
return origin, owner, repo, normalized_repo_url
|
||||
|
||||
|
||||
def parse_repo_target(
|
||||
repo_target: str,
|
||||
base_url: str | None = None,
|
||||
) -> tuple[str, str, str, str]:
|
||||
value = repo_target.strip()
|
||||
if not value:
|
||||
raise SystemExit("Repo target cannot be empty.")
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return parse_http_repo_url(value)
|
||||
|
||||
if value.startswith("ssh://"):
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit(
|
||||
"Invalid SSH repo URL. Expected ssh://git@host/owner/repo.git"
|
||||
)
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
host = parsed.hostname
|
||||
if not host:
|
||||
raise SystemExit("Invalid SSH repo URL. Missing host.")
|
||||
origin = (
|
||||
normalize_base_url(base_url)
|
||||
if base_url
|
||||
else f"https://{host}{f'/{prefix}' if prefix else ''}"
|
||||
)
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
ssh_match = SSH_RE.match(value)
|
||||
if ssh_match:
|
||||
path = ssh_match.group("path").rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit(
|
||||
"Invalid SSH repo target. Expected git@host:owner/repo.git"
|
||||
)
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = (
|
||||
normalize_base_url(base_url)
|
||||
if base_url
|
||||
else f"https://{ssh_match.group('host')}{f'/{prefix}' if prefix else ''}"
|
||||
)
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
path_match = REPO_PATH_RE.match(value)
|
||||
if path_match:
|
||||
if not base_url:
|
||||
raise SystemExit(
|
||||
"Repo shorthand owner/repo requires GITEA_BASE_URL or a full repo URL."
|
||||
)
|
||||
origin = normalize_base_url(base_url)
|
||||
owner = path_match.group("owner")
|
||||
repo = path_match.group("repo")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
raise SystemExit(
|
||||
"Invalid repo target. Use https://host/owner/repo, git@host:owner/repo.git, "
|
||||
"ssh://git@host/owner/repo.git, or owner/repo with GITEA_BASE_URL."
|
||||
)
|
||||
|
||||
|
||||
def get_remote_url(remote: str = "origin") -> str:
|
||||
ensure_git_repo()
|
||||
try:
|
||||
return run_git(["remote", "get-url", remote])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(f"Failed to read git remote '{remote}': {exc}") from exc
|
||||
|
||||
|
||||
def resolve_repo(
|
||||
repo_url: str | None = None,
|
||||
repo: str | None = None,
|
||||
remote: str = "origin",
|
||||
) -> RepoContext:
|
||||
base_url = os.getenv("GITEA_BASE_URL")
|
||||
remote_url = None
|
||||
target = repo_url or repo
|
||||
if not target:
|
||||
remote_url = get_remote_url(remote)
|
||||
target = remote_url
|
||||
origin, owner, repo_name, normalized_repo_url = parse_repo_target(
|
||||
target,
|
||||
base_url=base_url,
|
||||
)
|
||||
return RepoContext(
|
||||
origin=origin,
|
||||
owner=owner,
|
||||
repo=repo_name,
|
||||
repo_url=normalized_repo_url,
|
||||
remote_name=remote,
|
||||
remote_url=remote_url,
|
||||
)
|
||||
|
||||
|
||||
def api_base(context: RepoContext) -> str:
|
||||
return f"{context.origin}/api/v1/repos/{context.owner}/{context.repo}"
|
||||
|
||||
|
||||
def load_token(required: bool = True) -> str:
|
||||
token = os.getenv("GITEA_TOKEN", "").strip()
|
||||
if not token and required:
|
||||
raise SystemExit("Missing GITEA_TOKEN. Export it before using Gitea workflows.")
|
||||
return token
|
||||
|
||||
|
||||
def request_json(
|
||||
url: str,
|
||||
token: str,
|
||||
method: str = "GET",
|
||||
payload: dict | None = None,
|
||||
) -> dict | list:
|
||||
data = None
|
||||
headers = {"Accept": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace")
|
||||
raise SystemExit(
|
||||
f"Gitea API request failed: {exc.code} {exc.reason} | {body}"
|
||||
) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise SystemExit(f"Failed to reach Gitea API: {exc.reason}") from exc
|
||||
|
||||
|
||||
def get_current_user_login(origin: str, token: str) -> str:
|
||||
data = request_json(f"{origin}/api/v1/user", token)
|
||||
if not isinstance(data, dict):
|
||||
raise SystemExit("Unexpected user API response.")
|
||||
login = str(data.get("login") or "").strip()
|
||||
if not login:
|
||||
raise SystemExit("Failed to resolve current Gitea user login.")
|
||||
return login
|
||||
|
||||
|
||||
def current_branch() -> str:
|
||||
ensure_git_repo()
|
||||
try:
|
||||
branch = run_git(["symbolic-ref", "--quiet", "--short", "HEAD"])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(
|
||||
"Detached HEAD. Checkout a branch before running push or PR actions."
|
||||
) from exc
|
||||
if not branch:
|
||||
raise SystemExit("Failed to determine current git branch.")
|
||||
return branch
|
||||
|
||||
|
||||
def get_upstream(branch: str) -> str | None:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"rev-parse",
|
||||
"--abbrev-ref",
|
||||
"--symbolic-full-name",
|
||||
f"{branch}@{{upstream}}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
upstream = result.stdout.strip()
|
||||
return upstream or None
|
||||
|
||||
|
||||
def remote_branch_sha(remote: str, branch: str) -> str | None:
|
||||
try:
|
||||
output = run_git(["ls-remote", "--heads", remote, branch])
|
||||
except GitCommandError as exc:
|
||||
raise SystemExit(f"Failed to query remote branch '{remote}/{branch}': {exc}") from exc
|
||||
if not output:
|
||||
return None
|
||||
return output.split()[0]
|
||||
|
||||
|
||||
def ahead_behind(compare_ref: str) -> tuple[int, int]:
|
||||
output = run_git(["rev-list", "--left-right", "--count", f"{compare_ref}...HEAD"])
|
||||
parts = output.split()
|
||||
if len(parts) != 2:
|
||||
raise SystemExit(f"Unexpected rev-list output: {output}")
|
||||
behind, ahead = (int(part) for part in parts)
|
||||
return behind, ahead
|
||||
|
||||
|
||||
def worktree_changes() -> list[str]:
|
||||
output = run_git(["status", "--short"])
|
||||
return [line for line in output.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def remote_default_branch(remote: str = "origin") -> str:
|
||||
try:
|
||||
output = run_git(["symbolic-ref", "--quiet", "--short", f"refs/remotes/{remote}/HEAD"])
|
||||
if output.startswith(f"{remote}/"):
|
||||
return output.split("/", 1)[1]
|
||||
except GitCommandError:
|
||||
pass
|
||||
|
||||
for candidate in ("main", "master"):
|
||||
if remote_branch_sha(remote, candidate):
|
||||
return candidate
|
||||
raise SystemExit(
|
||||
f"Failed to infer default branch for remote '{remote}'. Pass --base explicitly."
|
||||
)
|
||||
|
||||
|
||||
def build_authenticated_push_url(repo_url: str, username: str, token: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
path = parsed.path.rstrip("/")
|
||||
if not path.endswith(".git"):
|
||||
path = f"{path}.git"
|
||||
netloc = (
|
||||
f"{urllib.parse.quote(username, safe='')}:"
|
||||
f"{urllib.parse.quote(token, safe='')}@{parsed.netloc}"
|
||||
)
|
||||
return urllib.parse.urlunsplit((parsed.scheme or "https", netloc, path, "", ""))
|
||||
|
||||
|
||||
def mask_url(url: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
if "@" not in parsed.netloc:
|
||||
return url
|
||||
_, host = parsed.netloc.rsplit("@", 1)
|
||||
return urllib.parse.urlunsplit((parsed.scheme, f"***@{host}", parsed.path, "", ""))
|
||||
|
||||
|
||||
def is_auth_error(stderr: str) -> bool:
|
||||
message = stderr.lower()
|
||||
indicators = (
|
||||
"authentication failed",
|
||||
"permission denied",
|
||||
"could not read username",
|
||||
"could not read password",
|
||||
"http basic: access denied",
|
||||
"access denied",
|
||||
"unauthorized",
|
||||
)
|
||||
return any(indicator in message for indicator in indicators)
|
||||
@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List, create, and comment on Gitea pull requests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
from common import (
|
||||
api_base,
|
||||
current_branch,
|
||||
load_token,
|
||||
remote_default_branch,
|
||||
request_json,
|
||||
resolve_repo,
|
||||
)
|
||||
from push_gitea import compute_push_plan, execute_push
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="List, create, and comment on Gitea pull requests."
|
||||
)
|
||||
parser.add_argument("--repo-url", help="Explicit target repo URL.")
|
||||
parser.add_argument("--repo", help="Shorthand owner/repo. Requires GITEA_BASE_URL.")
|
||||
parser.add_argument("--remote", default="origin", help="Git remote name. Default: origin")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
list_parser = subparsers.add_parser("list", help="List pull requests.")
|
||||
list_parser.add_argument(
|
||||
"--state",
|
||||
default="open",
|
||||
choices=("open", "closed", "all"),
|
||||
help="Pull request state filter.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--limit",
|
||||
default=20,
|
||||
type=int,
|
||||
help="Maximum number of pull requests to return.",
|
||||
)
|
||||
|
||||
create_parser = subparsers.add_parser("create", help="Create a pull request.")
|
||||
create_parser.add_argument("--base", help="Base branch. Default: remote default branch.")
|
||||
create_parser.add_argument("--head", help="Head branch. Default: current branch.")
|
||||
create_parser.add_argument("--title", required=True, help="Pull request title.")
|
||||
create_parser.add_argument("--body", default="", help="Pull request body.")
|
||||
|
||||
comment_parser = subparsers.add_parser("comment", help="Comment on a pull request.")
|
||||
comment_parser.add_argument("pr", type=int, help="Pull request number.")
|
||||
comment_parser.add_argument("--body", required=True, help="Comment body.")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def summarize_pull_request(item: dict) -> dict:
|
||||
user = item.get("user") or {}
|
||||
head = item.get("head") or {}
|
||||
base = item.get("base") or {}
|
||||
return {
|
||||
"number": item.get("number"),
|
||||
"title": item.get("title"),
|
||||
"state": item.get("state"),
|
||||
"html_url": item.get("html_url"),
|
||||
"author": user.get("full_name") or user.get("login"),
|
||||
"head": head.get("ref"),
|
||||
"base": base.get("ref"),
|
||||
"created_at": item.get("created_at"),
|
||||
"updated_at": item.get("updated_at"),
|
||||
}
|
||||
|
||||
|
||||
def list_pull_requests(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
url = f"{api_base(context)}/pulls?state={args.state}&page=1&limit={args.limit}"
|
||||
data = request_json(url, token)
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit("Unexpected pull request list response.")
|
||||
return {
|
||||
"action": "list",
|
||||
"repo_url": context.repo_url,
|
||||
"state": args.state,
|
||||
"limit": args.limit,
|
||||
"count": len(data),
|
||||
"pull_requests": [summarize_pull_request(item) for item in data],
|
||||
}
|
||||
|
||||
|
||||
def create_pull_request(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
head = args.head or current_branch()
|
||||
base = args.base or remote_default_branch(args.remote)
|
||||
|
||||
push_plan = compute_push_plan(
|
||||
repo_url=args.repo_url,
|
||||
repo=args.repo,
|
||||
remote=args.remote,
|
||||
branch=head,
|
||||
force=False,
|
||||
)
|
||||
push_result = None
|
||||
if push_plan["needs_push"]:
|
||||
push_result = execute_push(push_plan, force=False)
|
||||
elif push_plan["status"] == "blocked":
|
||||
raise SystemExit(
|
||||
"Cannot create PR because the current branch has diverged from the remote branch."
|
||||
)
|
||||
|
||||
payload = {"base": base, "head": head, "title": args.title, "body": args.body}
|
||||
created = request_json(f"{api_base(context)}/pulls", token, method="POST", payload=payload)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected pull request creation response.")
|
||||
return {
|
||||
"action": "create",
|
||||
"repo_url": context.repo_url,
|
||||
"base": base,
|
||||
"head": head,
|
||||
"push": push_result,
|
||||
"pull_request": summarize_pull_request(created),
|
||||
}
|
||||
|
||||
|
||||
def comment_pull_request(args: argparse.Namespace) -> dict:
|
||||
context = resolve_repo(repo_url=args.repo_url, repo=args.repo, remote=args.remote)
|
||||
token = load_token(required=True)
|
||||
payload = {"body": args.body}
|
||||
created = request_json(
|
||||
f"{api_base(context)}/issues/{args.pr}/comments",
|
||||
token,
|
||||
method="POST",
|
||||
payload=payload,
|
||||
)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected PR comment response.")
|
||||
user = created.get("user") or {}
|
||||
return {
|
||||
"action": "comment",
|
||||
"repo_url": context.repo_url,
|
||||
"pull_request": args.pr,
|
||||
"comment": {
|
||||
"id": created.get("id"),
|
||||
"html_url": created.get("html_url"),
|
||||
"author": user.get("full_name") or user.get("login"),
|
||||
"created_at": created.get("created_at"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.command == "list":
|
||||
payload = list_pull_requests(args)
|
||||
elif args.command == "create":
|
||||
payload = create_pull_request(args)
|
||||
else:
|
||||
payload = comment_pull_request(args)
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,236 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preflight and execute git push with optional Gitea token fallback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from common import (
|
||||
build_authenticated_push_url,
|
||||
current_branch,
|
||||
ensure_git_repo,
|
||||
get_current_user_login,
|
||||
get_upstream,
|
||||
is_auth_error,
|
||||
load_token,
|
||||
mask_url,
|
||||
remote_branch_sha,
|
||||
resolve_repo,
|
||||
worktree_changes,
|
||||
ahead_behind,
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Preflight and optionally push the current branch to Gitea."
|
||||
)
|
||||
parser.add_argument("--repo-url", help="Explicit target repo URL.")
|
||||
parser.add_argument("--repo", help="Shorthand owner/repo. Requires GITEA_BASE_URL.")
|
||||
parser.add_argument("--remote", default="origin", help="Git remote name. Default: origin")
|
||||
parser.add_argument("--branch", help="Branch to push. Default: current branch")
|
||||
parser.add_argument(
|
||||
"--execute",
|
||||
action="store_true",
|
||||
help="Run git push after preflight instead of only printing the plan.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Allow force push. Uses --force-with-lease under the hood.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def build_push_command(target: str, branch: str, force: bool) -> list[str]:
|
||||
command = ["git", "push"]
|
||||
if force:
|
||||
command.append("--force-with-lease")
|
||||
command.extend([target, f"{branch}:{branch}"])
|
||||
return command
|
||||
|
||||
|
||||
def command_text(command: list[str]) -> str:
|
||||
return " ".join(shlex.quote(part) for part in command)
|
||||
|
||||
|
||||
def compute_push_plan(
|
||||
repo_url: str | None = None,
|
||||
repo: str | None = None,
|
||||
remote: str = "origin",
|
||||
branch: str | None = None,
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
ensure_git_repo()
|
||||
context = resolve_repo(repo_url=repo_url, repo=repo, remote=remote)
|
||||
branch_name = branch or current_branch()
|
||||
dirty_lines = worktree_changes()
|
||||
upstream = get_upstream(branch_name)
|
||||
compare_ref = upstream
|
||||
remote_exists = True
|
||||
ahead = 0
|
||||
behind = 0
|
||||
|
||||
if upstream:
|
||||
behind, ahead = ahead_behind(upstream)
|
||||
else:
|
||||
remote_sha = remote_branch_sha(remote, branch_name)
|
||||
if remote_sha:
|
||||
compare_ref = f"{remote}/{branch_name}"
|
||||
behind, ahead = ahead_behind(remote_sha)
|
||||
else:
|
||||
remote_exists = False
|
||||
compare_ref = None
|
||||
ahead = None
|
||||
behind = 0
|
||||
|
||||
diverged = bool(remote_exists and behind > 0 and (ahead or 0) > 0)
|
||||
behind_only = bool(remote_exists and behind > 0 and (ahead or 0) == 0)
|
||||
needs_push = (not remote_exists) or ((ahead or 0) > 0)
|
||||
|
||||
status = "ready"
|
||||
if diverged and not force:
|
||||
status = "blocked"
|
||||
elif not needs_push:
|
||||
status = "noop"
|
||||
|
||||
messages: list[str] = []
|
||||
if dirty_lines:
|
||||
messages.append("Working tree is dirty; push will include only committed changes.")
|
||||
if not remote_exists:
|
||||
messages.append("Remote branch does not exist yet; push will create it.")
|
||||
if behind_only:
|
||||
messages.append("Local branch is behind the remote branch; there are no local commits to push.")
|
||||
if diverged and not force:
|
||||
messages.append("Branch has diverged from remote; rerun with explicit force only if that is intended.")
|
||||
|
||||
push_command = build_push_command(remote, branch_name, force)
|
||||
fallback_command = None
|
||||
token_available = bool(load_token(required=False))
|
||||
if token_available:
|
||||
try:
|
||||
token = load_token(required=False)
|
||||
login = get_current_user_login(context.origin, token)
|
||||
auth_url = build_authenticated_push_url(context.repo_url, login, token)
|
||||
fallback_command = build_push_command(auth_url, branch_name, force)
|
||||
except SystemExit as exc:
|
||||
messages.append(f"Failed to prepare HTTPS token fallback: {exc}")
|
||||
|
||||
return {
|
||||
"repo_url": context.repo_url,
|
||||
"origin": context.origin,
|
||||
"owner": context.owner,
|
||||
"repo": context.repo,
|
||||
"remote": remote,
|
||||
"branch": branch_name,
|
||||
"upstream": upstream,
|
||||
"compare_ref": compare_ref,
|
||||
"remote_branch_exists": remote_exists,
|
||||
"dirty": bool(dirty_lines),
|
||||
"dirty_paths": dirty_lines,
|
||||
"ahead": ahead,
|
||||
"behind": behind,
|
||||
"diverged": diverged,
|
||||
"behind_only": behind_only,
|
||||
"needs_push": needs_push,
|
||||
"status": status,
|
||||
"messages": messages,
|
||||
"push_command": command_text(push_command),
|
||||
"fallback_push_command": command_text(
|
||||
[
|
||||
*(fallback_command[:-2] if fallback_command else []),
|
||||
mask_url(fallback_command[-2]) if fallback_command else "",
|
||||
*(fallback_command[-1:] if fallback_command else []),
|
||||
]
|
||||
)
|
||||
if fallback_command
|
||||
else None,
|
||||
}
|
||||
|
||||
|
||||
def run_command(command: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(command, capture_output=True, text=True)
|
||||
|
||||
|
||||
def execute_push(plan: dict, force: bool = False) -> dict:
|
||||
if plan["status"] == "noop":
|
||||
return {
|
||||
"status": "noop",
|
||||
"executed": False,
|
||||
"reason": "nothing_to_push",
|
||||
"messages": plan["messages"],
|
||||
}
|
||||
if plan["diverged"] and not force:
|
||||
raise SystemExit(
|
||||
"Branch has diverged from remote. Refusing to push without explicit --force."
|
||||
)
|
||||
|
||||
primary_command = build_push_command(plan["remote"], plan["branch"], force)
|
||||
primary_result = run_command(primary_command)
|
||||
if primary_result.returncode == 0:
|
||||
return {
|
||||
"status": "pushed",
|
||||
"executed": True,
|
||||
"method": "remote",
|
||||
"command": command_text(primary_command),
|
||||
"stdout": primary_result.stdout.strip(),
|
||||
"stderr": primary_result.stderr.strip(),
|
||||
}
|
||||
|
||||
primary_stderr = primary_result.stderr.strip() or primary_result.stdout.strip()
|
||||
if not is_auth_error(primary_stderr):
|
||||
raise SystemExit(f"git push failed: {primary_stderr}")
|
||||
|
||||
token = load_token(required=True)
|
||||
login = get_current_user_login(plan["origin"], token)
|
||||
auth_url = build_authenticated_push_url(plan["repo_url"], login, token)
|
||||
fallback_command = build_push_command(auth_url, plan["branch"], force)
|
||||
fallback_result = run_command(fallback_command)
|
||||
if fallback_result.returncode == 0:
|
||||
return {
|
||||
"status": "pushed",
|
||||
"executed": True,
|
||||
"method": "https-token",
|
||||
"command": command_text(
|
||||
[
|
||||
*(fallback_command[:-2]),
|
||||
mask_url(fallback_command[-2]),
|
||||
fallback_command[-1],
|
||||
]
|
||||
),
|
||||
"stdout": fallback_result.stdout.strip(),
|
||||
"stderr": fallback_result.stderr.strip(),
|
||||
}
|
||||
|
||||
fallback_stderr = fallback_result.stderr.strip() or fallback_result.stdout.strip()
|
||||
raise SystemExit(
|
||||
"git push failed with remote auth and HTTPS token fallback: "
|
||||
f"{fallback_stderr}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
plan = compute_push_plan(
|
||||
repo_url=args.repo_url,
|
||||
repo=args.repo,
|
||||
remote=args.remote,
|
||||
branch=args.branch,
|
||||
force=args.force,
|
||||
)
|
||||
if not args.execute:
|
||||
print(json.dumps(plan, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
result = execute_push(plan, force=args.force)
|
||||
payload = {"plan": plan, "result": result}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,331 +0,0 @@
|
||||
---
|
||||
name: go
|
||||
description: 终极执行按钮,激进模式一口气完成开发任务,兼容 0->1 和 1->100 场景。
|
||||
---
|
||||
|
||||
# Go - 发射按钮
|
||||
|
||||
> **定位**:执行按钮。无论是从零开始的 0->1,还是迭代优化的 1->100,按下 `/go` 就开始干活,不要停。
|
||||
|
||||
当用户调用 `/go` 或 `/go <任务范围>` 时,执行以下步骤:
|
||||
|
||||
## 1. 前置检查
|
||||
|
||||
### 1.1 必要文档检查
|
||||
|
||||
检查以下文件是否存在:
|
||||
|
||||
| 文件 | 必要性 | 用途 |
|
||||
|------|--------|------|
|
||||
| `doc/tasks.md` | **必须** | 任务清单,执行的圣经 |
|
||||
| `doc/PRD.md` | **必须** | 产品需求,理解业务 |
|
||||
| `doc/FeatureSummary.md` | 建议 | 功能契约 |
|
||||
| `doc/DevelopmentPlan.md` | 建议 | 技术方案 |
|
||||
| `doc/UIDesign.md` | 可选 | 界面设计 |
|
||||
|
||||
**缺少必要文档时**:
|
||||
|
||||
```
|
||||
❌ 缺少必要文档:
|
||||
- doc/tasks.md (必须)
|
||||
- doc/PRD.md (必须)
|
||||
|
||||
请先准备这些文档,或运行:
|
||||
- /wp 生成 PRD
|
||||
- /wt 生成 tasks
|
||||
```
|
||||
|
||||
### 1.2 读取所有可用文档
|
||||
|
||||
读取存在的所有文档,建立完整上下文。
|
||||
|
||||
## 2. 智能判断执行范围
|
||||
|
||||
### 2.1 检测项目状态
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 项目状态检测 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 检查 src/ 或主代码目录是否存在? │
|
||||
│ │
|
||||
│ ├── 不存在 ──▶ 0->1 模式(全新项目) │
|
||||
│ │ │
|
||||
│ └── 存在 ──▶ 检查 tasks.md 中的 ITER 标记 │
|
||||
│ │ │
|
||||
│ ├── 有 ITER 标记 ──▶ 1->100 模式 │
|
||||
│ │ │
|
||||
│ └── 无 ITER 标记 ──▶ 继续未完成任务 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 确定任务范围
|
||||
|
||||
**用户指定范围**:
|
||||
|
||||
```bash
|
||||
/go T-005 # 执行单个任务
|
||||
/go T-005~T-010 # 执行任务范围
|
||||
/go T-005 T-008 # 执行多个指定任务
|
||||
```
|
||||
|
||||
**自动判断范围**:
|
||||
|
||||
| 场景 | 执行范围 |
|
||||
|------|----------|
|
||||
| 0->1 全新项目 | tasks.md 中的所有任务,从 T-001 开始 |
|
||||
| 1->100 有 ITER 标记 | 优先执行 `<!-- ITER: -->` 标记的新任务 |
|
||||
| 1->100 无 ITER 标记 | 执行所有状态为 pending/todo 的任务 |
|
||||
|
||||
### 2.3 向用户确认范围(唯一一次交互)
|
||||
|
||||
```
|
||||
检测到项目状态:{0->1 全新项目 / 1->100 迭代项目}
|
||||
|
||||
即将执行任务:
|
||||
- T-001: {任务名}
|
||||
- T-002: {任务名}
|
||||
- ...
|
||||
- T-xxx: {任务名}
|
||||
|
||||
共 X 个任务。确认执行?[Y/n]
|
||||
```
|
||||
|
||||
**用户确认后,不再有任何交互,直到全部完成。**
|
||||
|
||||
## 3. 激进模式执行
|
||||
|
||||
### 3.1 执行原则
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 激进模式执行原则 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. 以 tasks.md 为圣经,严格按顺序执行 │
|
||||
│ │
|
||||
│ 2. 先判断当前约束下的最优路径,再沿该路径推进 │
|
||||
│ │
|
||||
│ 3. 不先为自我防御铺退路,必要时才展开 fallback │
|
||||
│ │
|
||||
│ 4. 遇到问题先沿最优路径自主修复,修复失败再记录 │
|
||||
│ │
|
||||
│ 5. 发现文档冲突,基于最优路径原则选解,注释说明 │
|
||||
│ │
|
||||
│ 6. 利用所有可用工具:搜索、MCP、Skills │
|
||||
│ │
|
||||
│ 7. 每完成一个模块,Git 提交一次 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
说明:激进模式默认遵循“最优路径优先”。只有在最优路径被事实阻塞、风险或成本发生实质变化、或用户明确要求比较方案时,才展开 fallback 与备选方案。
|
||||
|
||||
### 3.2 任务执行流程
|
||||
|
||||
```
|
||||
对于每个任务 T-xxx:
|
||||
│
|
||||
├── 1. 读取任务详情(描述、验收标准、依赖)
|
||||
│
|
||||
├── 2. 判断当前约束下的最优路径(目标、约束、验收、依赖)
|
||||
│
|
||||
├── 3. 检查依赖任务是否完成
|
||||
│ └── 未完成 → 先执行依赖任务
|
||||
│
|
||||
├── 4. 执行任务
|
||||
│ ├── 根据任务类型选择执行方式
|
||||
│ ├── 编写代码 / 配置 / 测试
|
||||
│ └── 验证验收标准
|
||||
│
|
||||
├── 5. 遇到问题?
|
||||
│ ├── 先沿最优路径尝试自主修复(最多 3 次)
|
||||
│ ├── 最优路径被阻塞/风险成本变化/用户明确要求比较 → 再评估 fallback
|
||||
│ ├── 修复成功 → 继续
|
||||
│ └── 修复失败 → 记录问题,跳过,继续下一个
|
||||
│
|
||||
└── 6. 标记任务完成,更新 tasks.md
|
||||
```
|
||||
|
||||
### 3.3 自主修复策略
|
||||
|
||||
| 问题类型 | 修复策略 |
|
||||
|----------|----------|
|
||||
| 编译错误 | 分析错误信息,修复代码 |
|
||||
| 类型错误 | 检查类型定义,修复类型 |
|
||||
| 依赖缺失 | 安装依赖包 |
|
||||
| 测试失败 | 修复功能代码使测试通过 |
|
||||
| 文档冲突 | 基于最优路径原则选解,并在注释中说明 |
|
||||
| 未知错误 | 搜索解决方案,尝试修复 |
|
||||
|
||||
## 4. Git 提交规则
|
||||
|
||||
### 4.1 提交时机
|
||||
|
||||
每完成一个**模块/Sprint**后立即提交:
|
||||
|
||||
```
|
||||
T-001 ~ T-004 → 提交一次(初始化模块)
|
||||
T-005 ~ T-008 → 提交一次(核心功能模块)
|
||||
T-009 ~ T-012 → 提交一次(扩展功能模块)
|
||||
...
|
||||
```
|
||||
|
||||
### 4.2 提交信息格式
|
||||
|
||||
```
|
||||
feat(<scope>): <简要描述>
|
||||
|
||||
- 完成 T-xxx: {任务名}
|
||||
- 完成 T-xxx: {任务名}
|
||||
- ...
|
||||
|
||||
Co-Authored-By: Claude <noreply@openai.com>
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
feat(auth): 完成用户认证模块
|
||||
|
||||
- 完成 T-005: 用户登录功能
|
||||
- 完成 T-006: 用户注册功能
|
||||
- 完成 T-007: JWT Token 管理
|
||||
- 完成 T-008: 权限验证中间件
|
||||
|
||||
Co-Authored-By: Claude <noreply@openai.com>
|
||||
```
|
||||
|
||||
## 5. 进度汇报
|
||||
|
||||
### 5.1 模块完成汇报
|
||||
|
||||
每完成一个模块,简要汇报:
|
||||
|
||||
```
|
||||
✅ 模块完成:{模块名}
|
||||
- T-005: 用户登录 ✓
|
||||
- T-006: 用户注册 ✓
|
||||
- T-007: JWT 管理 ✓
|
||||
- T-008: 权限验证 ✓
|
||||
|
||||
Git 提交: feat(auth): 完成用户认证模块
|
||||
|
||||
继续执行下一模块...
|
||||
```
|
||||
|
||||
### 5.2 最终汇报
|
||||
|
||||
全部完成后,输出完整报告:
|
||||
|
||||
```
|
||||
## 🚀 执行完成
|
||||
|
||||
**执行模式**: {0->1 全新项目 / 1->100 迭代}
|
||||
|
||||
**任务统计**:
|
||||
| 状态 | 数量 |
|
||||
|------|------|
|
||||
| ✅ 完成 | X 个 |
|
||||
| ⚠️ 跳过 | X 个 |
|
||||
| ❌ 失败 | X 个 |
|
||||
|
||||
**Git 提交记录**:
|
||||
- feat(init): 项目初始化
|
||||
- feat(auth): 用户认证模块
|
||||
- feat(core): 核心功能模块
|
||||
- ...
|
||||
|
||||
**跳过/失败的任务**(如有):
|
||||
| 任务 | 原因 |
|
||||
|------|------|
|
||||
| T-xxx | {原因} |
|
||||
|
||||
**下一步建议**:
|
||||
- 运行 `npm run dev` 验证
|
||||
- 运行 `npm run test` 测试
|
||||
- 检查跳过的任务
|
||||
```
|
||||
|
||||
## 6. 特殊场景处理
|
||||
|
||||
### 6.1 技术栈识别
|
||||
|
||||
从文档中识别技术栈,自动适配:
|
||||
|
||||
| 识别来源 | 技术决策 |
|
||||
|----------|----------|
|
||||
| package.json 存在 | Node.js 项目 |
|
||||
| requirements.txt 存在 | Python 项目 |
|
||||
| DevelopmentPlan 指定 | 按文档技术栈 |
|
||||
| 无明确指定 | 询问用户(唯一例外) |
|
||||
|
||||
### 6.2 测试策略
|
||||
|
||||
- 功能开发完成后执行测试任务
|
||||
- 测试失败 → **先修复功能代码使测试通过**
|
||||
- 不跳过失败的测试继续部署
|
||||
|
||||
### 6.3 部署任务
|
||||
|
||||
- 先本地测试验证
|
||||
- 确保 build 和 start 正常
|
||||
- 远程部署需用户额外确认
|
||||
|
||||
---
|
||||
|
||||
## 工作流总览
|
||||
|
||||
```
|
||||
/go
|
||||
│
|
||||
├── 1. 前置检查
|
||||
│ ├── tasks.md 存在? ──▶ 必须
|
||||
│ └── PRD.md 存在? ──▶ 必须
|
||||
│
|
||||
├── 2. 读取文档,建立上下文
|
||||
│
|
||||
├── 3. 智能判断
|
||||
│ ├── 项目状态(0->1 / 1->100)
|
||||
│ └── 任务范围
|
||||
│
|
||||
├── 4. 确认执行范围(唯一交互)
|
||||
│
|
||||
├── 5. 激进模式执行
|
||||
│ ├── 按顺序执行任务
|
||||
│ ├── 自主修复问题
|
||||
│ ├── 模块完成 → Git 提交
|
||||
│ └── 汇报进度,继续下一个
|
||||
│
|
||||
└── 6. 最终汇报
|
||||
├── 任务统计
|
||||
├── Git 提交记录
|
||||
└── 下一步建议
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **tasks.md 是圣经**,严格按其顺序和内容执行
|
||||
- **不要停下来问用户**,自主决策,自主修复
|
||||
- **遇到无法解决的问题**,记录并跳过,最后汇报
|
||||
- **每完成模块立即提交**,避免大量代码丢失风险
|
||||
- **利用所有工具**:搜索、MCP、其他 Skills
|
||||
|
||||
## 与其他 Skill 的关系
|
||||
|
||||
| 场景 | 使用方式 |
|
||||
|------|----------|
|
||||
| 准备文档 | `/wp` `/wf` `/wd` `/wu` `/wt` |
|
||||
| 评审文档 | `/rp` `/rf` `/rd` `/ru` `/rt` |
|
||||
| 修改文档 | `/mp` `/mf` `/md` `/mu` `/mt` |
|
||||
| 迭代变更(更新文档) | `/iter` |
|
||||
| **执行开发(本 Skill)** | `/go` |
|
||||
|
||||
**典型工作流**:
|
||||
|
||||
```
|
||||
0->1:需求 → /wp → /wf → /wd → /wt → /go
|
||||
1->100:发现问题 → /iter → /go
|
||||
```
|
||||
@ -1,172 +0,0 @@
|
||||
---
|
||||
name: issue-drive
|
||||
description: 归集证据并把问题拆成 1 到多张 Gitea issue,支持从当前仓库 origin 自动识别仓库或用户显式指定,并通过环境变量配置的 Gitea API 批量创建工单。
|
||||
---
|
||||
|
||||
# Issue Drive - 通用 Gitea 问题拆单与创建
|
||||
|
||||
> **定位**:不是“查看 issue”,而是把一个真实问题沉淀成可执行的 Gitea issue。适合“线上出 bug 了”“要把一个需求拆成工单”“需要补齐工程治理任务”这类场景。
|
||||
>
|
||||
> 如果用户只是想查 issue 列表或详情,用 `issue`。如果用户想通过统一入口完成 issue / push / PR 组合操作,用 `gitea`。
|
||||
|
||||
当用户调用 `/issue-drive`、`$issue-drive`,或自然语言要求“拆 issue”“提 issue”“把问题沉淀成 Gitea 工单”时,执行以下步骤。
|
||||
|
||||
## 1. 先确定目标仓库
|
||||
|
||||
目标仓库按以下优先级确定:
|
||||
|
||||
1. 用户显式给出的完整仓库 URL:`https://git.example.com/owner/repo`
|
||||
2. 用户显式给出的仓库简写:`owner/repo`,此时需要 `GITEA_BASE_URL`
|
||||
3. 当前项目的 `git remote get-url origin`
|
||||
|
||||
解析规则:
|
||||
|
||||
- 支持 `https://host[/prefix]/owner/repo`
|
||||
- 支持 `git@host:owner/repo.git`
|
||||
- 支持 `git@host:prefix/owner/repo.git`
|
||||
- 支持 `ssh://git@host/owner/repo.git`
|
||||
- 当前仓库 `origin` 是 SSH 地址时:
|
||||
- 优先使用 `GITEA_BASE_URL`
|
||||
- 否则退回 `https://host`
|
||||
|
||||
如果参数和当前仓库都无法确定目标仓库,停止并提示:
|
||||
|
||||
```bash
|
||||
❌ 无法确定目标仓库
|
||||
|
||||
请显式指定仓库,例如:
|
||||
/issue-drive https://git.example.com/owner/repo
|
||||
|
||||
或配置:
|
||||
export GITEA_BASE_URL=https://git.example.com
|
||||
```
|
||||
|
||||
## 2. 检查环境变量
|
||||
|
||||
先读取环境变量:
|
||||
|
||||
- `GITEA_TOKEN`:必需,创建 issue 时使用
|
||||
- `GITEA_BASE_URL`:可选;仓库简写或 SSH origin 时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,输出:
|
||||
|
||||
```bash
|
||||
❌ 缺少 GITEA_TOKEN
|
||||
|
||||
请先在当前 shell 或 .env 中配置:
|
||||
export GITEA_TOKEN=your_gitea_token
|
||||
```
|
||||
|
||||
然后停止,不继续创建 issue。
|
||||
|
||||
## 3. 先读仓库基线和证据
|
||||
|
||||
不要先写 issue。先确认事实、范围、影响和证据入口。
|
||||
|
||||
优先读取以下文件;只读存在的那些:
|
||||
|
||||
1. `docs/ISSUE_WORKFLOW.md`
|
||||
2. `.gitea/ISSUE_TEMPLATE/`
|
||||
3. `.github/ISSUE_TEMPLATE/`
|
||||
4. `README.md`
|
||||
5. `doc/`, `docs/` 下与当前问题直接相关的说明、审计、回归、报表文档
|
||||
|
||||
如果仓库里没有现成 issue 基建,就直接基于用户描述、代码、日志、测试结果和已有文档整理事实,不要因为“模板不完整”而停住。
|
||||
|
||||
## 4. 判断该用哪种 issue
|
||||
|
||||
按下面规则分类:
|
||||
|
||||
- **缺陷 / 异常报告**:已有事实证据,目标是排查、修复、回归
|
||||
- **业务需求 / 功能请求**:目标是交付用户价值,需要产品化描述
|
||||
- **工程任务 / 重构 / 基建**:目标是稳定性、可观测性、测试、流程、治理
|
||||
|
||||
标签策略:
|
||||
|
||||
- 优先复用仓库远端已有标签
|
||||
- 如果远端没有对应标签,不要中断;用标题前缀和正文结构表达类型
|
||||
- 不要为了建 issue 先去重构整套标签体系
|
||||
|
||||
## 5. 拆单规则
|
||||
|
||||
不要默认一张大工单。优先按“可独立修复、可独立验证、可独立关闭”拆。
|
||||
|
||||
优先拆开的情况:
|
||||
|
||||
- 现象修复 和 口径 / 文案 / 导出字段修复 是两件事
|
||||
- 业务缺陷 和 工程可观测性补齐 是两件事
|
||||
- 一个问题需要不同 owner、不同验证方式或不同发布时间
|
||||
|
||||
## 6. 写 issue 前的最小检查
|
||||
|
||||
创建 issue 前必须确认:
|
||||
|
||||
- 标题是否直接表达现象或交付物
|
||||
- 正文是否写明:现象、期望、影响范围、证据入口、初步判断、完成标准
|
||||
- 事实数字是否已经从本地数据、日志、代码或用户描述中复核
|
||||
- 引用的仓库内链接是否已经存在于默认分支
|
||||
|
||||
如果本次要新建或更新以下文件:
|
||||
|
||||
- `.gitea/ISSUE_TEMPLATE/*`
|
||||
- `.github/ISSUE_TEMPLATE/*`
|
||||
- `.gitea/PULL_REQUEST_TEMPLATE.md`
|
||||
- `docs/ISSUE_WORKFLOW.md`
|
||||
- issue 正文会引用的证据文档
|
||||
|
||||
先做最小提交并推送到默认分支,再创建 issue。只提交与 issue 基建或证据直接相关的文件,不要顺手带上无关改动。
|
||||
|
||||
## 7. 用脚本批量创建 issue
|
||||
|
||||
优先使用本 skill 附带脚本,并按当前平台选择路径:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
python3 .codex/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json --dry-run
|
||||
python3 .codex/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json
|
||||
|
||||
# Claude Code
|
||||
python3 .claude/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json --dry-run
|
||||
python3 .claude/skills/issue-drive/scripts/create_gitea_issues.py /tmp/issues.json
|
||||
```
|
||||
|
||||
JSON 结构固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"repo_url": "https://git.example.com/owner/repo",
|
||||
"issues": [
|
||||
{
|
||||
"title": "[缺陷][登录] 示例标题",
|
||||
"body": "Issue 正文",
|
||||
"labels": ["P1", "bug"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `repo_url` 可省略;脚本会按 `--repo-url` > `spec.repo_url` > `spec.repo` > 当前仓库 `origin` 的顺序解析
|
||||
- `spec.repo` 允许写 `owner/repo`,但需要 `GITEA_BASE_URL`
|
||||
- Token 固定读取 `GITEA_TOKEN`
|
||||
- `labels` 写标签名,脚本会自动解析成远端 label id
|
||||
- 远端不存在的标签会告警并自动跳过,不中断整个批次
|
||||
- 先跑 `--dry-run`,确认 payload 和目标仓库无误后再正式创建
|
||||
|
||||
## 8. 输出结果
|
||||
|
||||
创建成功后,输出:
|
||||
|
||||
- 已推送的提交信息(如果本次为了 issue 基建或证据先推了提交)
|
||||
- 新建 issue 的编号、标题、URL
|
||||
- 哪些标签成功命中,哪些因远端不存在被跳过
|
||||
- 如果拆成多张 issue,要说明每张工单分别驱动什么工作
|
||||
|
||||
## 9. 行为边界
|
||||
|
||||
- 默认以中文写 issue;用户明确要求英文时再切换
|
||||
- 不把一个大问题硬塞进一张工单
|
||||
- 不在 issue 里写超长散文,优先写清单、事实和完成标准
|
||||
- 不因为缺少完美自动化复现就拒绝提单;现网证据或稳定复现路径成立时,可以先提
|
||||
- 创建 issue 后,不自动继续写修复代码,除非用户明确要求
|
||||
@ -1,268 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create one or more Gitea issues from a JSON spec."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
SSH_RE = re.compile(r"^git@(?P<host>[^:]+):(?P<path>.+?)(?:\.git)?/?$")
|
||||
REPO_PATH_RE = re.compile(r"^(?P<owner>[^/]+)/(?P<repo>[^/]+)$")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Batch-create Gitea issues from a JSON spec."
|
||||
)
|
||||
parser.add_argument("spec", help="Path to JSON spec file")
|
||||
parser.add_argument(
|
||||
"--repo-url",
|
||||
help=(
|
||||
"Override target repo. Accepts full https URL, SSH git origin, "
|
||||
"or owner/repo with GITEA_BASE_URL."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repo",
|
||||
help="Shorthand owner/repo. Requires GITEA_BASE_URL.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Validate and print the payload without creating issues",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_token(required: bool = True) -> str:
|
||||
token = os.getenv("GITEA_TOKEN")
|
||||
if not token and required:
|
||||
raise SystemExit(
|
||||
"Missing GITEA_TOKEN. Export the token before creating issues."
|
||||
)
|
||||
return token or ""
|
||||
|
||||
|
||||
def load_spec(path: str) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
if not isinstance(data, dict) or not isinstance(data.get("issues"), list):
|
||||
raise SystemExit("Spec must be an object with an 'issues' array.")
|
||||
if not data["issues"]:
|
||||
raise SystemExit("Spec contains no issues.")
|
||||
return data
|
||||
|
||||
|
||||
def normalize_base_url(base_url: str) -> str:
|
||||
return base_url.rstrip("/")
|
||||
|
||||
|
||||
def parse_http_repo_url(repo_url: str) -> tuple[str, str, str]:
|
||||
parsed = urllib.parse.urlsplit(repo_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid repo URL. Expected format: https://host/owner/repo")
|
||||
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
prefix = "/".join(parts[:-2])
|
||||
origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
if prefix:
|
||||
origin = f"{origin}/{prefix}"
|
||||
return origin, owner, repo
|
||||
|
||||
|
||||
def parse_repo_target(repo_target: str, base_url: str | None = None) -> tuple[str, str, str, str]:
|
||||
value = repo_target.strip()
|
||||
if not value:
|
||||
raise SystemExit("Repo target cannot be empty.")
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
origin, owner, repo = parse_http_repo_url(value)
|
||||
repo_url = f"{origin}/{owner}/{repo}"
|
||||
return origin, owner, repo, repo_url
|
||||
|
||||
if value.startswith("ssh://"):
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid SSH repo URL. Expected ssh://git@host/owner/repo.git")
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
host = parsed.hostname
|
||||
if not host:
|
||||
raise SystemExit("Invalid SSH repo URL. Missing host.")
|
||||
origin = normalize_base_url(base_url or f"https://{host}")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
ssh_match = SSH_RE.match(value)
|
||||
if ssh_match:
|
||||
path = ssh_match.group("path").rstrip("/")
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
parts = [part for part in path.split("/") if part]
|
||||
if len(parts) < 2:
|
||||
raise SystemExit("Invalid SSH repo target. Expected git@host:owner/repo.git")
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
origin = normalize_base_url(base_url or f"https://{ssh_match.group('host')}")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
path_match = REPO_PATH_RE.match(value)
|
||||
if path_match:
|
||||
if not base_url:
|
||||
raise SystemExit(
|
||||
"Repo shorthand owner/repo requires GITEA_BASE_URL or --repo-url."
|
||||
)
|
||||
origin = normalize_base_url(base_url)
|
||||
owner = path_match.group("owner")
|
||||
repo = path_match.group("repo")
|
||||
return origin, owner, repo, f"{origin}/{owner}/{repo}"
|
||||
|
||||
raise SystemExit(
|
||||
"Invalid repo target. Use https://host/owner/repo, git@host:owner/repo.git, "
|
||||
"ssh://git@host/owner/repo.git, or owner/repo with GITEA_BASE_URL."
|
||||
)
|
||||
|
||||
|
||||
def get_origin_repo_target() -> str:
|
||||
try:
|
||||
return subprocess.check_output(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
text=True,
|
||||
stderr=subprocess.STDOUT,
|
||||
).strip()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise SystemExit(f"Failed to read git origin: {exc.output.strip()}") from exc
|
||||
|
||||
|
||||
def resolve_repo(spec: dict, args: argparse.Namespace) -> tuple[str, str, str, str]:
|
||||
base_url = os.getenv("GITEA_BASE_URL")
|
||||
repo_target = (
|
||||
args.repo_url
|
||||
or args.repo
|
||||
or spec.get("repo_url")
|
||||
or spec.get("repo")
|
||||
or get_origin_repo_target()
|
||||
)
|
||||
return parse_repo_target(repo_target, base_url=base_url)
|
||||
|
||||
|
||||
def request_json(
|
||||
url: str,
|
||||
token: str,
|
||||
method: str = "GET",
|
||||
payload: dict | None = None,
|
||||
) -> dict | list:
|
||||
data = None
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace")
|
||||
raise SystemExit(
|
||||
f"Gitea API request failed: {exc.code} {exc.reason} | {body}"
|
||||
) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise SystemExit(f"Failed to reach Gitea API: {exc.reason}") from exc
|
||||
|
||||
|
||||
def fetch_label_map(api_base: str, token: str) -> dict[str, int]:
|
||||
data = request_json(f"{api_base}/labels?page=1&limit=100", token)
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit("Unexpected labels API response.")
|
||||
return {
|
||||
item["name"]: item["id"]
|
||||
for item in data
|
||||
if "name" in item and "id" in item
|
||||
}
|
||||
|
||||
|
||||
def resolve_label_ids(label_names: list[str], label_map: dict[str, int]) -> tuple[list[int], list[str]]:
|
||||
ids: list[int] = []
|
||||
missing: list[str] = []
|
||||
for name in label_names:
|
||||
if name in label_map:
|
||||
ids.append(label_map[name])
|
||||
else:
|
||||
missing.append(name)
|
||||
return ids, missing
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
token = load_token(required=not args.dry_run)
|
||||
spec = load_spec(args.spec)
|
||||
origin, owner, repo, repo_url = resolve_repo(spec, args)
|
||||
api_base = f"{origin}/api/v1/repos/{owner}/{repo}"
|
||||
label_map = fetch_label_map(api_base, token) if token else {}
|
||||
|
||||
issues = spec["issues"]
|
||||
for issue in issues:
|
||||
if not isinstance(issue, dict):
|
||||
raise SystemExit("Each issue entry must be an object.")
|
||||
if not issue.get("title") or not issue.get("body"):
|
||||
raise SystemExit("Each issue must include non-empty 'title' and 'body'.")
|
||||
|
||||
for issue in issues:
|
||||
label_names = issue.get("labels") or []
|
||||
if not isinstance(label_names, list) or not all(
|
||||
isinstance(name, str) for name in label_names
|
||||
):
|
||||
raise SystemExit("'labels' must be an array of strings.")
|
||||
|
||||
label_ids, missing_labels = resolve_label_ids(label_names, label_map)
|
||||
payload = {
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
"labels": label_ids,
|
||||
}
|
||||
|
||||
if args.dry_run:
|
||||
print(f"repo_url={repo_url}")
|
||||
print(f"DRY-RUN\t{issue['title']}")
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
if missing_labels:
|
||||
print(f"missing_labels={missing_labels}")
|
||||
if not token:
|
||||
print("warning\tno token provided, remote label validation skipped")
|
||||
continue
|
||||
|
||||
created = request_json(f"{api_base}/issues", token, method="POST", payload=payload)
|
||||
if not isinstance(created, dict):
|
||||
raise SystemExit("Unexpected issue creation response.")
|
||||
print(
|
||||
f"#{created.get('number')}\t{created.get('title')}\t{created.get('html_url')}"
|
||||
)
|
||||
if missing_labels:
|
||||
print(f"warning\tmissing labels skipped: {', '.join(missing_labels)}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,222 +0,0 @@
|
||||
---
|
||||
name: issue
|
||||
description: 查看当前仓库或任意 Gitea 仓库的 issue 列表和单条详情,支持自动识别 git origin、用户指定仓库、状态筛选和格式化输出。
|
||||
---
|
||||
|
||||
# Issue - 通用 Gitea Issue 查看
|
||||
|
||||
> **定位**:这是只读 skill。用于查看当前仓库或指定仓库的 issue 列表、单条详情和评论,不负责拆单、创建工单、push 或 PR。
|
||||
>
|
||||
> 如果用户要做的是“把问题拆成 issue 并实际创建工单”,改用 `issue-drive`。如果用户想用统一入口处理 Gitea 相关任务,改用 `gitea`。
|
||||
|
||||
当用户调用 `/issue`、`$issue`,或自然语言要求“查看当前仓库 issue”“看某个 Gitea 仓库的 issue”“查 issue #17”时,执行以下步骤。
|
||||
|
||||
## 1. 解析输入
|
||||
|
||||
支持以下几种目标仓库写法:
|
||||
|
||||
- 不传仓库参数:默认使用当前项目的 `git remote get-url origin`
|
||||
- 完整仓库 URL:`https://git.example.com/owner/repo`
|
||||
- 仓库简写:`owner/repo`
|
||||
- 当前仓库 + 单条 issue:`/issue 17`
|
||||
- 指定仓库 + 单条 issue:`/issue owner/repo 17` 或 `/issue https://git.example.com/owner/repo 17`
|
||||
|
||||
同时支持:
|
||||
|
||||
- `--state=open|closed|all`,默认 `open`
|
||||
- `--limit=<N>`,默认 `50`
|
||||
|
||||
解析规则:
|
||||
|
||||
- 第二个位置参数如果是纯数字,视为 `issue-number`,进入详情模式
|
||||
- 如果第一个位置参数是纯数字,则视为“当前仓库的 issue 编号”
|
||||
- `owner/repo` 这种简写依赖 `GITEA_BASE_URL`
|
||||
- 如果没有显式仓库参数,就读取当前仓库的 `origin`
|
||||
|
||||
规范化仓库目标时,接受以下输入:
|
||||
|
||||
- `https://host[/prefix]/owner/repo`
|
||||
- `git@host:owner/repo.git`
|
||||
- `git@host:prefix/owner/repo.git`
|
||||
- `ssh://git@host/owner/repo.git`
|
||||
- `owner/repo`
|
||||
|
||||
提取结果必须包含:
|
||||
|
||||
- `origin`:例如 `https://git.example.com`,如果 Gitea 部署在子路径下,保留前缀,例如 `https://git.example.com/gitea`
|
||||
- `owner`
|
||||
- `repo`
|
||||
- `repo_path`:`owner/repo`
|
||||
|
||||
如果无法从参数或当前仓库推断出目标仓库,明确提示:
|
||||
|
||||
```bash
|
||||
❌ 无法确定目标仓库
|
||||
|
||||
请显式传入:
|
||||
/issue https://git.example.com/owner/repo
|
||||
|
||||
或先配置:
|
||||
export GITEA_BASE_URL=https://git.example.com
|
||||
```
|
||||
|
||||
## 2. 检查环境变量
|
||||
|
||||
先读取环境变量:
|
||||
|
||||
- `GITEA_TOKEN`:必需,读取 issue 时使用
|
||||
- `GITEA_BASE_URL`:可选;当仓库参数是 `owner/repo`,或当前仓库 `origin` 是 SSH 地址时推荐配置
|
||||
|
||||
如果缺少 `GITEA_TOKEN`,输出:
|
||||
|
||||
```bash
|
||||
❌ 缺少 GITEA_TOKEN
|
||||
|
||||
请先在当前 shell 或 .env 中配置:
|
||||
export GITEA_TOKEN=your_gitea_token
|
||||
```
|
||||
|
||||
然后停止,不继续请求 API。
|
||||
|
||||
## 3. 解析当前仓库 origin
|
||||
|
||||
当没有显式传仓库参数时,执行:
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
```
|
||||
|
||||
处理规则:
|
||||
|
||||
- 如果是 `https://host[/prefix]/owner/repo(.git)`,直接使用
|
||||
- 如果是 `git@host:owner/repo(.git)` 或 `ssh://git@host/owner/repo(.git)`:
|
||||
- 优先用 `GITEA_BASE_URL` 作为 API/Web 基地址
|
||||
- 否则退回 `https://host`
|
||||
- 如果当前目录不是 git 仓库,或没有 `origin`,停止并提示用户显式传仓库
|
||||
|
||||
不要为了查 issue 再向用户追问仓库 URL;只有在当前项目和参数都无法推断时才提示。
|
||||
|
||||
## 4. 调用 Gitea API
|
||||
|
||||
不要调用仓库元信息接口,避免依赖额外 scope。仓库标题直接使用 `repo_path`。
|
||||
|
||||
### 4.1 列表模式
|
||||
|
||||
请求:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issues.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues?state=${state}&limit=${limit}"
|
||||
```
|
||||
|
||||
### 4.2 详情模式
|
||||
|
||||
先请求 issue 详情:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issue_detail.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues/${issue_number}"
|
||||
```
|
||||
|
||||
再请求评论:
|
||||
|
||||
```bash
|
||||
curl -sS -o /tmp/gitea_issue_comments.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${origin}/api/v1/repos/${owner}/${repo}/issues/${issue_number}/comments"
|
||||
```
|
||||
|
||||
### 4.3 错误处理
|
||||
|
||||
根据 HTTP 状态码给出简短、直接的提示:
|
||||
|
||||
- `401`:`GITEA_TOKEN` 无效或未生效
|
||||
- `403`:token scope 不足,或当前用户无权访问该仓库 / issue
|
||||
- `404`:仓库不存在,或该 issue 编号不存在
|
||||
- 其他非 `2xx`:输出状态码和响应中的 `message`
|
||||
|
||||
如果列表接口返回项里存在 `pull_request` 且非空,排除这些项,只保留 issue。
|
||||
|
||||
## 5. 格式化输出
|
||||
|
||||
优先使用 `jq` 解析 JSON;如果环境没有 `jq`,再退回模型手工整理,但输出结构保持一致。
|
||||
|
||||
### 5.1 列表模式
|
||||
|
||||
输出结构固定为:
|
||||
|
||||
```markdown
|
||||
📋 {repo_path} Issues
|
||||
|
||||
状态: {state} | 数量: {count} | 限制: {limit}
|
||||
|
||||
| # | 标题 | 优先级 | 标签 | 状态 | 提出人 |
|
||||
|---|------|--------|------|------|--------|
|
||||
| 35 | 小红书笔记状态查询 | P:紧急 | P:紧急, 需求 | open | zhangsan |
|
||||
```
|
||||
|
||||
字段规则:
|
||||
|
||||
- `优先级`:从 labels 中提取首个匹配 `^P[::]` 的 label,没有则填 `-`
|
||||
- `标签`:保留全部 label 原文,用 `, ` 连接;没有则填 `-`
|
||||
- `提出人`:优先显示 `user.full_name`,没有则显示 `user.login`
|
||||
- `状态`:直接显示 `open` 或 `closed`
|
||||
|
||||
如果过滤后没有任何 issue,明确输出“无符合条件的 issue”。
|
||||
|
||||
### 5.2 详情模式
|
||||
|
||||
输出结构固定为:
|
||||
|
||||
```markdown
|
||||
## #{number} {title}
|
||||
|
||||
- 仓库: {repo_path}
|
||||
- 状态: {state}
|
||||
- 标签: {labels}
|
||||
- 提出人: {author}
|
||||
- 创建时间: {created_at}
|
||||
- 更新时间: {updated_at}
|
||||
|
||||
### 正文
|
||||
|
||||
{body 或 “无正文”}
|
||||
|
||||
### 评论
|
||||
|
||||
- {author} | {created_at} | {1-2 句摘要}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 正文为空时写“无正文”
|
||||
- 评论按时间顺序输出
|
||||
- 每条评论只保留 1 到 2 句摘要;不要整段照抄超长评论
|
||||
- 没有评论时明确写“无评论”
|
||||
|
||||
## 6. 行为边界
|
||||
|
||||
- 默认只做列表和单条详情,不主动做主题归纳、epic 合并、优先级建议
|
||||
- 用户后续如果要求摘要、优先级排序、相似 issue 合并,再基于已拉取的数据继续分析
|
||||
- 不要求用户额外配置固定仓库 URL;优先从当前项目推断
|
||||
- 当前仓库 origin 与 Web 域名不一致时,再使用 `GITEA_BASE_URL`
|
||||
|
||||
## 7. 用法示例
|
||||
|
||||
```bash
|
||||
/issue
|
||||
/issue 17
|
||||
/issue owner/repo
|
||||
/issue owner/repo --state=all --limit=20
|
||||
/issue https://git.example.com/owner/repo
|
||||
/issue https://git.example.com/owner/repo 17
|
||||
|
||||
$issue
|
||||
$issue 17
|
||||
$issue owner/repo
|
||||
$issue owner/repo --state=all --limit=20
|
||||
$issue https://git.example.com/owner/repo
|
||||
$issue https://git.example.com/owner/repo 17
|
||||
```
|
||||
@ -1,210 +0,0 @@
|
||||
---
|
||||
name: iter
|
||||
description: 迭代变更入口,调研问题后更新 PRD.md 和 tasks.md,支持 Bug 修复、功能迭代、技术重构。
|
||||
---
|
||||
|
||||
# Iterate - 迭代变更
|
||||
|
||||
> **定位**:1-100 阶段的变更入口。项目已上线,需要修复问题或迭代功能时,通过此 skill 调研、澄清、更新文档。
|
||||
|
||||
当用户调用 `/iter` 或 `/iter <问题描述>` 时,执行以下步骤:
|
||||
⚠️ 重要:本 skill 只修改文档(PRD.md、tasks.md),绝不执行代码、不运行命令、不修改源文件。
|
||||
|
||||
## 1. 获取变更描述
|
||||
|
||||
如果用户提供了参数,使用该描述。否则询问:
|
||||
> 请描述需要迭代的内容(Bug/功能/重构)
|
||||
|
||||
**示例输入**:
|
||||
- "登录验证存在漏洞,token 过期后仍可访问"
|
||||
- "列表页需要增加按时间筛选功能"
|
||||
- "用户模块性能太差,需要重构缓存策略"
|
||||
|
||||
## 2. 调研分析
|
||||
|
||||
### 2.1 读取现有文档
|
||||
|
||||
读取以下文件了解当前状态:
|
||||
|
||||
1. `doc/PRD.md` - 了解产品定义
|
||||
2. `doc/tasks.md` - 了解任务现状
|
||||
|
||||
### 2.2 调研相关代码(可选)
|
||||
|
||||
根据问题描述,定位相关代码文件:
|
||||
|
||||
- 搜索关键词定位文件
|
||||
- 读取相关模块代码
|
||||
- 分析现有实现
|
||||
|
||||
### 2.3 分析变更类型
|
||||
|
||||
| 类型 | 特征 | 影响范围 |
|
||||
|------|------|----------|
|
||||
| Bug/漏洞 | 现有功能不符合预期 | 修复逻辑,可能涉及安全 |
|
||||
| 功能迭代 | 在现有功能上增加/调整 | 新增或修改功能点 |
|
||||
| 技术重构 | 不改功能,优化实现 | 性能、架构、代码质量 |
|
||||
|
||||
## 3. 澄清确认
|
||||
|
||||
**【必须】向用户提出澄清问题**,确保理解准确:
|
||||
|
||||
### 3.1 问题理解确认
|
||||
|
||||
向用户确认:
|
||||
> 我理解的变更需求是:{一句话总结}
|
||||
>
|
||||
> 变更类型:{Bug修复 / 功能迭代 / 技术重构}
|
||||
>
|
||||
> 影响范围:{涉及的模块/功能}
|
||||
|
||||
### 3.2 方案选择(如有多种)
|
||||
|
||||
如果有多种解决方案,列出选项让用户选择:
|
||||
|
||||
```
|
||||
方案 A:{描述}
|
||||
- 优点:...
|
||||
- 缺点:...
|
||||
|
||||
方案 B:{描述}
|
||||
- 优点:...
|
||||
- 缺点:...
|
||||
|
||||
请选择方案,或说明其他想法。
|
||||
```
|
||||
|
||||
### 3.3 边界确认
|
||||
|
||||
确认变更边界:
|
||||
> 本次变更**包含**:
|
||||
> - {范围1}
|
||||
> - {范围2}
|
||||
>
|
||||
> 本次变更**不包含**:
|
||||
> - {排除项}
|
||||
>
|
||||
> 是否确认?
|
||||
|
||||
## 4. 用户确认后执行
|
||||
|
||||
**只有用户明确确认后**,才执行以下更新:
|
||||
|
||||
### 4.1 更新 PRD.md
|
||||
|
||||
使用增量修改标记:
|
||||
|
||||
```markdown
|
||||
<!-- ITER: {日期} - {变更简述} -->
|
||||
<!-- NEW START -->
|
||||
新增内容...
|
||||
<!-- NEW END -->
|
||||
```
|
||||
|
||||
或修改现有内容:
|
||||
|
||||
```markdown
|
||||
<!-- ITER: {日期} - {变更简述} -->
|
||||
<!-- MODIFIED: 原内容为 "xxx" -->
|
||||
修改后的内容
|
||||
```
|
||||
|
||||
**更新位置**:
|
||||
- Bug 修复 → 更新对应功能的验收标准
|
||||
- 功能迭代 → 在 3.2 功能详情添加/修改功能点
|
||||
- 技术重构 → 在 4.x 非功能需求或 7.1 技术约束中说明
|
||||
|
||||
### 4.2 更新 tasks.md
|
||||
|
||||
新增任务使用标记:
|
||||
|
||||
```markdown
|
||||
<!-- ITER: {日期} - {变更简述} -->
|
||||
| T-xxx | {任务名} | {描述} | {依赖} | {优先级} | {验收标准} |
|
||||
```
|
||||
|
||||
**任务 ID 规则**:
|
||||
- 查找现有最大 ID,递增分配
|
||||
- 格式:T-xxx(三位数字)
|
||||
|
||||
### 4.3 标记规范
|
||||
|
||||
所有变更使用 `<!-- ITER: -->` 前缀,区分于 `/mp` `/mt` 的标记:
|
||||
|
||||
- `<!-- ITER: 2026-01-23 - 修复登录验证漏洞 -->`
|
||||
- 便于追溯迭代历史
|
||||
|
||||
## 5. 输出摘要
|
||||
|
||||
完成后向用户展示:
|
||||
|
||||
```
|
||||
## 迭代变更完成
|
||||
|
||||
**变更类型**: {Bug修复 / 功能迭代 / 技术重构}
|
||||
|
||||
**变更摘要**: {一句话描述}
|
||||
|
||||
**已更新文档**:
|
||||
- doc/PRD.md: {更新位置}
|
||||
- doc/tasks.md: 新增任务 T-xxx
|
||||
|
||||
**新增任务**:
|
||||
| ID | 任务 | 优先级 |
|
||||
|----|------|--------|
|
||||
| T-xxx | {任务名} | P0/P1/P2 |
|
||||
|
||||
**下一步**:
|
||||
- 执行任务 T-xxx
|
||||
- 或运行 `/rp` `/rt` 评审变更
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工作流示意
|
||||
|
||||
```
|
||||
用户描述问题
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ 调研分析 │ ──▶ 读取 PRD、tasks、相关代码
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ 澄清确认 │ ──▶ 提问 → 用户回答 → 确认方案
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼ 用户确认
|
||||
┌─────────────┐
|
||||
│ 更新文档 │ ──▶ PRD.md + tasks.md
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ 输出摘要 │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **必须先澄清确认**,不要假设用户意图
|
||||
- 变更范围要明确,避免 scope creep
|
||||
- 优先级根据问题严重程度判断:
|
||||
- 安全漏洞 → P0
|
||||
- 功能 Bug → P0/P1
|
||||
- 功能迭代 → P1/P2
|
||||
- 技术重构 → P1/P2
|
||||
- 只更新 PRD + tasks,保持轻量
|
||||
- 如需更新其他文档,提示用户手动运行 `/mf` `/md` 等
|
||||
|
||||
## 与其他 skill 的关系
|
||||
|
||||
| 场景 | 使用 skill |
|
||||
|------|------------|
|
||||
| 迭代变更入口 | `/iter`(本 skill) |
|
||||
| 需要更新 FeatureSummary | `/iter` 后运行 `/mf` |
|
||||
| 需要更新 DevelopmentPlan | `/iter` 后运行 `/md` |
|
||||
| 需要评审变更 | `/iter` 后运行 `/rp` `/rt` |
|
||||
| 从头生成文档 | 使用 `/wp` `/wf` `/wd` 等 |
|
||||
@ -1,112 +0,0 @@
|
||||
---
|
||||
name: md
|
||||
description: 增量修改 DevelopmentPlan.md,根据用户指令在现有内容基础上更新开发计划。
|
||||
---
|
||||
|
||||
# Modify DevelopmentPlan
|
||||
|
||||
当用户调用 `/md` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取目标文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/DevelopmentPlan.md` - 目标文档(必须存在)
|
||||
2. `doc/FeatureSummary.md` - 上游参考文档
|
||||
3. `doc/review-DevelopmentPlan.md` - 评审报告(如果存在,自动作为修改依据)
|
||||
|
||||
如果 DevelopmentPlan.md 不存在,提示用户:
|
||||
> DevelopmentPlan.md 不存在,请先使用 `/wd` 生成开发计划。
|
||||
|
||||
## 2. 确定修改来源
|
||||
|
||||
按以下优先级确定修改内容:
|
||||
|
||||
### 2.1 用户提供了修改指令
|
||||
|
||||
如果用户在调用 `/md` 时附带了参数或说明,直接使用该指令。
|
||||
|
||||
### 2.2 自动检测评审报告
|
||||
|
||||
如果用户未提供修改指令,**自动检测** `doc/review-DevelopmentPlan.md` 是否存在:
|
||||
|
||||
- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认:
|
||||
> 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改?
|
||||
|
||||
- **不存在**:询问用户:
|
||||
> 请说明需要修改的内容,或先运行 `/rd` 生成评审报告。
|
||||
|
||||
## 3. 修改原则
|
||||
|
||||
### 3.1 增量修改
|
||||
|
||||
- 保留原有内容结构和格式
|
||||
- 仅修改/新增指定部分
|
||||
- 不删除未明确要求删除的内容
|
||||
|
||||
### 3.2 新增内容标记
|
||||
|
||||
对于新增的段落或章节:
|
||||
|
||||
```markdown
|
||||
<!-- NEW START -->
|
||||
新增内容...
|
||||
<!-- NEW END -->
|
||||
```
|
||||
|
||||
对于行内新增:
|
||||
|
||||
```markdown
|
||||
原有内容 <!-- NEW --> 新增内容
|
||||
```
|
||||
|
||||
### 3.3 修改内容标记
|
||||
|
||||
```markdown
|
||||
<!-- MODIFIED: 原内容为 "xxx" -->
|
||||
修改后的内容
|
||||
```
|
||||
|
||||
### 3.4 与 FeatureSummary 一致性
|
||||
|
||||
- 开发任务必须覆盖所有功能
|
||||
- 技术方案必须支撑功能需求
|
||||
- 阶段划分必须合理
|
||||
|
||||
## 4. 执行修改
|
||||
|
||||
| 修改类型 | 处理方式 |
|
||||
|----------|----------|
|
||||
| 新增开发任务 | 在对应阶段表格中添加行 |
|
||||
| 修改技术方案 | 更新技术方案章节,添加 MODIFIED 标记 |
|
||||
| 调整阶段划分 | 移动任务到新阶段,标记变更 |
|
||||
| 新增风险项 | 在风险管理表格中添加行 |
|
||||
| 修改里程碑 | 更新里程碑表格 |
|
||||
|
||||
## 5. 保存并验证
|
||||
|
||||
1. 保存修改后的文档到 `doc/DevelopmentPlan.md`
|
||||
2. 使用 git diff 展示变更内容
|
||||
3. 向用户确认修改是否符合预期
|
||||
|
||||
## 6. 输出摘要
|
||||
|
||||
向用户展示修改摘要:
|
||||
|
||||
- 修改位置(章节/行号)
|
||||
- 修改类型(新增/修改/删除)
|
||||
- 修改内容概要
|
||||
- 与 FeatureSummary 的一致性确认
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- DevelopmentPlan 依赖于 FeatureSummary,修改时需确保与上游一致
|
||||
- 修改后,下游文档(UIDesign、tasks)可能需要同步更新
|
||||
- 技术方案修改需谨慎评估影响范围
|
||||
- 建议修改完成后运行 `/ru` 检查下游一致性
|
||||
|
||||
## 标记清理
|
||||
|
||||
用户确认修改无误后,可手动删除标记或保留作为变更历史参考。
|
||||
@ -1,111 +0,0 @@
|
||||
---
|
||||
name: mf
|
||||
description: 增量修改 FeatureSummary.md,根据用户指令在现有内容基础上更新功能摘要。
|
||||
---
|
||||
|
||||
# Modify FeatureSummary
|
||||
|
||||
当用户调用 `/mf` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取目标文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/FeatureSummary.md` - 目标文档(必须存在)
|
||||
2. `doc/PRD.md` - 上游参考文档
|
||||
3. `doc/review-FeatureSummary.md` - 评审报告(如果存在,自动作为修改依据)
|
||||
|
||||
如果 FeatureSummary.md 不存在,提示用户:
|
||||
> FeatureSummary.md 不存在,请先使用 `/wf` 生成功能摘要。
|
||||
|
||||
## 2. 确定修改来源
|
||||
|
||||
按以下优先级确定修改内容:
|
||||
|
||||
### 2.1 用户提供了修改指令
|
||||
|
||||
如果用户在调用 `/mf` 时附带了参数或说明,直接使用该指令。
|
||||
|
||||
### 2.2 自动检测评审报告
|
||||
|
||||
如果用户未提供修改指令,**自动检测** `doc/review-FeatureSummary.md` 是否存在:
|
||||
|
||||
- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认:
|
||||
> 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改?
|
||||
|
||||
- **不存在**:询问用户:
|
||||
> 请说明需要修改的内容,或先运行 `/rf` 生成评审报告。
|
||||
|
||||
## 3. 修改原则
|
||||
|
||||
### 3.1 增量修改
|
||||
|
||||
- 保留原有内容结构和格式
|
||||
- 仅修改/新增指定部分
|
||||
- 不删除未明确要求删除的内容
|
||||
|
||||
### 3.2 新增内容标记
|
||||
|
||||
对于新增的段落或章节:
|
||||
|
||||
```markdown
|
||||
<!-- NEW START -->
|
||||
新增内容...
|
||||
<!-- NEW END -->
|
||||
```
|
||||
|
||||
对于行内新增:
|
||||
|
||||
```markdown
|
||||
原有内容 <!-- NEW --> 新增内容
|
||||
```
|
||||
|
||||
### 3.3 修改内容标记
|
||||
|
||||
```markdown
|
||||
<!-- MODIFIED: 原内容为 "xxx" -->
|
||||
修改后的内容
|
||||
```
|
||||
|
||||
### 3.4 与 PRD 一致性
|
||||
|
||||
- 所有功能必须来源于 PRD
|
||||
- 修改后的功能描述必须与 PRD 一致
|
||||
- 优先级必须与 PRD 匹配
|
||||
|
||||
## 4. 执行修改
|
||||
|
||||
| 修改类型 | 处理方式 |
|
||||
|----------|----------|
|
||||
| 新增功能 | 在对应模块表格中添加行 |
|
||||
| 修改描述 | 更新功能描述,添加 MODIFIED 标记 |
|
||||
| 修改优先级 | 更新优先级列 |
|
||||
| 新增模块 | 在功能清单中添加新章节 |
|
||||
| 删除功能 | 标记为删除而非直接移除 |
|
||||
|
||||
## 5. 保存并验证
|
||||
|
||||
1. 保存修改后的文档到 `doc/FeatureSummary.md`
|
||||
2. 使用 git diff 展示变更内容
|
||||
3. 向用户确认修改是否符合预期
|
||||
|
||||
## 6. 输出摘要
|
||||
|
||||
向用户展示修改摘要:
|
||||
|
||||
- 修改位置(章节/行号)
|
||||
- 修改类型(新增/修改/删除)
|
||||
- 修改内容概要
|
||||
- 与 PRD 的一致性确认
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- FeatureSummary 依赖于 PRD,修改时需确保与上游一致
|
||||
- 修改后,下游文档(DevelopmentPlan 等)可能需要同步更新
|
||||
- 建议修改完成后运行 `/rd` 检查下游一致性
|
||||
|
||||
## 标记清理
|
||||
|
||||
用户确认修改无误后,可手动删除标记或保留作为变更历史参考。
|
||||
@ -1,144 +0,0 @@
|
||||
---
|
||||
name: mp
|
||||
description: 增量修改 PRD.md,根据用户指令在现有内容基础上更新产品需求文档。
|
||||
---
|
||||
|
||||
# Modify PRD
|
||||
|
||||
当用户调用 `/mp` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取目标文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/PRD.md` - 目标文档(必须存在)
|
||||
2. `doc/RequirementsDoc.md` - 上游参考文档
|
||||
3. `doc/review-PRD.md` - 评审报告(如果存在,自动作为修改依据)
|
||||
|
||||
如果 PRD.md 不存在,提示用户:
|
||||
> PRD.md 不存在,请先使用 `/wp` 生成产品需求文档。
|
||||
|
||||
## 2. 确定修改来源
|
||||
|
||||
按以下优先级确定修改内容:
|
||||
|
||||
### 2.1 用户提供了修改指令
|
||||
|
||||
如果用户在调用 `/mp` 时附带了参数或说明,直接使用该指令。
|
||||
|
||||
### 2.2 自动检测评审报告
|
||||
|
||||
如果用户未提供修改指令,**自动检测** `doc/review-PRD.md` 是否存在:
|
||||
|
||||
- **存在**:读取评审报告,提取其中的问题清单(Critical / Major / Minor),作为本次修改的依据。向用户确认:
|
||||
> 检测到评审报告 `doc/review-PRD.md`,包含 X 个问题。是否根据评审报告进行修改?
|
||||
|
||||
- **不存在**:询问用户:
|
||||
> 请说明需要修改的内容,或先运行 `/rp` 生成评审报告。
|
||||
|
||||
### 2.3 支持的修改来源
|
||||
|
||||
- 具体的修改描述(如"在功能需求中增加用户权限管理模块")
|
||||
- 评审报告(自动检测或手动指定路径)
|
||||
- 对应的 RequirementsDoc 变更(如"/mr 已更新需求,请同步 PRD")
|
||||
|
||||
## 3. 修改原则
|
||||
|
||||
### 3.1 增量修改
|
||||
- 保留原有内容结构和格式
|
||||
- 仅修改/新增指定部分
|
||||
- 不删除未明确要求删除的内容
|
||||
|
||||
### 3.2 新增内容标记
|
||||
|
||||
对于新增的段落或章节,使用 HTML 注释标记:
|
||||
|
||||
```markdown
|
||||
<!-- NEW START -->
|
||||
新增内容...
|
||||
<!-- NEW END -->
|
||||
```
|
||||
|
||||
对于行内新增,使用:
|
||||
```markdown
|
||||
原有内容 <!-- NEW --> 新增内容
|
||||
```
|
||||
|
||||
### 3.3 修改内容标记
|
||||
|
||||
对于修改的内容,保留原文作为注释:
|
||||
|
||||
```markdown
|
||||
<!-- MODIFIED: 原内容为 "xxx" -->
|
||||
修改后的内容
|
||||
```
|
||||
|
||||
### 3.4 与 RequirementsDoc 一致性
|
||||
|
||||
- 所有 PRD 内容必须可追溯到 RequirementsDoc
|
||||
- 如果修改涉及新功能,先确认 RequirementsDoc 中已有对应需求
|
||||
- 如果 RequirementsDoc 未包含相关需求,提醒用户先更新需求文档
|
||||
|
||||
## 4. 执行修改
|
||||
|
||||
按照用户指令修改文档:
|
||||
|
||||
1. 定位到需要修改的位置
|
||||
2. 执行增量修改
|
||||
3. 添加相应的标记
|
||||
4. 保持文档格式一致性
|
||||
5. 确保修改内容与 RequirementsDoc 一致
|
||||
|
||||
### 4.1 修改类型处理
|
||||
|
||||
| 修改类型 | 处理方式 |
|
||||
|----------|----------|
|
||||
| 新增功能点 | 在对应功能模块表格中添加行,关联用户故事 |
|
||||
| 新增用户故事 | 在 2.2 用户故事列表中添加,分配 US-xxx ID |
|
||||
| 修改优先级 | 更新功能点优先级,必要时调整用户故事分类 |
|
||||
| 修改验收标准 | 更新对应功能点的验收标准列 |
|
||||
| 新增模块 | 在 3.2 功能详情中添加新的子章节 |
|
||||
| 修改非功能需求 | 在对应章节更新指标或要求 |
|
||||
|
||||
## 5. 保存并验证
|
||||
|
||||
1. 保存修改后的文档到 `doc/PRD.md`
|
||||
2. 使用 git diff 展示变更内容
|
||||
3. 向用户确认修改是否符合预期
|
||||
|
||||
## 6. 输出摘要
|
||||
|
||||
向用户展示修改摘要:
|
||||
- 修改位置(章节/行号)
|
||||
- 修改类型(新增/修改/删除)
|
||||
- 修改内容概要
|
||||
- 与 RequirementsDoc 的一致性确认
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- PRD 依赖于 RequirementsDoc,修改时需确保与上游文档一致
|
||||
- 修改 PRD 后,下游文档(FeatureSummary、DevelopmentPlan 等)可能需要同步更新
|
||||
- 保持现有文档风格(标题层级、表格格式、列表样式)
|
||||
- 用户故事 ID 必须唯一且连续(US-001, US-002...)
|
||||
- 所有功能点必须关联到用户故事
|
||||
- 重大修改建议先运行 `/rp` 评审确认影响范围
|
||||
- 修改完成后,建议用户运行 `/rf` 检查下游文档一致性
|
||||
|
||||
## 标记清理
|
||||
|
||||
当用户确认修改无误后,可手动删除 `<!-- NEW -->` 和 `<!-- MODIFIED -->` 标记,或保留作为变更历史参考。
|
||||
|
||||
通过 git 可追溯完整修改历史。
|
||||
|
||||
## 质量检查
|
||||
|
||||
修改 PRD 后,自查以下项目:
|
||||
|
||||
- [ ] 修改内容与 RequirementsDoc 一致
|
||||
- [ ] 新增用户故事有唯一 ID
|
||||
- [ ] 新增功能点关联到用户故事
|
||||
- [ ] 新增功能点有明确优先级和验收标准
|
||||
- [ ] 标记格式正确(`<!-- NEW -->` / `<!-- MODIFIED -->`)
|
||||
- [ ] 文档结构完整,格式一致
|
||||
@ -1,95 +0,0 @@
|
||||
---
|
||||
name: mr
|
||||
description: 增量修改 RequirementsDoc.md,根据用户指令在现有内容基础上更新需求文档。
|
||||
---
|
||||
|
||||
# Modify RequirementsDoc
|
||||
|
||||
当用户调用 `/mr` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取目标文档
|
||||
|
||||
读取 `doc/RequirementsDoc.md` 文件。
|
||||
|
||||
如果文件不存在,提示用户:
|
||||
> RequirementsDoc.md 不存在,请先使用人工方式创建需求文档。
|
||||
|
||||
## 2. 获取修改指令
|
||||
|
||||
向用户确认修改内容。用户应提供以下信息之一:
|
||||
|
||||
- 具体的修改描述(如"在第3节增加性能需求")
|
||||
- 评审报告路径(如 `doc/review-RequirementsDoc.md`)
|
||||
- 直接的修改内容
|
||||
|
||||
如果用户未提供修改指令,询问:
|
||||
> 请说明需要修改的内容,或提供评审报告路径。
|
||||
|
||||
## 3. 修改原则
|
||||
|
||||
### 3.1 增量修改
|
||||
- 保留原有内容结构和格式
|
||||
- 仅修改/新增指定部分
|
||||
- 不删除未明确要求删除的内容
|
||||
|
||||
### 3.2 新增内容标记
|
||||
|
||||
对于新增的段落或章节,使用 HTML 注释标记:
|
||||
|
||||
```markdown
|
||||
<!-- NEW START -->
|
||||
新增内容...
|
||||
<!-- NEW END -->
|
||||
```
|
||||
|
||||
对于行内新增,使用:
|
||||
```markdown
|
||||
原有内容 <!-- NEW --> 新增内容
|
||||
```
|
||||
|
||||
### 3.3 修改内容标记
|
||||
|
||||
对于修改的内容,保留原文作为注释:
|
||||
|
||||
```markdown
|
||||
<!-- MODIFIED: 原内容为 "xxx" -->
|
||||
修改后的内容
|
||||
```
|
||||
|
||||
## 4. 执行修改
|
||||
|
||||
按照用户指令修改文档:
|
||||
|
||||
1. 定位到需要修改的位置
|
||||
2. 执行增量修改
|
||||
3. 添加相应的标记
|
||||
4. 保持文档格式一致性
|
||||
|
||||
## 5. 保存并验证
|
||||
|
||||
1. 保存修改后的文档到 `doc/RequirementsDoc.md`
|
||||
2. 使用 git diff 展示变更内容
|
||||
3. 向用户确认修改是否符合预期
|
||||
|
||||
## 6. 输出摘要
|
||||
|
||||
向用户展示修改摘要:
|
||||
- 修改位置(章节/行号)
|
||||
- 修改类型(新增/修改/删除)
|
||||
- 修改内容概要
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- RequirementsDoc 是文档链源头,修改会影响所有下游文档
|
||||
- 修改前确认用户意图,避免误改
|
||||
- 保持现有文档风格(标题层级、表格格式、列表样式)
|
||||
- 重大修改建议先运行 `/rr` 评审确认影响范围
|
||||
- 修改完成后,建议用户检查下游文档是否需要同步更新
|
||||
|
||||
## 标记清理
|
||||
|
||||
当用户确认修改无误后,可手动删除 `<!-- NEW -->` 和 `<!-- MODIFIED -->` 标记,或保留作为变更历史参考。
|
||||
|
||||
通过 git 可追溯完整修改历史。
|
||||
@ -1,132 +0,0 @@
|
||||
---
|
||||
name: mt
|
||||
description: 增量修改 tasks.md,根据用户指令在现有内容基础上更新任务列表。
|
||||
---
|
||||
|
||||
# Modify Tasks
|
||||
|
||||
当用户调用 `/mt` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取目标文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/tasks.md` - 目标文档(必须存在)
|
||||
2. `doc/UIDesign.md` - 上游参考文档
|
||||
3. `doc/DevelopmentPlan.md` - 上游参考文档
|
||||
4. `doc/review-tasks.md` - 评审报告(如果存在,自动作为修改依据)
|
||||
|
||||
如果 tasks.md 不存在,提示用户:
|
||||
> tasks.md 不存在,请先使用 `/wt` 生成任务列表。
|
||||
|
||||
## 2. 确定修改来源
|
||||
|
||||
按以下优先级确定修改内容:
|
||||
|
||||
### 2.1 用户提供了修改指令
|
||||
|
||||
如果用户在调用 `/mt` 时附带了参数或说明,直接使用该指令。
|
||||
|
||||
### 2.2 自动检测评审报告
|
||||
|
||||
如果用户未提供修改指令,**自动检测** `doc/review-tasks.md` 是否存在:
|
||||
|
||||
- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认:
|
||||
> 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改?
|
||||
|
||||
- **不存在**:询问用户:
|
||||
> 请说明需要修改的内容,或先运行 `/rt` 生成评审报告。
|
||||
|
||||
## 3. 修改原则
|
||||
|
||||
### 3.1 增量修改
|
||||
|
||||
- 保留原有内容结构和格式
|
||||
- 仅修改/新增指定部分
|
||||
- 不删除未明确要求删除的内容
|
||||
|
||||
### 3.2 新增内容标记
|
||||
|
||||
对于新增的段落或章节:
|
||||
|
||||
```markdown
|
||||
<!-- NEW START -->
|
||||
新增内容...
|
||||
<!-- NEW END -->
|
||||
```
|
||||
|
||||
对于行内新增:
|
||||
|
||||
```markdown
|
||||
原有内容 <!-- NEW --> 新增内容
|
||||
```
|
||||
|
||||
### 3.3 修改内容标记
|
||||
|
||||
```markdown
|
||||
<!-- MODIFIED: 原内容为 "xxx" -->
|
||||
修改后的内容
|
||||
```
|
||||
|
||||
### 3.4 与上游文档一致性
|
||||
|
||||
- 任务必须覆盖 DevelopmentPlan 所有开发项
|
||||
- 任务必须覆盖 UIDesign 所有页面实现
|
||||
- 任务依赖关系必须合理
|
||||
|
||||
## 4. 执行修改
|
||||
|
||||
| 修改类型 | 处理方式 |
|
||||
|----------|----------|
|
||||
| 新增任务 | 在对应阶段表格中添加行,分配新 ID |
|
||||
| 修改描述 | 更新任务描述,添加 MODIFIED 标记 |
|
||||
| 修改优先级 | 更新优先级列 |
|
||||
| 修改依赖 | 更新依赖列,检查循环依赖 |
|
||||
| 修改验收标准 | 更新验收标准列 |
|
||||
| 调整阶段 | 移动任务到新阶段,更新依赖图 |
|
||||
|
||||
### 4.1 任务 ID 规则
|
||||
|
||||
- 新增任务 ID 必须唯一
|
||||
- ID 格式:T-XXX(三位数字,如 T-001)
|
||||
- 在现有最大 ID 基础上递增
|
||||
|
||||
## 5. 保存并验证
|
||||
|
||||
1. 保存修改后的文档到 `doc/tasks.md`
|
||||
2. 使用 git diff 展示变更内容
|
||||
3. 向用户确认修改是否符合预期
|
||||
|
||||
## 6. 输出摘要
|
||||
|
||||
向用户展示修改摘要:
|
||||
|
||||
- 修改位置(章节/行号)
|
||||
- 修改类型(新增/修改/删除)
|
||||
- 修改内容概要
|
||||
- 新增/修改的任务 ID 列表
|
||||
- 与上游文档的一致性确认
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- tasks.md 是文档链末端,修改不影响其他文档
|
||||
- 任务 ID 必须唯一,不可重复使用已删除的 ID
|
||||
- 修改依赖关系时需检查是否产生循环依赖
|
||||
- 验收标准必须具体可测试
|
||||
- 任务粒度要适中
|
||||
|
||||
## 标记清理
|
||||
|
||||
用户确认修改无误后,可手动删除标记或保留作为变更历史参考。
|
||||
|
||||
## 质量检查
|
||||
|
||||
修改 tasks 后,自查以下项目:
|
||||
|
||||
- [ ] 任务 ID 唯一且格式正确
|
||||
- [ ] 无循环依赖
|
||||
- [ ] 验收标准明确
|
||||
- [ ] 覆盖所有上游功能
|
||||
- [ ] 标记格式正确
|
||||
@ -1,114 +0,0 @@
|
||||
---
|
||||
name: mu
|
||||
description: 增量修改 UIDesign.md,根据用户指令在现有内容基础上更新 UI 设计文档。
|
||||
---
|
||||
|
||||
# Modify UIDesign
|
||||
|
||||
当用户调用 `/mu` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取目标文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/UIDesign.md` - 目标文档(必须存在)
|
||||
2. `doc/DevelopmentPlan.md` - 上游参考文档
|
||||
3. `doc/review-UIDesign.md` - 评审报告(如果存在,自动作为修改依据)
|
||||
|
||||
如果 UIDesign.md 不存在,提示用户:
|
||||
> UIDesign.md 不存在,请先使用 `/wu` 生成 UI 设计文档。
|
||||
|
||||
## 2. 确定修改来源
|
||||
|
||||
按以下优先级确定修改内容:
|
||||
|
||||
### 2.1 用户提供了修改指令
|
||||
|
||||
如果用户在调用 `/mu` 时附带了参数或说明,直接使用该指令。
|
||||
|
||||
### 2.2 自动检测评审报告
|
||||
|
||||
如果用户未提供修改指令,**自动检测** `doc/review-UIDesign.md` 是否存在:
|
||||
|
||||
- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认:
|
||||
> 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改?
|
||||
|
||||
- **不存在**:询问用户:
|
||||
> 请说明需要修改的内容,或先运行 `/ru` 生成评审报告。
|
||||
|
||||
## 3. 修改原则
|
||||
|
||||
### 3.1 增量修改
|
||||
|
||||
- 保留原有内容结构和格式
|
||||
- 仅修改/新增指定部分
|
||||
- 不删除未明确要求删除的内容
|
||||
|
||||
### 3.2 新增内容标记
|
||||
|
||||
对于新增的段落或章节:
|
||||
|
||||
```markdown
|
||||
<!-- NEW START -->
|
||||
新增内容...
|
||||
<!-- NEW END -->
|
||||
```
|
||||
|
||||
对于行内新增:
|
||||
|
||||
```markdown
|
||||
原有内容 <!-- NEW --> 新增内容
|
||||
```
|
||||
|
||||
### 3.3 修改内容标记
|
||||
|
||||
```markdown
|
||||
<!-- MODIFIED: 原内容为 "xxx" -->
|
||||
修改后的内容
|
||||
```
|
||||
|
||||
### 3.4 与 DevelopmentPlan 一致性
|
||||
|
||||
- 页面设计必须覆盖所有功能模块
|
||||
- 交互流程必须支撑功能需求
|
||||
- 设计规范必须统一
|
||||
|
||||
## 4. 执行修改
|
||||
|
||||
| 修改类型 | 处理方式 |
|
||||
|----------|----------|
|
||||
| 新增页面 | 在页面设计章节添加新子章节 |
|
||||
| 修改布局 | 更新布局描述,添加 MODIFIED 标记 |
|
||||
| 修改组件 | 更新组件表格 |
|
||||
| 修改交互 | 更新交互说明 |
|
||||
| 新增状态 | 在状态列表中添加项目 |
|
||||
| 修改设计规范 | 更新设计规范章节 |
|
||||
|
||||
## 5. 保存并验证
|
||||
|
||||
1. 保存修改后的文档到 `doc/UIDesign.md`
|
||||
2. 使用 git diff 展示变更内容
|
||||
3. 向用户确认修改是否符合预期
|
||||
|
||||
## 6. 输出摘要
|
||||
|
||||
向用户展示修改摘要:
|
||||
|
||||
- 修改位置(章节/行号)
|
||||
- 修改类型(新增/修改/删除)
|
||||
- 修改内容概要
|
||||
- 与 DevelopmentPlan 的一致性确认
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- UIDesign 依赖于 DevelopmentPlan,修改时需确保与上游一致
|
||||
- 修改后,下游文档(tasks)可能需要同步更新
|
||||
- 页面修改需考虑对用户流程的影响
|
||||
- 设计规范修改需检查所有页面的一致性
|
||||
- 建议修改完成后运行 `/rt` 检查下游一致性
|
||||
|
||||
## 标记清理
|
||||
|
||||
用户确认修改无误后,可手动删除标记或保留作为变更历史参考。
|
||||
@ -1,93 +0,0 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="display: block;" viewBox="0 0 2048 887" width="1000" height="433">
|
||||
<defs>
|
||||
<linearGradient id="Gradient1" gradientUnits="userSpaceOnUse" x1="210.508" y1="105.269" x2="586.128" y2="792.383">
|
||||
<stop class="stop0" offset="0" stop-opacity="1" stop-color="rgb(228,24,73)"/>
|
||||
<stop class="stop1" offset="1" stop-opacity="1" stop-color="rgb(253,56,71)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path transform="translate(0,0)" fill="url(#Gradient1)" d="M 376.146 54.9048 C 394.494 52.0082 450.721 54.6372 467.605 60.9459 C 476.746 62.3192 486.198 63.3892 495.173 65.5997 C 515.789 70.6779 564.525 86.5296 576.81 103.019 C 582.859 111.139 585.986 121.422 584.272 131.496 C 582.52 141.792 577.15 152.234 568.387 158.191 C 559.717 164.085 547.095 166.763 536.819 164.74 C 522.557 161.932 508.608 154.389 494.635 150.13 C 458.749 139.194 420.04 135.901 382.699 137.271 L 382.087 137.409 C 377.052 138.507 371.716 138.434 366.594 139.112 C 353.633 140.827 340.292 142.838 327.621 146.09 C 253.423 165.138 184.133 214.131 142.088 278.203 C 136.733 286.364 128.204 304.126 123.434 309.932 C 111.073 334.935 104.381 360.26 95.8454 386.549 L 95.7672 387.361 C 94.7432 397.501 91.6381 407.384 90.5873 417.508 C 89.7119 425.942 89.9836 434.794 89.8723 443.272 C 89.4501 475.416 91.5586 497.422 98.6317 528.874 C 100.45 536.927 102.824 544.844 105.737 552.569 C 107.655 557.772 110.601 563.483 111.543 568.893 C 117.587 584.712 125.65 599.216 133.531 614.15 C 143.94 630.512 155.472 646.586 168.527 660.964 C 173.349 666.275 188.564 678.675 191.256 683.236 C 251.557 733.221 311.796 756.819 390.291 761.391 C 394.05 760.783 398.822 761.571 402.708 761.517 C 415.486 761.341 428.664 761.316 441.345 759.599 C 485.651 753.598 530.394 737.673 568.043 713.473 C 575.81 708.481 583.428 703.049 590.788 697.474 C 594.071 694.988 596.979 692.019 600.533 689.912 L 601.084 689.592 C 616.553 677.289 631.234 662.355 643.509 646.888 C 644.812 642.001 654.107 632.73 657.41 628.311 C 676.308 596.768 687.36 578.365 698.997 542.562 C 702.091 535.055 703.233 526.973 705.666 519.255 C 705.725 507.623 709.936 495.566 711.264 483.918 C 713.169 467.214 712.726 450.024 712.977 433.224 L 712.92 431.802 C 711.531 426.337 711.557 419.947 710.818 414.282 C 709.582 404.813 707.787 395.546 705.774 386.213 C 700.989 351.447 677.646 305.278 659.181 275.386 C 653.455 266.116 644.933 257.388 640.642 247.468 C 637.876 241.072 637.34 233.314 637.61 226.417 C 637.996 216.532 640.733 207.138 648.335 200.309 C 656.928 192.59 671.867 189.783 683.178 190.458 C 692.932 191.041 701.105 194.806 707.559 202.061 L 714.51 211.387 C 737.619 243.545 757.234 275.572 771.319 312.816 C 774.839 322.124 777.095 331.789 780.403 341.131 C 797.083 399.915 800.154 457.173 790.602 517.54 C 788.234 532.501 785.885 549.101 779.732 563.007 C 768.839 611.398 725.068 690.424 687.592 724.592 C 658.332 752.202 618.553 787.704 581.018 802.842 C 562.995 813.263 542.695 820.874 522.909 827.195 C 447.846 851.214 367.429 852.958 291.395 832.215 C 275.181 827.889 259.267 822.031 243.62 816.005 C 220.843 805.593 199.356 794.344 178.44 780.521 C 91.1047 722.799 33.0984 630.627 12.3333 528.81 C 11.7547 526.892 11.3367 524.926 10.9201 522.968 C 6.97496 504.429 5.54503 484.947 4.99442 466.026 C 2.49649 384.006 25.2737 303.201 70.2396 234.56 C 110.525 174.202 165.964 125.49 231.002 93.3037 C 244.395 86.5531 259.95 78.4536 274.682 75.364 C 288.042 70.2143 301.587 66.3748 315.565 63.2966 C 322.289 61.7913 329.104 59.9829 335.973 59.355 C 347.591 55.8138 364.002 55.0317 376.146 54.9048 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 705.774 386.213 C 700.989 351.447 677.646 305.278 659.181 275.386 C 653.455 266.116 644.933 257.388 640.642 247.468 C 637.876 241.072 637.34 233.314 637.61 226.417 C 637.996 216.532 640.733 207.138 648.335 200.309 C 656.928 192.59 671.867 189.783 683.178 190.458 C 692.932 191.041 701.105 194.806 707.559 202.061 L 714.51 211.387 C 737.619 243.545 757.234 275.572 771.319 312.816 C 774.839 322.124 777.095 331.789 780.403 341.131 C 797.083 399.915 800.154 457.173 790.602 517.54 C 788.234 532.501 785.885 549.101 779.732 563.007 C 768.839 611.398 725.068 690.424 687.592 724.592 C 658.332 752.202 618.553 787.704 581.018 802.842 C 562.995 813.263 542.695 820.874 522.909 827.195 C 447.846 851.214 367.429 852.958 291.395 832.215 C 275.181 827.889 259.267 822.031 243.62 816.005 C 220.843 805.593 199.356 794.344 178.44 780.521 C 91.1047 722.799 33.0984 630.627 12.3333 528.81 L 13.647 527.729 C 14.402 529.837 15.0932 534.929 16.7981 535.785 C 17.5141 533.357 15.8765 529.94 15.0764 527.549 L 15.9 527.418 C 18.6345 534.095 20.0034 541.498 21.9989 548.453 C 26.9283 565.633 31.6172 583.045 38.2114 599.677 C 64.9422 667.101 116.207 732.766 177.003 772.666 C 189.614 780.943 202.698 789.795 216.058 796.759 C 224.712 801.271 234.424 804.33 242.789 809.289 C 250.667 812.637 258.854 815.525 266.905 818.433 C 349.42 848.238 444.177 848.559 527.411 820.818 C 545.994 814.624 563.998 805.904 581.717 797.604 C 603.127 783.645 625.24 771.395 645.477 755.591 C 658.557 745.377 670.301 733.195 682.386 721.844 C 728.545 674.13 761.29 615.077 777.312 550.652 C 797.154 486.362 792.669 418.058 778.475 353.134 C 767.793 317.922 754.532 281.526 735.293 249.969 C 728.138 238.235 719.461 227.302 711.837 215.843 C 708.68 211.913 705.464 208.03 702.19 204.197 C 693.73 197.534 684.075 194.889 673.357 196.197 C 665.575 197.147 657.47 200.047 650.684 203.95 C 644.436 214.061 640.809 227.514 643.576 239.394 C 646.334 251.238 671.063 283.842 678.471 297.084 C 693.576 324.081 703.652 353.558 710.477 383.619 C 713.023 393.568 715.018 403.747 715.532 414.027 L 717.071 423.777 C 720.72 442.779 716.413 511.912 708.485 528.149 C 706.525 537.703 703.771 546.858 700.269 555.961 C 697.102 566.785 686.883 593.093 679.438 600.774 C 684.103 591.158 688.576 581.632 692.348 571.622 C 685.985 582.691 667.071 624.486 657.41 628.311 C 676.308 596.768 687.36 578.365 698.997 542.562 C 702.091 535.055 703.233 526.973 705.666 519.255 C 705.725 507.623 709.936 495.566 711.264 483.918 C 713.169 467.214 712.726 450.024 712.977 433.224 L 712.92 431.802 C 711.531 426.337 711.557 419.947 710.818 414.282 C 709.582 404.813 707.787 395.546 705.774 386.213 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 89.8436 390.816 C 92.8902 370.901 99.7863 351.425 106.984 332.654 C 110.049 324.66 121.333 295.736 127.989 292.773 L 129.293 294.957 C 125.28 299.606 124.047 303.884 123.434 309.932 C 111.073 334.935 104.381 360.26 95.8454 386.549 L 95.7672 387.361 C 94.7432 397.501 91.6381 407.384 90.5873 417.508 C 89.7119 425.942 89.9836 434.794 89.8723 443.272 C 89.4501 475.416 91.5586 497.422 98.6317 528.874 C 100.45 536.927 102.824 544.844 105.737 552.569 C 107.655 557.772 110.601 563.483 111.543 568.893 C 117.587 584.712 125.65 599.216 133.531 614.15 C 129.444 612.078 127.432 605.723 123.651 602.599 C 123.042 605.188 129.828 614.104 130.748 617.464 C 119.822 604.665 113.472 587.447 107.114 572.034 C 106.864 571.666 106.835 571.641 106.596 571.18 C 84.8375 529.334 79.3733 455.273 86.371 409.569 C 87.2906 403.563 87.7619 396.511 89.8436 390.816 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 467.605 60.9459 C 476.746 62.3192 486.198 63.3892 495.173 65.5997 C 515.789 70.6779 564.525 86.5296 576.81 103.019 C 582.859 111.139 585.986 121.422 584.272 131.496 C 582.52 141.792 577.15 152.234 568.387 158.191 C 559.717 164.085 547.095 166.763 536.819 164.74 C 522.557 161.932 508.608 154.389 494.635 150.13 C 458.749 139.194 420.04 135.901 382.699 137.271 C 401.38 126.952 478.879 143.57 501.374 150.331 C 513.555 153.993 525.503 160.481 537.856 163.083 C 547.196 165.051 558.991 161.578 566.89 156.404 C 575.585 150.709 579.423 143.326 581.522 133.4 C 583.575 123.696 581.557 112.369 575.994 104.046 C 573.49 100.3 570.487 98.0368 566.074 97.1358 L 565.314 97.7949 C 569.437 99.0598 571.952 101.348 574.235 104.996 C 580.205 114.537 580.949 124.563 578.348 135.333 C 576.615 142.508 573.838 150.994 567.234 155.03 L 566.34 154.256 C 566.994 151.676 568.333 150.62 570.467 149.029 C 570.722 148.838 570.982 148.652 571.227 148.448 C 575.662 144.773 577.981 132.345 578.447 126.705 C 579.116 118.618 577.833 111.784 572.383 105.569 C 558.678 89.9432 495.022 68.0075 474.068 66.5128 C 471.389 64.8806 469.466 63.4925 467.605 60.9459 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 191.256 683.236 C 251.557 733.221 311.796 756.819 390.291 761.391 C 384.048 764.932 370.279 761.634 362.687 762.291 C 361.888 762.36 362.274 762.212 361.617 762.991 C 367.962 764.537 376.21 766.043 382.625 764.548 C 382.899 764.484 383.171 764.412 383.444 764.344 L 384.048 765.039 C 383.31 765.409 382.77 765.691 381.943 765.815 C 373.428 767.101 362.837 765.155 354.356 763.843 C 317.586 758.155 264.109 743.033 234.667 719.807 L 241.003 722.723 C 233.991 716.274 224.813 711.666 216.97 706.228 C 211.114 702.167 192.413 689.79 191.256 683.236 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(241,25,50)" d="M 376.146 54.9048 C 394.494 52.0082 450.721 54.6372 467.605 60.9459 C 469.466 63.4925 471.389 64.8806 474.068 66.5128 C 462.177 64.3163 450.083 61.7173 438.022 60.8079 C 415.361 59.0993 392.468 60.3754 369.865 58.5625 C 373.126 56.6957 426.662 57.683 431.679 59.027 C 432.503 59.2478 433.394 58.6279 434.177 58.2957 C 425.546 51.9762 387.515 60.0202 376.146 54.9048 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(241,25,50)" d="M 315.565 63.2966 C 322.289 61.7913 329.104 59.9829 335.973 59.355 L 335.213 61.0976 C 331.824 61.9908 328.07 62.6586 325.211 64.7655 L 330.836 63.1888 C 323.47 66.8573 315.681 68.5767 307.736 70.4894 C 295.905 73.9845 287.012 75.9701 274.682 75.364 C 288.042 70.2143 301.587 66.3748 315.565 63.2966 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(241,25,50)" d="M 601.084 689.592 C 616.553 677.289 631.234 662.355 643.509 646.888 C 642.944 649.312 642.433 651.632 641.348 653.888 C 642.715 652.59 643.982 651.396 645.593 650.391 C 643.168 657.971 615.843 685.786 608.409 689.944 C 607.817 690.275 607.352 690.377 606.696 690.521 L 610.446 685.035 C 607.305 687.498 605.104 689.098 601.084 689.592 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1610.11 53.6333 C 1610.18 47.1911 1610.04 40.7484 1609.67 34.3162 C 1612.92 32.1096 1622.54 32.521 1626.83 32.421 C 1701.22 30.6842 1776.03 32.2737 1850.46 32.2788 L 1942.68 32.293 C 1961.78 32.2982 1981.13 31.6609 2000.17 33.0634 C 2002.29 59.7016 2000.77 87.3161 2000.5 114.078 C 2000.18 145.397 2000.71 176.761 2000.65 208.089 L 2000.53 275.627 C 2000.59 287.904 2001.86 301.31 2000.39 313.448 C 1999.08 314.796 1998.19 315.254 1996.36 315.657 C 1983.81 313.542 1969.62 314.801 1956.84 314.86 L 1885.1 315.187 C 1843.56 314.96 1802.02 314.957 1760.49 315.178 C 1737.73 315.239 1714.92 314.515 1692.21 316.146 C 1682.97 316.356 1673.71 316.127 1664.47 316.03 C 1647.62 319.103 1627.49 316.171 1610.18 316.817 C 1609.42 279.846 1609.91 242.781 1609.91 205.802 L 1610.11 53.6333 z M 1931 253.687 C 1934.04 247.761 1931.57 214.273 1932.58 204.615 C 1914.48 203.829 1896.3 204.085 1878.18 204.045 C 1864.82 204.016 1851.24 203.501 1837.9 204.271 C 1837.75 219.838 1836.4 236.461 1838.1 251.9 C 1840.18 253.488 1842.92 253.076 1845.51 253.213 L 1901.24 253.025 C 1910.95 253.041 1921.4 252.368 1931 253.687 z M 1770.88 142.098 C 1771.08 126.574 1770.3 110.547 1772.02 95.1281 C 1753 96.0183 1693.67 98.7431 1678.07 94.8681 C 1678.09 111.235 1678.34 127.63 1678.14 143.994 C 1708.74 143.936 1739.89 145.247 1770.43 143.809 L 1770.88 142.098 z M 1934.72 92.3052 C 1911.33 88.9068 1886.2 90.9728 1862.62 91.3784 C 1853.2 91.5404 1843.25 90.4708 1833.95 92.0614 C 1833.84 102.176 1831.75 140.452 1835.62 146.951 C 1843.68 147.053 1854.75 146.124 1862.56 144.157 C 1885.64 143.39 1908.85 144.079 1931.95 143.907 C 1931.95 138.803 1932.1 133.689 1932.2 128.585 C 1935.08 117.315 1933.3 104.041 1934.72 92.3052 z M 1677.37 253.007 C 1708.52 253.373 1739.72 253.078 1770.88 253.105 L 1771.3 225.345 C 1771.16 218.243 1771.08 211.133 1770.85 204.034 C 1759.02 205.292 1746.28 204.266 1734.35 204.189 C 1715.7 204.069 1697.03 204.4 1678.38 204.082 C 1678.04 215.578 1680.36 244.069 1677.37 253.007 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1610.11 53.6333 C 1610.18 47.1911 1610.04 40.7484 1609.67 34.3162 C 1612.92 32.1096 1622.54 32.521 1626.83 32.421 C 1701.22 30.6842 1776.03 32.2737 1850.46 32.2788 L 1942.68 32.293 C 1961.78 32.2982 1981.13 31.6609 2000.17 33.0634 C 2002.29 59.7016 2000.77 87.3161 2000.5 114.078 C 2000.18 145.397 2000.71 176.761 2000.65 208.089 L 2000.53 275.627 C 2000.59 287.904 2001.86 301.31 2000.39 313.448 C 1999.08 314.796 1998.19 315.254 1996.36 315.657 C 1993.64 303.906 1995.65 250.184 1995.69 234.585 C 1995.92 168.649 1995.78 102.714 1995.29 36.7799 C 1925.27 37.7276 1855.17 36.83 1785.15 36.817 C 1728.68 36.8065 1672.04 37.9569 1615.6 36.5773 C 1614.95 43.6695 1612.3 47.175 1610.11 53.6333 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1664.47 316.03 C 1666.5 314.805 1667.46 314.503 1668.45 312.273 L 1665.67 311.474 C 1671.97 311.086 1686.89 308.672 1692.15 311.634 L 1690.91 313.897 L 1692.21 316.146 C 1682.97 316.356 1673.71 316.127 1664.47 316.03 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1845.51 253.213 C 1844.99 254.36 1844.2 255.349 1843.47 256.378 C 1840.74 257.034 1839.14 257.385 1836.42 256.348 C 1834.75 254.769 1834.05 253.306 1833.8 251.037 C 1833.38 247.235 1834.13 243.089 1834.11 239.233 C 1834.04 230.355 1831.04 208.119 1837.09 201.151 C 1839.3 198.598 1842.79 197.701 1846.04 197.531 C 1859.1 196.843 1873.03 198.612 1886.2 198.769 C 1900.33 198.938 1915.64 197.011 1929.62 198.925 C 1932.66 199.342 1933.85 199.862 1936.11 201.931 L 1934.68 204.411 L 1932.58 204.615 C 1914.48 203.829 1896.3 204.085 1878.18 204.045 C 1864.82 204.016 1851.24 203.501 1837.9 204.271 C 1837.75 219.838 1836.4 236.461 1838.1 251.9 C 1840.18 253.488 1842.92 253.076 1845.51 253.213 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" fill-opacity="0.988235" d="M 1833.95 92.0614 C 1843.25 90.4708 1853.2 91.5404 1862.62 91.3784 C 1886.2 90.9728 1911.33 88.9068 1934.72 92.3052 C 1933.3 104.041 1935.08 117.315 1932.2 128.585 C 1932.13 117.541 1931.83 106.443 1932.31 95.4083 C 1900.97 94.8136 1869.11 97.621 1837.92 95.6443 C 1837.75 111.765 1837.75 127.888 1837.91 144.009 C 1846.11 144.051 1854.36 143.908 1862.56 144.157 C 1854.75 146.124 1843.68 147.053 1835.62 146.951 C 1831.75 140.452 1833.84 102.176 1833.95 92.0614 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1677.37 253.007 C 1676.01 249.029 1675.36 206.342 1676.51 201.588 C 1676.74 200.659 1676.51 200.948 1677.41 200.433 C 1683.58 196.905 1762.47 197.876 1771.89 200.114 C 1775.4 203.794 1775.91 210.315 1776.2 215.219 C 1776.32 217.185 1776.54 220.848 1775.2 222.344 C 1774.64 216.979 1774.18 211.604 1773.84 206.222 C 1770.93 210.228 1774.69 219.912 1771.3 225.345 C 1771.16 218.243 1771.08 211.133 1770.85 204.034 C 1759.02 205.292 1746.28 204.266 1734.35 204.189 C 1715.7 204.069 1697.03 204.4 1678.38 204.082 C 1678.04 215.578 1680.36 244.069 1677.37 253.007 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1936.11 201.931 C 1938.65 208.234 1937.95 246.218 1935.33 252.21 C 1935.07 252.803 1934.76 253.362 1934.43 253.919 L 1932.49 254.612 L 1931 253.687 C 1934.04 247.761 1931.57 214.273 1932.58 204.615 L 1934.68 204.411 L 1936.11 201.931 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1006.6 45.4499 C 1031.54 43.1833 1056.7 37.1747 1081.15 31.9075 L 1166.87 13.5267 C 1184.98 9.6218 1204.58 3.5952 1223.1 2.70382 C 1227.92 13.2229 1243.3 49.5666 1243.48 59.1636 C 1241.48 63.1086 1237.17 64.3833 1233.13 65.7144 C 1213.13 72.2883 1183.43 75.7795 1165.67 83.6946 C 1165.77 96.0882 1166.22 108.972 1165.32 121.308 C 1165.5 131.861 1165.51 142.417 1165.35 152.971 C 1185.9 153.481 1206.51 152.376 1227.03 153.395 L 1229.57 154.32 C 1231.82 159.785 1230.47 209.232 1228.99 217.22 C 1221.36 221.118 1176.66 218.927 1165.12 218.805 L 1165.69 233.003 C 1186.68 256.95 1208.07 280.214 1228.61 304.623 C 1233.71 310.689 1239.48 316.447 1244.29 322.719 C 1236.36 331.171 1208.23 368.286 1200.38 369.973 C 1196.71 368.94 1195.46 367.551 1193.56 364.221 C 1185.87 350.734 1178.82 336.778 1171.16 323.225 C 1169.29 319.923 1167.83 315.741 1165.57 312.761 L 1161.87 313.52 L 1159.86 313.826 C 1157.91 317.848 1159.13 490.857 1159.14 509.002 C 1145.76 510.751 1115.26 511.795 1102.82 509.487 C 1101.45 507.065 1101.81 504.262 1101.87 501.573 C 1101.28 499.136 1100.77 496.759 1100.43 494.27 L 1098.89 498.558 L 1098.39 503.214 L 1097.35 503.129 L 1095.92 499.799 C 1095.86 471.761 1095.12 443.548 1096.09 415.536 C 1095.38 399.262 1096.02 382.718 1096.5 366.436 C 1095.43 354.843 1098.31 342.422 1096.3 331.133 C 1094.22 332.644 1093.54 335.68 1092.65 338.015 C 1086.5 355.983 1076.78 383.433 1064.1 397.913 L 1060.97 393.867 C 1057.86 398.825 1054.89 404.149 1051.36 408.806 L 1055.69 410.177 C 1050.14 417.984 1039.76 431.194 1031.55 435.802 C 1030.6 436.331 1029.46 435.666 1028.45 435.368 C 1025.23 430.779 1025.33 423.663 1023.19 418.389 C 1017.87 405.294 1004.74 390.662 1003.89 376.48 C 1003.53 370.47 1008.7 365.943 1012.39 361.839 C 1015.56 356.263 1020.27 350.809 1023.91 345.415 C 1031.48 334.187 1038.5 322.905 1045.51 311.328 C 1052.16 296.668 1060.7 282.29 1068.14 267.958 C 1073.98 251.756 1082.05 236.285 1087.94 219.978 C 1064.75 219.246 1041.57 219.542 1018.39 218.85 C 1018.39 197.742 1020.26 175.702 1016.45 154.876 C 1036.44 153.75 1080.04 150.789 1098.35 154.211 C 1096.51 136.91 1101.28 111.579 1097.41 95.7053 L 1095.49 94.8062 L 1094.39 97.2745 C 1080.68 100.429 1036.63 107.213 1023.9 106.893 C 1019.48 88.4613 1013.94 62.5029 1006.6 45.4499 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1006.6 45.4499 C 1031.54 43.1833 1056.7 37.1747 1081.15 31.9075 L 1166.87 13.5267 C 1184.98 9.6218 1204.58 3.5952 1223.1 2.70382 C 1227.92 13.2229 1243.3 49.5666 1243.48 59.1636 C 1241.48 63.1086 1237.17 64.3833 1233.13 65.7144 C 1213.13 72.2883 1183.43 75.7795 1165.67 83.6946 C 1165.77 96.0882 1166.22 108.972 1165.32 121.308 C 1165.17 108.312 1161.23 93.7619 1162.46 81.3738 C 1167.31 72.6395 1224.87 64.7892 1237.57 59.0255 L 1238.01 58.8212 C 1234.92 42.6656 1226.92 21.5456 1218.09 7.64251 C 1183.03 16.4377 1147.04 22.5421 1111.67 29.9825 C 1081.42 36.346 1044.19 46.3595 1014.09 48.6933 C 1017.42 66.1257 1021.73 84.9866 1027.72 101.699 C 1037.27 99.5806 1047.53 99.321 1057.25 97.9256 C 1067.59 96.4403 1078.76 93.28 1089.14 93.0457 C 1091.63 92.9897 1093.37 93.5455 1095.49 94.8062 L 1094.39 97.2745 C 1080.68 100.429 1036.63 107.213 1023.9 106.893 C 1019.48 88.4613 1013.94 62.5029 1006.6 45.4499 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1227.03 153.395 L 1229.57 154.32 C 1231.82 159.785 1230.47 209.232 1228.99 217.22 C 1221.36 221.118 1176.66 218.927 1165.12 218.805 L 1165.69 233.003 C 1186.68 256.95 1208.07 280.214 1228.61 304.623 C 1233.71 310.689 1239.48 316.447 1244.29 322.719 C 1236.36 331.171 1208.23 368.286 1200.38 369.973 C 1196.71 368.94 1195.46 367.551 1193.56 364.221 C 1185.87 350.734 1178.82 336.778 1171.16 323.225 C 1169.29 319.923 1167.83 315.741 1165.57 312.761 L 1161.87 313.52 C 1162.79 312.227 1163.93 311.067 1164.99 309.887 L 1166.66 309.967 C 1176.31 318.59 1194.1 354.305 1200.74 367.374 C 1211.15 351.42 1225.19 336.886 1237.77 322.592 C 1232.06 314.885 1224.74 307.856 1218.47 300.554 C 1206.08 286.135 1194.14 271.427 1181.47 257.237 C 1175.55 250.609 1166.98 243.796 1162.42 236.366 C 1160.81 233.731 1160.19 229.324 1159.97 226.307 C 1159.71 222.718 1159.8 217.944 1162.42 215.168 C 1165.18 212.253 1216.51 213.835 1224.21 213.874 C 1224.9 199.546 1221.88 165.12 1227.03 153.395 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1016.45 154.876 C 1036.44 153.75 1080.04 150.789 1098.35 154.211 L 1098.86 157.509 C 1093.04 162.702 1033.68 159.729 1021.95 160.202 C 1022.35 178.003 1023.23 196.081 1021.96 213.849 C 1035.1 214.169 1048.2 215.267 1061.34 215.599 C 1068.9 215.79 1077.33 214.713 1084.77 215.904 C 1086.92 216.25 1088.38 217.037 1090.04 218.449 C 1091.77 221.73 1091.02 224.48 1089.98 227.878 C 1085.85 241.274 1077.5 253.557 1073.43 266.75 L 1068.14 267.958 C 1073.98 251.756 1082.05 236.285 1087.94 219.978 C 1064.75 219.246 1041.57 219.542 1018.39 218.85 C 1018.39 197.742 1020.26 175.702 1016.45 154.876 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1096.3 331.133 L 1098.37 332.127 L 1100.45 329.983 C 1102.99 334.17 1102.09 344.035 1102.13 349.091 C 1101.85 376.406 1101.72 403.722 1101.74 431.038 C 1101.9 454.468 1102.81 478.159 1101.87 501.573 C 1101.28 499.136 1100.77 496.759 1100.43 494.27 L 1098.89 498.558 L 1098.39 503.214 L 1097.35 503.129 L 1095.92 499.799 C 1095.86 471.761 1095.12 443.548 1096.09 415.536 C 1095.38 399.262 1096.02 382.718 1096.5 366.436 C 1095.43 354.843 1098.31 342.422 1096.3 331.133 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1098.09 397.344 C 1103.18 430.466 1098.83 465.213 1098.89 498.558 L 1098.39 503.214 L 1097.35 503.129 L 1095.92 499.799 C 1095.86 471.761 1095.12 443.548 1096.09 415.536 C 1096.79 409.475 1097.46 403.411 1098.09 397.344 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1096.3 331.133 L 1098.37 332.127 L 1100.45 329.983 C 1102.99 334.17 1102.09 344.035 1102.13 349.091 C 1101.25 350.375 1099.94 351.819 1099.24 353.183 C 1097.54 356.483 1099.89 371.953 1097.7 373.359 C 1097.32 371.052 1096.98 368.724 1096.5 366.436 C 1095.43 354.843 1098.31 342.422 1096.3 331.133 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1055.69 410.177 C 1050.14 417.984 1039.76 431.194 1031.55 435.802 C 1030.6 436.331 1029.46 435.666 1028.45 435.368 C 1025.23 430.779 1025.33 423.663 1023.19 418.389 C 1017.87 405.294 1004.74 390.662 1003.89 376.48 C 1003.53 370.47 1008.7 365.943 1012.39 361.839 C 1012 367.193 1010.01 370.558 1007.25 375.007 C 1015.56 392.717 1024.97 410.345 1031.75 428.703 C 1038.97 422.747 1045.27 415.897 1051.36 408.806 L 1055.69 410.177 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1060.97 393.867 C 1069.61 382.495 1075.33 368.04 1080.97 354.989 C 1082.91 350.497 1084.77 344.948 1087.38 340.887 C 1088.86 338.592 1090.22 338.608 1092.65 338.015 C 1086.5 355.983 1076.78 383.433 1064.1 397.913 L 1060.97 393.867 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1045.51 311.328 C 1052.16 296.668 1060.7 282.29 1068.14 267.958 L 1073.43 266.75 C 1068.67 277.741 1064.11 289.546 1056.19 298.684 L 1056.92 297.011 C 1053.86 301.211 1050 309.144 1045.51 311.328 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1024.1 600.841 C 1047.97 600.009 1071.69 601.581 1095.51 600.771 C 1099.64 604.33 1170.28 752.401 1171.95 760.294 C 1176.61 751.322 1180.84 742.243 1184.93 732.998 C 1191.38 719.083 1197.76 705.128 1205.41 691.816 C 1205.98 686.662 1243.62 609.471 1248.51 603.039 C 1259.03 598.373 1283.35 601.001 1295.44 601.178 C 1301.24 601.05 1315.75 599.339 1320.16 602.87 C 1319.94 639.745 1319.9 676.621 1320.06 713.496 C 1320.06 731.326 1320.78 749.534 1319.91 767.322 C 1319.01 759.086 1318.34 750.784 1316.68 742.659 C 1315.21 769.119 1316.01 795.856 1316.08 822.353 C 1316.12 834.41 1317.27 848.465 1315.16 860.225 L 1314.9 871.753 C 1301.12 871.207 1287.28 871.195 1273.48 870.972 C 1271.7 825.968 1273.02 780.543 1273.06 735.492 C 1273.08 716.382 1273.89 696.863 1272.4 677.824 L 1267.99 676.603 L 1267.63 675.295 L 1266.67 675.836 C 1264.44 681.942 1261.74 687.639 1258.79 693.429 C 1245.24 722.308 1231.21 750.965 1216.72 779.388 C 1206.98 798.855 1196.52 817.923 1188.53 838.222 C 1177.64 838.068 1166.43 838.44 1155.58 837.607 C 1145.89 810.774 1132.07 786.333 1119.58 760.765 C 1111.04 746.688 1105.01 731.328 1097.53 716.692 C 1089.74 705.324 1083.36 685.16 1075.1 671.059 C 1074.43 686.924 1075.9 703.244 1075.93 719.184 C 1076.03 771.654 1076.95 824.35 1075.59 876.794 C 1062.17 877.872 1037.93 878.989 1025.08 876.684 C 1021.51 865.099 1023.86 801.739 1023.86 784.074 C 1023.85 723.089 1022.53 661.788 1024.1 600.841 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1257.38 685.941 C 1258.15 683.808 1258.89 681.379 1260.01 679.406 C 1261.53 676.728 1263.97 676.529 1266.67 675.836 C 1264.44 681.942 1261.74 687.639 1258.79 693.429 C 1245.24 722.308 1231.21 750.965 1216.72 779.388 C 1206.98 798.855 1196.52 817.923 1188.53 838.222 C 1177.64 838.068 1166.43 838.44 1155.58 837.607 C 1145.89 810.774 1132.07 786.333 1119.58 760.765 L 1124.51 760.253 C 1131.96 774.613 1139.34 789.102 1146.23 803.735 C 1150.72 813.264 1154.39 823.749 1160.18 832.525 C 1168.3 832.727 1176.37 832.42 1184.47 832.109 C 1191.31 822.822 1199.47 801.76 1204.97 790.568 C 1222.11 755.658 1239.16 720.284 1257.38 685.941 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1095.51 600.771 C 1099.64 604.33 1170.28 752.401 1171.95 760.294 L 1169.41 762.968 C 1164.11 755.23 1160.62 746.15 1156.61 737.705 L 1139.57 702.304 C 1125.34 672.546 1108.39 642.438 1097.1 611.494 C 1095.78 607.861 1094.88 604.678 1095.51 600.771 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1295.44 601.178 C 1301.24 601.05 1315.75 599.339 1320.16 602.87 C 1319.94 639.745 1319.9 676.621 1320.06 713.496 C 1320.06 731.326 1320.78 749.534 1319.91 767.322 C 1319.01 759.086 1318.34 750.784 1316.68 742.659 C 1315.21 769.119 1316.01 795.856 1316.08 822.353 C 1316.12 834.41 1317.27 848.465 1315.16 860.225 C 1313.6 841.169 1315.01 820.666 1315 801.472 C 1314.97 750.692 1316.1 699.905 1315.32 649.126 C 1315.09 633.754 1316.66 617.607 1314.96 602.396 C 1308.48 602.278 1301.81 602.444 1295.44 601.178 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1184.93 732.998 C 1191.38 719.083 1197.76 705.128 1205.41 691.816 C 1205.06 700.163 1194.49 725.473 1189.98 733.031 C 1186.31 741.754 1181.62 758.633 1173.98 764.009 L 1171.41 764.502 L 1169.41 762.968 L 1171.95 760.294 C 1176.61 751.322 1180.84 742.243 1184.93 732.998 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1100.83 713.389 C 1109.2 728.732 1116.18 744.852 1124.51 760.253 L 1119.58 760.765 C 1111.04 746.688 1105.01 731.328 1097.53 716.692 L 1100.83 713.389 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1075.1 671.059 L 1075.12 668.605 L 1076.38 667.77 C 1085.98 673.264 1094.56 702.882 1100.83 713.389 L 1097.53 716.692 C 1089.74 705.324 1083.36 685.16 1075.1 671.059 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1862.25 670.478 C 1880.93 667.773 1896.95 668.992 1915.11 674.171 C 1939.98 682.89 1958.96 697.763 1970.61 721.844 C 1974.85 730.544 1977.65 739.876 1978.9 749.474 C 1979.65 762.044 1982.27 780.333 1977.85 792.266 C 1965.44 793.497 1952.36 792.896 1939.89 792.928 L 1868.62 792.706 C 1859.85 792.641 1842.58 790.411 1835.05 793.02 C 1835.06 794.206 1835.12 795.382 1835.26 796.561 C 1836.75 808.376 1843.6 819.298 1852.87 826.583 C 1870.62 840.532 1902.35 844.521 1923.16 835.886 C 1932.33 832.08 1942.21 824.064 1952.03 822.909 C 1961.27 830.284 1969.55 841.111 1977.55 849.937 C 1977.46 850.379 1977.35 850.82 1977.23 851.254 C 1975.77 856.368 1968.02 860.401 1963.63 862.842 C 1930.23 881.449 1894.48 889.16 1856.87 878.407 C 1827.99 870.151 1807.38 854.607 1792.81 828.361 C 1777.42 800.661 1777.72 765.932 1786.24 736.177 C 1789.08 728.564 1792.41 721.458 1796.62 714.498 C 1812 689.073 1834.13 677.333 1862.25 670.478 z M 1832.77 750.534 L 1832.6 753.744 C 1837.03 756.175 1919.83 755.14 1931.45 754.943 L 1932.41 754.175 C 1932.45 753.78 1932.48 753.383 1932.48 752.986 C 1932.63 742.832 1926.78 730.587 1919.78 723.426 C 1909.85 713.275 1898.11 709.974 1884.23 709.793 C 1869.9 710.091 1857.17 712.692 1846.88 723.466 C 1839.09 731.624 1837.4 740.852 1832.77 750.534 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1977.85 792.266 C 1965.44 793.497 1952.36 792.896 1939.89 792.928 L 1868.62 792.706 C 1859.85 792.641 1842.58 790.411 1835.05 793.02 C 1835.06 794.206 1835.12 795.382 1835.26 796.561 C 1836.75 808.376 1843.6 819.298 1852.87 826.583 C 1870.62 840.532 1902.35 844.521 1923.16 835.886 C 1932.33 832.08 1942.21 824.064 1952.03 822.909 C 1961.27 830.284 1969.55 841.111 1977.55 849.937 C 1977.46 850.379 1977.35 850.82 1977.23 851.254 C 1975.77 856.368 1968.02 860.401 1963.63 862.842 C 1930.23 881.449 1894.48 889.16 1856.87 878.407 C 1827.99 870.151 1807.38 854.607 1792.81 828.361 C 1777.42 800.661 1777.72 765.932 1786.24 736.177 C 1787.98 750.767 1786.53 764.692 1786.75 779.273 C 1787.18 807.634 1796.6 836.528 1819.59 854.608 C 1845.01 874.601 1879.25 880.104 1910.71 876.175 C 1933.33 873.35 1953.2 863.533 1972.08 851.205 C 1964.55 843.368 1956.75 835.676 1950.16 827.017 C 1927.95 841.131 1903.52 849.225 1877.18 843.435 C 1861.67 840.028 1846.84 831.35 1838.17 817.8 C 1835.25 813.243 1828.18 798.871 1829.17 793.863 C 1829.7 791.142 1831.65 789.098 1833.82 787.559 C 1842.07 786.012 1851.78 786.992 1860.2 787.011 L 1909.58 787.109 C 1930 787.139 1954.01 785.182 1973.85 787.28 C 1975.72 788.666 1976.68 790.318 1977.85 792.266 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1915.11 674.171 C 1939.98 682.89 1958.96 697.763 1970.61 721.844 C 1974.85 730.544 1977.65 739.876 1978.9 749.474 C 1976.96 750.058 1976.53 750.472 1974.6 749.698 C 1969.64 741.386 1968.35 730.085 1964.59 721.074 C 1956.37 701.386 1935.75 687.009 1916.71 679.202 L 1915.11 674.171 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1862.25 670.478 C 1880.93 667.773 1896.95 668.992 1915.11 674.171 L 1916.71 679.202 C 1910.32 678.039 1904.34 675.455 1897.84 674.535 C 1885.79 672.831 1873.74 675.126 1862.25 670.478 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1832.77 750.534 L 1829.62 750.529 L 1828.67 748.633 C 1829.3 737.861 1837.01 726.372 1844.52 718.98 C 1856.99 706.716 1869.9 703.755 1887 703.878 C 1884.87 705.53 1882.91 706.77 1880.45 707.863 C 1881.61 709.367 1882.43 709.3 1884.23 709.793 C 1869.9 710.091 1857.17 712.692 1846.88 723.466 C 1839.09 731.624 1837.4 740.852 1832.77 750.534 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1887 703.878 C 1898.99 703.387 1912.03 709.519 1920.7 717.574 C 1927.81 724.175 1934.63 734.806 1934.93 744.727 C 1935.03 748.027 1933.13 751.045 1932.41 754.175 C 1932.45 753.78 1932.48 753.383 1932.48 752.986 C 1932.63 742.832 1926.78 730.587 1919.78 723.426 C 1909.85 713.275 1898.11 709.974 1884.23 709.793 C 1882.43 709.3 1881.61 709.367 1880.45 707.863 C 1882.91 706.77 1884.87 705.53 1887 703.878 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1509.72 674.522 C 1524.52 673.337 1539.79 674.077 1554.65 674.261 C 1557.15 674.728 1557.81 674.604 1559.48 676.666 C 1558.01 694.465 1559.82 713.654 1559.82 731.649 C 1559.8 780.374 1560.59 829.246 1558.87 877.94 C 1547.23 877.733 1536.18 877.872 1524.56 878.672 C 1520.77 878.933 1516.8 879.432 1513.78 876.608 C 1508.56 871.724 1510.06 853.707 1509.97 846.797 L 1509.79 847.192 C 1502.71 862.277 1488.37 872.827 1472.93 878.369 C 1453.48 885.349 1426.75 884.654 1408.04 875.717 C 1392.29 868.197 1378.44 855.365 1372.67 838.624 C 1370 830.902 1368.77 822.571 1368.28 814.438 C 1367.6 803.093 1366.65 679.902 1368.75 675.904 C 1369.14 675.169 1372 674.526 1372.89 674.222 L 1418.14 674.141 C 1420.83 717.929 1417.54 762.305 1418.98 806.2 C 1422.11 817.652 1425.69 826.327 1436.65 832.502 C 1445.66 837.578 1455.88 838.986 1466.05 839.365 C 1477.2 837.184 1488.17 833.768 1496.58 825.776 C 1501.63 820.979 1505.09 815.435 1506.6 808.578 C 1510.4 791.304 1508.06 751.908 1508.06 732.246 C 1508.06 712.947 1509.13 693.796 1509.72 674.522 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1554.65 674.261 C 1557.15 674.728 1557.81 674.604 1559.48 676.666 C 1558.01 694.465 1559.82 713.654 1559.82 731.649 C 1559.8 780.374 1560.59 829.246 1558.87 877.94 C 1547.23 877.733 1536.18 877.872 1524.56 878.672 C 1520.77 878.933 1516.8 879.432 1513.78 876.608 C 1508.56 871.724 1510.06 853.707 1509.97 846.797 L 1509.79 847.192 C 1502.71 862.277 1488.37 872.827 1472.93 878.369 C 1453.48 885.349 1426.75 884.654 1408.04 875.717 C 1392.29 868.197 1378.44 855.365 1372.67 838.624 C 1370 830.902 1368.77 822.571 1368.28 814.438 C 1367.6 803.093 1366.65 679.902 1368.75 675.904 C 1369.14 675.169 1372 674.526 1372.89 674.222 C 1373.97 706.292 1373.32 738.434 1373.14 770.517 C 1373.03 788.732 1371.13 809.125 1374.92 826.979 C 1377.5 839.125 1382.79 850.808 1391.94 859.405 C 1406.05 872.651 1426.3 877.804 1445.22 877.145 C 1464.85 876.461 1482.26 871.621 1495.84 856.903 C 1499.93 852.472 1503.37 847.412 1509.7 846.33 C 1511.94 847.139 1512.87 847.422 1513.89 849.756 C 1516.34 855.324 1514.82 864.949 1514.82 871.08 C 1527.54 871.567 1540.27 871.176 1552.99 870.937 C 1553.87 824.628 1554.2 778.311 1553.97 731.995 C 1554.02 713.159 1551.42 692.76 1554.65 674.261 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1466.05 839.365 C 1460.8 840.484 1454.13 841.156 1448.72 841.827 C 1443.07 842.527 1432.96 836.371 1428.56 832.871 C 1422.64 828.169 1416.78 821.805 1416.07 813.947 C 1415.74 810.314 1416.8 808.888 1418.98 806.2 C 1422.11 817.652 1425.69 826.327 1436.65 832.502 C 1445.66 837.578 1455.88 838.986 1466.05 839.365 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1686.26 344.071 C 1707.65 342.909 1729.57 343.183 1750.96 344.27 C 1750.64 367.963 1751.27 391.664 1750.83 415.359 C 1750.86 424.891 1749.2 442.359 1756.24 449.563 C 1766.3 459.867 1812.95 456.58 1827.95 456.475 C 1841.65 456.38 1870.45 459.419 1880.46 449.032 C 1888.5 440.687 1885.34 417.979 1885.04 407.161 C 1904.77 411.367 1923.75 420.442 1943.77 422.862 C 1942.66 447.472 1945.63 480.992 1925.66 499.182 C 1921.62 502.861 1916.86 505.713 1912.23 508.577 C 1899.91 512.096 1887.55 513.952 1874.72 513.965 C 1852.29 515.323 1829.28 514.138 1806.77 514.038 C 1782.8 513.932 1756.97 515.717 1733.3 512.005 C 1723.94 510.537 1716.17 508.099 1708.33 502.759 C 1706.03 501.144 1703.64 499.314 1701.61 497.37 C 1692.41 488.551 1687.08 471.178 1686.84 458.827 C 1683.92 449.183 1686.41 362.882 1686.26 344.071 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1885.04 407.161 C 1904.77 411.367 1923.75 420.442 1943.77 422.862 C 1942.66 447.472 1945.63 480.992 1925.66 499.182 C 1921.62 502.861 1916.86 505.713 1912.23 508.577 L 1911.28 504.136 C 1915.44 502.702 1919.3 499.244 1922.45 496.226 C 1940.11 479.268 1937.38 449.249 1937.84 426.814 C 1922.1 423.748 1906.09 418.259 1890.65 413.853 C 1890.07 425.165 1891.37 437.638 1887.43 448.4 C 1886.72 449.133 1886 449.858 1885.28 450.574 C 1880.75 455.058 1876.08 457.795 1869.89 459.539 C 1852.13 464.546 1813.56 462.296 1793.97 462.144 C 1780.89 462.042 1761.44 462.112 1751.64 451.774 C 1745.35 445.138 1746.06 434.717 1746.3 426.279 C 1746.93 431.815 1747.5 437.616 1748.83 443.028 C 1749.44 445.512 1750.21 448.166 1752.52 449.568 C 1753.18 446.212 1750.35 441.943 1749.6 438.52 C 1748.06 431.552 1748.69 422.103 1750.83 415.359 C 1750.86 424.891 1749.2 442.359 1756.24 449.563 C 1766.3 459.867 1812.95 456.58 1827.95 456.475 C 1841.65 456.38 1870.45 459.419 1880.46 449.032 C 1888.5 440.687 1885.34 417.979 1885.04 407.161 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1686.84 458.827 C 1688.01 461.379 1688.99 465.914 1690.92 467.655 C 1691.87 466.271 1691.48 463.959 1691.43 462.325 C 1694.33 473.282 1696.57 485.677 1704.41 494.304 C 1707.04 497.198 1709.82 498.543 1708.33 502.759 C 1706.03 501.144 1703.64 499.314 1701.61 497.37 C 1692.41 488.551 1687.08 471.178 1686.84 458.827 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1874.72 513.965 C 1884.8 507.63 1899.8 506.316 1911.28 504.136 L 1912.23 508.577 C 1899.91 512.096 1887.55 513.952 1874.72 513.965 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1334.48 13.9338 C 1356.82 11.7735 1380.56 13.0312 1403.03 13.3648 L 1403.28 178.728 C 1403.28 195.197 1405.61 341.541 1400.41 346.659 C 1395.43 351.552 1351.55 348.949 1342.66 348.814 C 1339.99 348.69 1337.1 348.93 1334.83 347.359 C 1332.39 340.545 1334.27 288.507 1334.28 277.413 L 1334.35 117.816 C 1334.4 83.3054 1332.82 48.3826 1334.48 13.9338 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1334.48 13.9338 C 1356.82 11.7735 1380.56 13.0312 1403.03 13.3648 L 1403.28 178.728 C 1401.91 176.692 1400.72 174.599 1400.4 172.133 C 1398.71 159.197 1399.79 144.773 1399.73 131.669 C 1399.57 94.1965 1401.33 56.787 1399.74 19.3588 C 1379.58 18.9091 1359.2 20.014 1339.11 19.094 L 1339.07 238.008 L 1339.09 306.158 C 1339.09 317.838 1338.29 330.152 1339.46 341.758 C 1339.75 344.669 1340.69 346.635 1342.66 348.814 C 1339.99 348.69 1337.1 348.93 1334.83 347.359 C 1332.39 340.545 1334.27 288.507 1334.28 277.413 L 1334.35 117.816 C 1334.4 83.3054 1332.82 48.3826 1334.48 13.9338 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1437.81 285.832 C 1450.19 288.726 1462.02 297.18 1473.33 302.966 C 1482.13 307.466 1491.19 311.454 1500.05 315.823 C 1468.93 380.537 1435.5 424.255 1374.31 463.085 C 1372.58 462.337 1371.34 461.017 1369.98 459.756 C 1363.85 463.458 1357.79 467.159 1352.21 471.672 C 1349.51 473.859 1347.67 476.765 1344.74 478.628 L 1345.07 480.114 C 1328.25 490.37 1289 509.674 1270.37 513.707 C 1261.26 516.702 1250.77 518.647 1241.24 519.589 C 1238.19 514.057 1211.75 461.266 1211.64 459.456 C 1216.22 456.162 1224.83 455.283 1230.37 453.943 C 1241.44 450.161 1253.02 447.622 1263.92 443.393 C 1315.24 423.471 1374.73 386.65 1407.33 341.82 C 1414.57 329.092 1422.18 316.586 1429.37 303.818 C 1432.11 297.845 1434.63 291.579 1437.81 285.832 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1437.81 285.832 C 1450.19 288.726 1462.02 297.18 1473.33 302.966 C 1482.13 307.466 1491.19 311.454 1500.05 315.823 C 1468.93 380.537 1435.5 424.255 1374.31 463.085 C 1372.58 462.337 1371.34 461.017 1369.98 459.756 C 1426.79 425.832 1465.42 376.585 1493.43 317.476 C 1475.76 310.947 1456.81 301.346 1440.7 291.567 C 1438.7 295.452 1436.95 299.465 1435.16 303.447 L 1429.37 303.818 C 1432.11 297.845 1434.63 291.579 1437.81 285.832 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1230.37 453.943 C 1228.42 456.645 1218.11 459.493 1218.02 460.316 L 1218.7 460.176 C 1221.03 459.699 1223.16 459.584 1225.54 459.633 L 1217.79 462.212 C 1225.83 479.691 1234.34 497.962 1244.02 514.565 C 1249.63 513.139 1263.55 508.21 1268.53 510.972 L 1270.37 513.707 C 1261.26 516.702 1250.77 518.647 1241.24 519.589 C 1238.19 514.057 1211.75 461.266 1211.64 459.456 C 1216.22 456.162 1224.83 455.283 1230.37 453.943 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1429.37 303.818 L 1435.16 303.447 C 1430.04 313.293 1419.07 335.18 1410.87 341.762 C 1409.54 342.046 1408.67 342.252 1407.33 341.82 C 1414.57 329.092 1422.18 316.586 1429.37 303.818 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1660.73 669.673 C 1691.27 663.698 1723.37 673.239 1750.64 686.914 C 1748.74 693.578 1747.37 700.914 1744.12 707.051 C 1741.44 712.983 1740.14 719.451 1736.87 725.075 C 1724.35 720.183 1708.64 716.616 1697.19 711.593 C 1686.82 709.514 1677.27 709.645 1666.76 710.499 C 1659.37 713.565 1653.87 717.201 1647.56 722.098 C 1647.48 726.491 1646.52 732.962 1648.38 736.963 C 1655.47 743.461 1668.99 746.473 1678.22 748.439 L 1702.01 755.475 C 1720 759.434 1741.31 768.689 1751.6 784.857 C 1759.88 797.869 1761.62 814.771 1758.3 829.632 C 1754.46 846.773 1744.67 860.635 1729.81 869.99 C 1703.68 886.436 1670.98 886.434 1641.75 879.628 C 1626.4 876.056 1609.07 868.968 1595.67 860.538 C 1597.39 852.348 1601.78 829.076 1608.58 824.321 C 1610.45 823.011 1612.66 822.913 1614.82 823.39 C 1625.26 825.698 1636.36 832.834 1646.67 836.633 C 1662.24 842.154 1679.68 845.475 1695.84 840.318 C 1704.38 835.618 1707.96 831.049 1711.52 822.188 C 1711.02 819.924 1710.55 817.503 1709.64 815.365 C 1702.17 797.866 1651.68 791.019 1633.95 784.064 C 1621.36 779.127 1608.54 770.922 1603.04 757.996 C 1596.23 741.965 1598.23 720.557 1604.61 704.766 C 1605.41 703.433 1606.23 702.119 1607.08 700.824 C 1620.56 680.411 1637.68 674.379 1660.73 669.673 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1662.99 706.269 C 1665.64 707.659 1665.67 707.712 1666.76 710.499 C 1659.37 713.565 1653.87 717.201 1647.56 722.098 C 1647.48 726.491 1646.52 732.962 1648.38 736.963 C 1655.47 743.461 1668.99 746.473 1678.22 748.439 L 1702.01 755.475 C 1720 759.434 1741.31 768.689 1751.6 784.857 C 1759.88 797.869 1761.62 814.771 1758.3 829.632 C 1754.46 846.773 1744.67 860.635 1729.81 869.99 C 1703.68 886.436 1670.98 886.434 1641.75 879.628 C 1626.4 876.056 1609.07 868.968 1595.67 860.538 C 1597.39 852.348 1601.78 829.076 1608.58 824.321 C 1610.45 823.011 1612.66 822.913 1614.82 823.39 C 1625.26 825.698 1636.36 832.834 1646.67 836.633 C 1643.63 836.776 1640.71 836.742 1637.67 836.503 C 1636.84 836.919 1637.22 836.76 1636.53 837.011 C 1627.33 835.457 1620.88 831.105 1613.15 826.317 C 1607.95 836.107 1604.22 846.187 1601.04 856.792 C 1609.69 862.988 1619.7 867.705 1629.82 870.931 C 1657.33 879.709 1695.82 882.263 1722.23 868.386 C 1735.23 861.558 1746.87 849.806 1751.25 835.565 C 1755.74 820.97 1755.24 802.618 1747.98 788.976 C 1737.89 770.007 1715.12 764.503 1696.12 758.726 C 1689.65 758.071 1682.91 754.848 1676.34 753.58 C 1665.89 750.175 1650.3 748.155 1644.27 737.694 C 1640.53 731.202 1642.44 725 1644.3 718.319 C 1650.11 712.622 1655.37 709.153 1662.99 706.269 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1604.61 704.766 C 1604.65 704.931 1604.69 705.095 1604.72 705.261 C 1606.57 714.069 1604.59 723.13 1604.06 732 C 1603.15 747.367 1608.62 763.736 1621.8 772.532 C 1642.73 786.497 1709.86 790.767 1715.08 816.492 C 1715.38 817.993 1715.52 819.528 1715.65 821.052 C 1715.64 826.168 1714.21 831.196 1710.47 834.895 C 1705.89 839.433 1702.14 840.336 1695.84 840.318 C 1704.38 835.618 1707.96 831.049 1711.52 822.188 C 1711.02 819.924 1710.55 817.503 1709.64 815.365 C 1702.17 797.866 1651.68 791.019 1633.95 784.064 C 1621.36 779.127 1608.54 770.922 1603.04 757.996 C 1596.23 741.965 1598.23 720.557 1604.61 704.766 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1660.73 669.673 C 1691.27 663.698 1723.37 673.239 1750.64 686.914 C 1748.74 693.578 1747.37 700.914 1744.12 707.051 C 1743.86 704.044 1743.45 701.754 1742.18 699.003 L 1744.59 692.183 C 1743.42 686.139 1736.45 684.167 1731.19 682.4 C 1722.53 679.487 1713.68 676.986 1704.63 675.613 C 1694.02 674.002 1668.52 675.896 1660.73 669.673 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1742.18 699.003 C 1743.45 701.754 1743.86 704.044 1744.12 707.051 C 1741.44 712.983 1740.14 719.451 1736.87 725.075 C 1724.35 720.183 1708.64 716.616 1697.19 711.593 L 1698.34 710.376 L 1697.85 706.372 C 1709.78 708.728 1723.46 713.101 1733.88 719.387 C 1736.54 712.553 1739.41 705.79 1742.18 699.003 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1662.99 706.269 C 1669.26 704.599 1691.65 703.746 1697.85 706.372 L 1698.34 710.376 L 1697.19 711.593 C 1686.82 709.514 1677.27 709.645 1666.76 710.499 C 1665.67 707.712 1665.64 707.659 1662.99 706.269 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1258.68 97.9654 C 1258.78 95.7311 1258.73 95.0922 1259.93 93.2493 C 1269.17 89.7092 1284.92 93.6455 1294.73 94.3416 C 1302.83 94.6407 1310.94 95.0189 1319.05 94.9453 C 1318.84 158.821 1313.68 223.32 1298.94 285.621 C 1297.27 292.678 1295.18 307.259 1289.98 312.386 C 1288.65 313.7 1287.75 313.835 1285.97 313.726 C 1280.92 313.417 1274.6 310.454 1270.19 308.075 L 1248.85 298.083 C 1242.9 295.861 1234.86 292.165 1230.6 287.398 C 1230.17 283.138 1243.32 235.723 1245.03 225.775 C 1252.33 183.217 1253.42 140.652 1258.68 97.9654 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1258.68 97.9654 C 1258.78 95.7311 1258.73 95.0922 1259.93 93.2493 C 1269.17 89.7092 1284.92 93.6455 1294.73 94.3416 C 1302.83 94.6407 1310.94 95.0189 1319.05 94.9453 C 1318.84 158.821 1313.68 223.32 1298.94 285.621 C 1297.27 292.678 1295.18 307.259 1289.98 312.386 C 1288.65 313.7 1287.75 313.835 1285.97 313.726 C 1280.92 313.417 1274.6 310.454 1270.19 308.075 L 1248.85 298.083 C 1242.9 295.861 1234.86 292.165 1230.6 287.398 C 1230.17 283.138 1243.32 235.723 1245.03 225.775 C 1252.33 183.217 1253.42 140.652 1258.68 97.9654 C 1259.26 107.744 1260.44 117.631 1260.36 127.422 C 1259.9 180.228 1248.47 234.324 1236.2 285.407 C 1240.84 288.244 1245.46 291.176 1250.18 293.872 C 1258.28 297.285 1266.98 300.327 1274.73 304.453 C 1278.62 306.181 1282.38 308.059 1286.15 310.04 C 1293.78 288.631 1298.19 265.477 1301.85 243.09 C 1304.24 228.47 1307.34 213.733 1308.91 199.016 C 1312.41 166.45 1312.91 132.73 1314.04 99.9949 L 1303.44 98.1334 C 1288.55 98.2177 1273.57 98.4942 1258.68 97.9654 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1463.42 274.843 C 1461.03 275.192 1458.49 275.875 1456.27 274.663 C 1446.55 255.916 1438.37 217.855 1432.83 196.518 C 1424.73 165.393 1416.04 134.213 1408.98 102.85 C 1411.91 100.732 1427.26 99.1802 1431.79 98.0583 C 1443.09 95.2586 1459.4 86.9186 1470.22 86.6587 C 1478.75 106.288 1520.34 240.83 1518.4 258.035 C 1516.64 260.62 1515.26 260.835 1512.43 262.046 C 1503.51 261.363 1473.57 271.946 1463.42 274.843 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1463.42 274.843 C 1461.03 275.192 1458.49 275.875 1456.27 274.663 C 1446.55 255.916 1438.37 217.855 1432.83 196.518 C 1424.73 165.393 1416.04 134.213 1408.98 102.85 C 1411.91 100.732 1427.26 99.1802 1431.79 98.0583 C 1443.09 95.2586 1459.4 86.9186 1470.22 86.6587 C 1478.75 106.288 1520.34 240.83 1518.4 258.035 C 1516.64 260.62 1515.26 260.835 1512.43 262.046 L 1511.19 258.062 C 1511.9 257.421 1513.47 256.424 1513.54 255.487 C 1514.08 247.799 1510.44 236.888 1508.55 229.285 C 1501.38 200.343 1492.38 171.486 1483.16 143.133 C 1477.82 126.685 1470.04 107.797 1467.23 90.9897 C 1449.17 98.559 1433.96 103.983 1414.18 103.679 C 1419.68 129.612 1450.58 261.478 1463.42 274.843 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1968.55 328.412 C 1970.72 327.76 1972.59 326.613 1974.74 327.692 C 1982.4 331.543 2038.58 442.052 2045.53 454.665 C 2031.84 463.552 2003.54 483.539 1988.69 488.08 C 1970.75 451.652 1951.21 415.038 1930.27 380.217 C 1926.46 373.882 1912.12 358.194 1912.72 351.658 C 1915.95 349.185 1920.14 348.408 1924 347.357 C 1931.55 344.442 1939.23 341.687 1946.42 337.945 C 1953.84 334.875 1961.22 331.697 1968.55 328.412 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1968.55 328.412 C 1970.72 327.76 1972.59 326.613 1974.74 327.692 C 1982.4 331.543 2038.58 442.052 2045.53 454.665 C 2031.84 463.552 2003.54 483.539 1988.69 488.08 C 1970.75 451.652 1951.21 415.038 1930.27 380.217 C 1926.46 373.882 1912.12 358.194 1912.72 351.658 C 1915.95 349.185 1920.14 348.408 1924 347.357 C 1923.4 351.102 1920.9 352.233 1919.88 355.655 C 1946.18 396.818 1966.33 440.802 1991.23 482.623 C 2007.43 473.201 2023.75 463.749 2038.88 452.656 C 2034.41 442.441 2028.21 432.519 2022.97 422.638 C 2013.67 405.102 2004.65 387.312 1994.85 370.049 C 1988.47 358.801 1979.43 346.546 1975.41 334.321 C 1975.25 333.524 1975.07 332.574 1974.65 331.869 C 1973.18 329.373 1971.09 329.179 1968.55 328.412 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1968.55 328.412 C 1971.09 329.179 1973.18 329.373 1974.65 331.869 C 1975.07 332.574 1975.25 333.524 1975.41 334.321 C 1970.94 332.417 1956.18 339.404 1946.42 337.945 C 1953.84 334.875 1961.22 331.697 1968.55 328.412 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1604.08 358.446 C 1604.2 355.183 1604.99 351.131 1605.79 347.956 C 1606.33 345.823 1607.65 343.985 1609.59 342.928 C 1617.1 338.853 1631.66 345.014 1639.53 347.12 C 1650.47 349.158 1661.25 351.703 1672.06 354.334 C 1659.76 406.552 1656.12 437.8 1624.18 483.834 C 1621.36 487.894 1617.44 496.537 1613.36 498.899 C 1611.54 499.952 1609.99 499.306 1608.16 498.645 C 1603.31 496.888 1598.26 493.639 1593.87 490.9 C 1586.6 488.498 1579.26 482.7 1572.86 478.484 C 1568.23 475.515 1561.73 472.898 1558.37 468.516 C 1557.44 462.974 1574.21 440.015 1577.66 433.243 C 1585.54 417.755 1593.09 399.948 1597.92 383.226 C 1600.29 375.042 1601.68 366.634 1604.08 358.446 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1639.53 347.12 C 1650.47 349.158 1661.25 351.703 1672.06 354.334 C 1659.76 406.552 1656.12 437.8 1624.18 483.834 C 1621.36 487.894 1617.44 496.537 1613.36 498.899 C 1611.54 499.952 1609.99 499.306 1608.16 498.645 C 1603.31 496.888 1598.26 493.639 1593.87 490.9 L 1598.37 487.952 C 1602.29 490.158 1605.83 492.375 1609.33 495.236 C 1641.87 461.602 1658.84 402.8 1665.94 357.572 C 1660.05 356.21 1654.15 354.947 1648.31 353.383 C 1648.99 353.187 1649.53 353.034 1650.25 353.021 C 1655.32 352.932 1662.26 354.092 1666.08 357.482 C 1670.45 369.742 1649.92 426.867 1645.26 439.899 L 1645.85 439.779 C 1656.11 420.323 1660.79 398.671 1665.53 377.343 C 1666.83 371.515 1668.82 366.079 1668.95 360.046 C 1669.16 349.844 1645.67 353.48 1639.53 347.12 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1572.86 478.484 C 1568.23 475.515 1561.73 472.898 1558.37 468.516 C 1557.44 462.974 1574.21 440.015 1577.66 433.243 C 1585.54 417.755 1593.09 399.948 1597.92 383.226 C 1600.29 375.042 1601.68 366.634 1604.08 358.446 C 1604.74 365.667 1603.07 373.348 1602.05 380.505 C 1603.03 376.337 1603.99 372.161 1604.91 367.98 C 1604.27 387.324 1587.87 422.435 1578.43 439.752 C 1573.38 449.017 1567.03 458.018 1563.52 467.998 C 1567.23 470.098 1570.92 472.314 1574.71 474.262 L 1572.86 478.484 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" d="M 1574.71 474.262 C 1581.13 478.09 1593.25 483.61 1598.37 487.952 L 1593.87 490.9 C 1586.6 488.498 1579.26 482.7 1572.86 478.484 L 1574.71 474.262 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 210.512 303.979 C 212.896 298.215 218.021 292.777 223.161 289.438 C 231.868 283.781 241.066 282.421 251.159 284.58 C 261.104 285.013 270.184 290.877 276.803 298.138 C 284.456 306.532 286.804 316.158 286.192 327.235 C 285.513 339.537 281.7 352.043 272.213 360.438 C 264.162 367.562 253.037 370.669 242.407 369.901 C 231.592 369.102 221.546 364.008 214.51 355.756 C 206.988 346.862 204.102 334.645 205.078 323.189 C 205.629 316.717 207.414 309.696 210.512 303.979 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 251.159 284.58 C 261.104 285.013 270.184 290.877 276.803 298.138 C 284.456 306.532 286.804 316.158 286.192 327.235 C 285.513 339.537 281.7 352.043 272.213 360.438 C 264.162 367.562 253.037 370.669 242.407 369.901 C 231.592 369.102 221.546 364.008 214.51 355.756 C 206.988 346.862 204.102 334.645 205.078 323.189 C 205.629 316.717 207.414 309.696 210.512 303.979 C 212.863 320.548 205.226 334.056 216.582 349.813 C 221.998 357.329 230.707 362.055 239.76 363.524 C 248.677 364.971 259.591 364.14 266.981 358.575 C 274.844 352.655 280.299 336.764 281.372 327.351 C 282.807 314.767 274.521 299.523 263.095 293.7 C 259.094 291.661 249.84 290.221 247.204 286.888 C 247.737 286.785 248.002 286.693 248.523 286.719 C 252.723 286.934 268.875 294.67 271.819 297.659 C 281.419 307.404 282.844 319.203 282.731 332.276 L 283.397 332.191 L 283.491 330.446 C 284.594 307.901 280.538 298.119 259.393 288.233 C 256.682 286.966 253.455 287.344 251.435 284.945 C 251.336 284.829 251.251 284.702 251.159 284.58 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 405.775 585.787 C 402.286 584 398.737 582.435 396.1 579.441 L 396.173 578.049 C 414.175 571.945 434.199 570.435 447.739 555.563 C 451.705 551.207 467.115 525.303 469.081 524.849 C 469.427 524.769 470.483 525.078 470.886 525.144 C 472.583 548.13 480.476 567.719 498.098 582.985 C 502.139 586.486 517.009 594.541 518.474 597.064 C 517.594 599.336 515.543 600.338 513.436 601.275 C 505.224 604.927 496.149 606.07 487.796 609.663 C 475.159 616.204 462.947 628.68 456.767 641.508 C 454.428 646.362 452.452 652.908 448.461 656.58 L 446.773 656.483 C 444.116 650.832 444.886 641.677 443.524 635.347 C 438.787 613.324 424.282 597.46 405.775 585.787 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 405.775 585.787 C 409.338 585.087 411.516 586.974 414.456 588.922 C 422.805 594.453 429.948 602.779 435.358 611.129 C 436.234 612.482 437.263 615.281 438.812 615.161 C 438.445 608.42 425.972 595.687 420.978 591.23 C 417.107 587.775 413.126 585.155 408.536 582.754 L 408.555 582.048 C 422.566 586.434 434.168 601.621 440.723 614.215 C 445.105 622.632 444.938 633.874 449.711 641.576 C 453.927 639.297 461.966 613.967 483.317 608.424 L 478.55 610.815 L 478.805 611.142 C 481.476 610.248 484.154 609.375 486.839 608.523 L 487.796 609.663 C 475.159 616.204 462.947 628.68 456.767 641.508 C 454.428 646.362 452.452 652.908 448.461 656.58 L 446.773 656.483 C 444.116 650.832 444.886 641.677 443.524 635.347 C 438.787 613.324 424.282 597.46 405.775 585.787 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(23,30,47)" d="M 1803.05 318.144 C 1816.9 328.396 1829.2 342.719 1841.34 354.945 C 1850.51 364.185 1860.98 373.401 1869.13 383.536 C 1864.21 392.856 1841.35 416.739 1831.82 420.308 C 1828.69 419.912 1826.84 418.064 1824.55 415.972 C 1814.44 406.707 1805.51 394.919 1796.47 384.558 C 1788.37 375.28 1779.6 366.52 1772.04 356.799 C 1770.51 354.836 1767.08 351.135 1767.46 348.594 C 1767.69 347.041 1769.03 345.378 1769.89 344.09 C 1771.44 342.481 1773.17 341.022 1774.83 339.526 C 1783.59 331.421 1793.08 324.646 1803.05 318.144 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" d="M 1803.05 318.144 C 1816.9 328.396 1829.2 342.719 1841.34 354.945 C 1850.51 364.185 1860.98 373.401 1869.13 383.536 C 1864.21 392.856 1841.35 416.739 1831.82 420.308 C 1828.69 419.912 1826.84 418.064 1824.55 415.972 C 1814.44 406.707 1805.51 394.919 1796.47 384.558 C 1788.37 375.28 1779.6 366.52 1772.04 356.799 C 1770.51 354.836 1767.08 351.135 1767.46 348.594 C 1767.69 347.041 1769.03 345.378 1769.89 344.09 C 1770.78 347.369 1772.03 350.104 1774.02 352.875 C 1779.13 359.973 1785.71 366.103 1791.54 372.605 C 1804.73 387.32 1818.42 402.335 1830.22 418.183 C 1841.2 406.851 1853.01 396.325 1863.99 384.947 C 1854.41 374.027 1843.29 363.941 1833.02 353.635 C 1822.96 343.541 1813.04 332.114 1801.79 323.366 C 1797.29 327.355 1792.79 331.646 1787.93 335.178 C 1783.82 338.163 1780 340.314 1774.83 339.526 C 1783.59 331.421 1793.08 324.646 1803.05 318.144 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 445.897 250.388 C 449.325 246.881 456.128 244.104 460.925 242.921 C 469.145 240.895 475.293 241.862 482.484 246.097 C 491.384 250.018 498.032 255.541 501.663 264.868 C 505.5 274.725 503.67 286.246 499.378 295.673 C 495.288 304.656 488.547 311.581 479.176 315.066 C 470.634 318.243 460.901 318.567 452.537 314.674 C 444.78 311.064 437.485 303.083 434.372 295.102 C 430.932 288.26 430.416 281.74 431.633 274.237 C 433.149 264.886 438.193 255.998 445.897 250.388 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 472.668 248.788 C 488.812 251.718 499.974 266.59 498.277 282.91 C 496.579 299.23 482.596 311.487 466.194 311.032 C 449.793 310.577 436.511 297.563 435.721 281.174 C 435.287 272.158 438.756 263.392 445.242 257.115 C 452.508 250.083 462.718 246.983 472.668 248.788 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(241,25,50)" d="M 445.897 250.388 C 445.973 253.142 442.428 256.106 443.685 257.49 L 445.242 257.115 C 438.756 263.392 435.287 272.158 435.721 281.174 L 434.494 279.664 C 433.38 284.252 433.671 290.428 434.372 295.102 C 430.932 288.26 430.416 281.74 431.633 274.237 C 433.149 264.886 438.193 255.998 445.897 250.388 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 376.604 379.194 C 384.836 378.003 394.172 379.707 401.018 384.463 C 407.857 389.215 412.169 396.668 413.657 404.791 C 415.561 415.181 414.546 427.19 408.255 435.969 C 402.123 444.527 393.347 448.061 383.349 449.728 C 374.452 449.598 366.757 448.063 359.368 442.715 C 351.971 437.36 346.912 429.624 345.634 420.523 C 344.299 411.009 346.208 400.522 352.162 392.843 C 358.046 385.253 367.162 380.35 376.604 379.194 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 245.278 481.533 C 257.539 479.362 270.02 483.955 277.95 493.555 C 285.88 503.155 288.032 516.279 283.585 527.909 C 279.138 539.54 268.778 547.878 256.466 549.738 C 237.761 552.563 220.261 539.826 217.199 521.159 C 214.137 502.492 226.651 484.832 245.278 481.533 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(246,42,69)" d="M 581.532 430.797 C 583.737 426.223 585.763 420.476 589.684 417.162 L 591.653 417.292 C 594.088 420.177 593.57 425.309 594.04 428.969 C 596.145 445.356 610.271 455.983 623.899 463.083 C 606.858 469.748 593.857 473.295 582.916 489.14 C 581.766 491.553 581.517 494.399 580.229 496.771 C 579.253 498.57 578.644 499.099 576.752 499.681 C 573.484 495.743 574.503 486.104 572.779 481.118 C 571.347 476.976 567.817 473.984 566.688 469.622 C 562.444 466.354 558.851 462.241 554.464 458.93 C 552.165 457.194 548.388 454.735 547.974 451.776 C 551.501 446.938 564.993 444.86 570.638 441.417 C 574.943 438.791 577.163 433.481 581.532 430.797 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(251,23,37)" d="M 581.532 430.797 C 583.737 426.223 585.763 420.476 589.684 417.162 L 591.653 417.292 C 594.088 420.177 593.57 425.309 594.04 428.969 C 596.145 445.356 610.271 455.983 623.899 463.083 C 606.858 469.748 593.857 473.295 582.916 489.14 C 581.766 491.553 581.517 494.399 580.229 496.771 C 579.253 498.57 578.644 499.099 576.752 499.681 C 573.484 495.743 574.503 486.104 572.779 481.118 C 571.347 476.976 567.817 473.984 566.688 469.622 C 571.173 471.572 572.524 477.478 575.983 480.956 L 574.541 477.616 C 577.199 480.049 578.267 482.89 579.609 486.173 C 581.592 483.925 582.987 480.98 584.97 478.61 C 591.857 470.376 600.56 466.217 610.139 461.923 C 604.823 456.132 598.001 450.067 593.691 443.548 C 591.471 440.19 590.468 436.192 588.242 432.738 L 585.264 432.437 C 585.278 431.088 585.42 429.746 585.536 428.403 C 584.408 429.65 583.607 430.186 581.984 430.67 C 581.834 430.715 581.683 430.755 581.532 430.797 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" fill-opacity="0.988235" d="M 1268.53 876.747 C 1267.01 839.046 1267.99 800.953 1268.02 763.211 C 1268.04 734.464 1266.89 705.284 1267.99 676.603 L 1272.4 677.824 C 1273.89 696.863 1273.08 716.382 1273.06 735.492 C 1273.02 780.543 1271.7 825.968 1273.48 870.972 C 1287.28 871.195 1301.12 871.207 1314.9 871.753 L 1315.16 860.225 C 1317.27 848.465 1316.12 834.41 1316.08 822.353 C 1316.01 795.856 1315.21 769.119 1316.68 742.659 C 1318.34 750.784 1319.01 759.086 1319.91 767.322 C 1320.82 777.589 1320.18 788.461 1320.16 798.785 C 1320.1 824.831 1319.18 851.171 1320.3 877.184 C 1303.1 877.483 1285.72 877.43 1268.53 876.747 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" fill-opacity="0.988235" d="M 1161.87 313.52 L 1165.57 312.761 C 1163.16 322.307 1165.36 349.256 1165.36 360.697 C 1165.34 411.592 1166.05 462.605 1165.23 513.484 C 1146.99 516.978 1114.16 518.771 1096.1 514.8 L 1095.92 499.799 L 1097.35 503.129 L 1098.39 503.214 L 1098.89 498.558 L 1100.43 494.27 C 1100.77 496.759 1101.28 499.136 1101.87 501.573 C 1101.81 504.262 1101.45 507.065 1102.82 509.487 C 1115.26 511.795 1145.76 510.751 1159.14 509.002 C 1159.13 490.857 1157.91 317.848 1159.86 313.826 L 1161.87 313.52 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(15,17,24)" fill-opacity="0.984314" d="M 1345.07 480.114 L 1344.74 478.628 C 1347.67 476.765 1349.51 473.859 1352.21 471.672 C 1357.79 467.159 1363.85 463.458 1369.98 459.756 C 1371.34 461.017 1372.58 462.337 1374.31 463.085 C 1365.98 469.573 1354.87 476.027 1345.07 480.114 z"/>
|
||||
<path transform="translate(0,0)" fill="rgb(0,0,1)" fill-opacity="0.984314" d="M 1051.36 408.806 C 1054.89 404.149 1057.86 398.825 1060.97 393.867 L 1064.1 397.913 C 1061.59 402.37 1058.98 406.26 1055.69 410.177 L 1051.36 408.806 z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 58 KiB |
@ -1,101 +0,0 @@
|
||||
---
|
||||
name: rd
|
||||
description: 评审 DevelopmentPlan.md,检查技术可行性和与上游文档一致性,输出结构化评审报告。
|
||||
---
|
||||
|
||||
# Review DevelopmentPlan
|
||||
|
||||
当用户调用 `/rd` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/DevelopmentPlan.md` - 目标文档(必须存在)
|
||||
2. `doc/FeatureSummary.md` - 上游参照文档
|
||||
|
||||
如果 DevelopmentPlan.md 不存在,提示用户:
|
||||
> DevelopmentPlan.md 不存在,请先使用 `/wd` 生成开发计划。
|
||||
|
||||
## 2. 评审维度
|
||||
|
||||
### 2.1 与 FeatureSummary 一致性检查
|
||||
|
||||
- 开发任务是否覆盖所有功能模块
|
||||
- 技术方案是否支撑功能需求
|
||||
- 排期是否合理
|
||||
|
||||
### 2.2 技术可行性检查
|
||||
|
||||
- 技术方案是否可行
|
||||
- 技术栈选择是否合理
|
||||
- 是否存在技术风险
|
||||
- 依赖关系是否明确
|
||||
|
||||
### 2.3 完整性检查
|
||||
|
||||
- 是否有明确的里程碑划分
|
||||
- 是否有资源分配说明
|
||||
- 是否有风险应对措施
|
||||
|
||||
## 3. 生成评审报告
|
||||
|
||||
输出到 `doc/review-DevelopmentPlan.md`,结构如下:
|
||||
|
||||
```markdown
|
||||
# DevelopmentPlan 评审报告
|
||||
|
||||
## 概要
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 评审时间 | {YYYY-MM-DD HH:MM} |
|
||||
| 目标文档 | doc/DevelopmentPlan.md |
|
||||
| 参照文档 | doc/FeatureSummary.md |
|
||||
| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 |
|
||||
|
||||
## 功能覆盖分析
|
||||
|
||||
| FeatureSummary 功能 | DevelopmentPlan 对应 | 状态 |
|
||||
|---------------------|----------------------|------|
|
||||
| {功能名} | {对应任务/模块} | ✅/⚠️/❌ |
|
||||
|
||||
## 技术风险分析
|
||||
|
||||
| 风险项 | 影响范围 | 严重程度 | 建议措施 |
|
||||
|--------|----------|----------|----------|
|
||||
| {风险} | {范围} | 高/中/低 | {措施} |
|
||||
|
||||
## 问题清单
|
||||
|
||||
### 严重问题 (Critical)
|
||||
{问题列表,含位置引用}
|
||||
|
||||
### 一般问题 (Major)
|
||||
{问题列表,含位置引用}
|
||||
|
||||
### 改进建议 (Minor)
|
||||
{建议列表}
|
||||
|
||||
## 评审结论
|
||||
|
||||
{通过 / 需修改后通过 / 不通过}
|
||||
|
||||
### 下一步行动
|
||||
- [ ] {待办事项}
|
||||
```
|
||||
|
||||
## 4. 输出规范
|
||||
|
||||
- 输出语言:中文
|
||||
- 问题分级:Critical / Major / Minor
|
||||
- 包含文件引用(如 `doc/DevelopmentPlan.md:28`)
|
||||
- 技术风险需明确影响范围和应对建议
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 只做评审,不修改原文档
|
||||
- 重点关注技术可行性和风险
|
||||
- 评审报告保存后,建议用户根据问题运行 `/md` 修改
|
||||
@ -1,96 +0,0 @@
|
||||
---
|
||||
name: rf
|
||||
description: 评审 FeatureSummary.md,对比 PRD 检查一致性,输出结构化评审报告。
|
||||
---
|
||||
|
||||
# Review FeatureSummary
|
||||
|
||||
当用户调用 `/rf` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/FeatureSummary.md` - 目标文档(必须存在)
|
||||
2. `doc/PRD.md` - 上游参照文档
|
||||
|
||||
如果 FeatureSummary.md 不存在,提示用户:
|
||||
> FeatureSummary.md 不存在,请先使用 `/wf` 生成功能摘要。
|
||||
|
||||
## 2. 评审维度
|
||||
|
||||
### 2.1 与 PRD 一致性检查
|
||||
|
||||
- 功能模块是否完整覆盖 PRD 3.2 功能详情
|
||||
- 功能描述是否与 PRD 一致
|
||||
- 优先级标注是否与 PRD 匹配
|
||||
|
||||
### 2.2 完整性检查
|
||||
|
||||
- 每个功能模块是否有清晰的描述
|
||||
- 是否遗漏 PRD 中的功能点
|
||||
- 功能分类是否合理
|
||||
|
||||
### 2.3 质量检查
|
||||
|
||||
- 描述是否简洁准确
|
||||
- 是否有冗余或重复内容
|
||||
- 格式是否规范统一
|
||||
|
||||
## 3. 生成评审报告
|
||||
|
||||
输出到 `doc/review-FeatureSummary.md`,结构如下:
|
||||
|
||||
```markdown
|
||||
# FeatureSummary 评审报告
|
||||
|
||||
## 概要
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 评审时间 | {YYYY-MM-DD HH:MM} |
|
||||
| 目标文档 | doc/FeatureSummary.md |
|
||||
| 参照文档 | doc/PRD.md |
|
||||
| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 |
|
||||
|
||||
## 覆盖度分析
|
||||
|
||||
| PRD 功能模块 | FeatureSummary 对应 | 状态 |
|
||||
|--------------|---------------------|------|
|
||||
| {模块名} | {对应位置} | ✅/⚠️/❌ |
|
||||
|
||||
**覆盖率**: X/Y 完全覆盖
|
||||
|
||||
## 问题清单
|
||||
|
||||
### 严重问题 (Critical)
|
||||
{问题列表,含位置引用}
|
||||
|
||||
### 一般问题 (Major)
|
||||
{问题列表,含位置引用}
|
||||
|
||||
### 改进建议 (Minor)
|
||||
{建议列表}
|
||||
|
||||
## 评审结论
|
||||
|
||||
{通过 / 需修改后通过 / 不通过}
|
||||
|
||||
### 下一步行动
|
||||
- [ ] {待办事项}
|
||||
```
|
||||
|
||||
## 4. 输出规范
|
||||
|
||||
- 输出语言:中文
|
||||
- 问题分级:Critical / Major / Minor
|
||||
- 包含文件引用(如 `doc/FeatureSummary.md:15`)
|
||||
- 问题按严重性排序
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 只做评审,不修改原文档
|
||||
- 重点检查与 PRD 的一致性
|
||||
- 评审报告保存后,建议用户根据问题运行 `/mf` 修改
|
||||
@ -1,177 +0,0 @@
|
||||
---
|
||||
name: rp
|
||||
description: 评审 PRD.md,对比 RequirementsDoc 检查一致性,输出结构化评审报告。
|
||||
---
|
||||
|
||||
# Review PRD
|
||||
|
||||
当用户调用 `/rp` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取文档
|
||||
|
||||
读取以下文件:
|
||||
- 目标文档:`doc/PRD.md`
|
||||
- 上游文档:`doc/RequirementsDoc.md`
|
||||
|
||||
如果 PRD.md 不存在,提示用户:
|
||||
> PRD.md 不存在,请先使用 `/wp` 生成 PRD。
|
||||
|
||||
如果 RequirementsDoc.md 不存在,提示用户:
|
||||
> RequirementsDoc.md 不存在,无法进行一致性检查。请先创建需求文档。
|
||||
|
||||
## 2. 评审维度
|
||||
|
||||
PRD 位于文档链的第二层,需要对比上游 RequirementsDoc 进行评审。
|
||||
|
||||
### 2.1 与 RequirementsDoc 的一致性
|
||||
|
||||
- [ ] PRD 是否覆盖了 RequirementsDoc 中的所有功能需求
|
||||
- [ ] PRD 是否覆盖了 RequirementsDoc 中的所有非功能需求
|
||||
- [ ] PRD 中是否有 RequirementsDoc 中未提及的需求(需标注来源)
|
||||
- [ ] 术语定义是否与 RequirementsDoc 一致
|
||||
- [ ] 优先级划分是否与 RequirementsDoc 一致
|
||||
|
||||
### 2.2 用户故事质量
|
||||
|
||||
- [ ] 所有用户故事是否有唯一 ID(US-xxx)
|
||||
- [ ] 用户故事是否符合格式:作为{角色},我想要{功能},以便{价值}
|
||||
- [ ] 用户角色是否明确定义
|
||||
- [ ] 验收标准是否具体可测试
|
||||
- [ ] 用户旅程是否完整描述核心流程
|
||||
|
||||
### 2.3 功能需求完整性
|
||||
|
||||
- [ ] 功能架构是否清晰(模块划分合理)
|
||||
- [ ] 所有功能点是否关联到用户故事
|
||||
- [ ] 功能点是否有明确的优先级
|
||||
- [ ] 功能点是否有验收标准
|
||||
- [ ] 是否遗漏边界情况和异常处理
|
||||
|
||||
### 2.4 非功能需求
|
||||
|
||||
- [ ] 性能需求是否有量化指标
|
||||
- [ ] 安全需求是否明确
|
||||
- [ ] 兼容性需求是否完整
|
||||
- [ ] 可用性需求是否可验证
|
||||
|
||||
### 2.5 文档结构
|
||||
|
||||
- [ ] 文档结构是否完整(无空章节)
|
||||
- [ ] 格式是否统一(表格、列表、标题层级)
|
||||
- [ ] 术语表是否完整
|
||||
|
||||
## 3. 生成评审报告
|
||||
|
||||
按以下格式输出评审报告:
|
||||
|
||||
```markdown
|
||||
# PRD 评审报告
|
||||
|
||||
## 概要
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 评审时间 | {YYYY-MM-DD HH:mm} |
|
||||
| 目标文档 | doc/PRD.md |
|
||||
| 参照文档 | doc/RequirementsDoc.md |
|
||||
| 问题统计 | {critical} 个严重 / {major} 个一般 / {minor} 个建议 |
|
||||
|
||||
## 一致性检查
|
||||
|
||||
### 需求覆盖分析
|
||||
|
||||
| RequirementsDoc 需求项 | PRD 对应位置 | 状态 |
|
||||
|------------------------|--------------|------|
|
||||
| {需求1} | {PRD章节/用户故事ID} | ✅ 已覆盖 / ⚠️ 部分覆盖 / ❌ 未覆盖 |
|
||||
|
||||
### 差异说明
|
||||
|
||||
{列出 PRD 中新增的、RequirementsDoc 未提及的内容,需说明来源或理由}
|
||||
|
||||
## 问题清单
|
||||
|
||||
### 严重问题 (Critical)
|
||||
|
||||
> 必须修复,否则影响后续文档生成
|
||||
|
||||
1. **[位置: doc/PRD.md:行号]** 问题描述
|
||||
- 现状:...
|
||||
- 与 RequirementsDoc 的差异:...
|
||||
- 建议:...
|
||||
|
||||
### 一般问题 (Major)
|
||||
|
||||
> 建议修复,可提升文档质量
|
||||
|
||||
1. **[位置]** 问题描述
|
||||
- 建议:...
|
||||
|
||||
### 改进建议 (Minor)
|
||||
|
||||
> 可选优化项
|
||||
|
||||
1. **[位置]** 建议内容
|
||||
|
||||
## 用户故事评估
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 用户故事总数 | {数量} |
|
||||
| 符合格式规范 | {数量} / {总数} |
|
||||
| 有验收标准 | {数量} / {总数} |
|
||||
| 关联功能点 | {数量} / {总数} |
|
||||
|
||||
### 用户故事问题
|
||||
|
||||
{列出不符合规范的用户故事}
|
||||
|
||||
## 评审结论
|
||||
|
||||
{通过 / 需修改后通过 / 不通过}
|
||||
|
||||
**结论说明**:
|
||||
- 通过:PRD 与 RequirementsDoc 一致,可进入下一阶段
|
||||
- 需修改后通过:存在问题但不影响整体理解,修复后可继续
|
||||
- 不通过:存在严重一致性问题或遗漏,需重新生成
|
||||
|
||||
### 下一步行动
|
||||
|
||||
- [ ] 行动项1
|
||||
- [ ] 行动项2
|
||||
```
|
||||
|
||||
## 4. 保存报告
|
||||
|
||||
将评审报告保存到 `doc/review-PRD.md`。
|
||||
|
||||
如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。
|
||||
|
||||
## 5. 输出摘要
|
||||
|
||||
向用户展示评审摘要:
|
||||
- 一致性检查结果(覆盖率)
|
||||
- 发现的问题数量(按严重程度分类)
|
||||
- 用户故事评估结果
|
||||
- 评审结论
|
||||
- 报告文件路径
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 评审时保持客观,聚焦于文档质量和一致性
|
||||
- 问题描述要具体,给出明确的位置引用(如 `doc/PRD.md:42`)
|
||||
- 一致性检查要逐项对比,不能遗漏
|
||||
- 建议要可操作,避免模糊表述
|
||||
- 不要修改原文档,只输出评审报告
|
||||
|
||||
## 常见问题模式
|
||||
|
||||
在评审时重点关注以下常见问题:
|
||||
|
||||
1. **需求遗漏**:RequirementsDoc 中有但 PRD 中没有的需求
|
||||
2. **需求偏离**:PRD 中的描述与 RequirementsDoc 不一致
|
||||
3. **凭空添加**:PRD 中有但 RequirementsDoc 中没有的需求(需要来源说明)
|
||||
4. **用户故事缺陷**:格式不规范、缺少验收标准、角色不明确
|
||||
5. **功能孤立**:功能点未关联到任何用户故事
|
||||
6. **优先级冲突**:PRD 与 RequirementsDoc 的优先级划分不一致
|
||||
@ -1,111 +0,0 @@
|
||||
---
|
||||
name: rr
|
||||
description: 评审 RequirementsDoc.md,检查需求文档的完整性、清晰度和可执行性,输出结构化评审报告。
|
||||
---
|
||||
|
||||
# Review RequirementsDoc
|
||||
|
||||
当用户调用 `/rr` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取目标文档
|
||||
|
||||
读取 `doc/RequirementsDoc.md` 文件。
|
||||
|
||||
如果文件不存在,提示用户:
|
||||
> RequirementsDoc.md 不存在,请先创建需求文档。
|
||||
|
||||
## 2. 评审维度
|
||||
|
||||
RequirementsDoc 是文档链的源头,没有上游依赖。重点检查以下维度:
|
||||
|
||||
### 2.1 完整性
|
||||
- [ ] 产品概述是否清晰(定位、目标用户、核心价值)
|
||||
- [ ] 功能需求是否完整列出
|
||||
- [ ] 非功能需求是否涵盖(性能、安全、兼容性)
|
||||
- [ ] 数据规范是否明确(输入输出格式、字段定义)
|
||||
- [ ] 边界条件和异常情况是否考虑
|
||||
|
||||
### 2.2 清晰度
|
||||
- [ ] 术语定义是否一致,无歧义
|
||||
- [ ] 用例描述是否具体可理解
|
||||
- [ ] 优先级是否明确标注
|
||||
- [ ] 是否有模糊表述("等"、"可能"、"应该"等)
|
||||
|
||||
### 2.3 可执行性
|
||||
- [ ] 需求是否可被验证(有明确的验收标准)
|
||||
- [ ] 技术约束是否合理
|
||||
- [ ] 依赖项是否明确
|
||||
|
||||
### 2.4 结构规范
|
||||
- [ ] 文档结构是否清晰(章节划分合理)
|
||||
- [ ] 格式是否统一(表格、列表、标题层级)
|
||||
|
||||
## 3. 生成评审报告
|
||||
|
||||
按以下格式输出评审报告:
|
||||
|
||||
```markdown
|
||||
# RequirementsDoc 评审报告
|
||||
|
||||
## 概要
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 评审时间 | {YYYY-MM-DD HH:mm} |
|
||||
| 目标文档 | doc/RequirementsDoc.md |
|
||||
| 问题统计 | {critical} 个严重 / {major} 个一般 / {minor} 个建议 |
|
||||
|
||||
## 问题清单
|
||||
|
||||
### 严重问题 (Critical)
|
||||
|
||||
> 必须修复,否则影响后续文档生成
|
||||
|
||||
1. **[位置: 第X节/第Y行]** 问题描述
|
||||
- 现状:...
|
||||
- 建议:...
|
||||
|
||||
### 一般问题 (Major)
|
||||
|
||||
> 建议修复,可提升文档质量
|
||||
|
||||
1. **[位置]** 问题描述
|
||||
- 建议:...
|
||||
|
||||
### 改进建议 (Minor)
|
||||
|
||||
> 可选优化项
|
||||
|
||||
1. **[位置]** 建议内容
|
||||
|
||||
## 评审结论
|
||||
|
||||
{通过 / 需修改后通过 / 不通过}
|
||||
|
||||
### 下一步行动
|
||||
|
||||
- [ ] 行动项1
|
||||
- [ ] 行动项2
|
||||
```
|
||||
|
||||
## 4. 保存报告
|
||||
|
||||
将评审报告保存到 `doc/review-RequirementsDoc.md`。
|
||||
|
||||
如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。
|
||||
|
||||
## 5. 输出摘要
|
||||
|
||||
向用户展示评审摘要:
|
||||
- 发现的问题数量(按严重程度分类)
|
||||
- 评审结论
|
||||
- 报告文件路径
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 评审时保持客观,聚焦于文档质量而非业务判断
|
||||
- 问题描述要具体,给出明确的位置引用
|
||||
- 建议要可操作,避免模糊表述
|
||||
- 不要修改原文档,只输出评审报告
|
||||
@ -1,115 +0,0 @@
|
||||
---
|
||||
name: rt
|
||||
description: 评审 tasks.md,检查任务完整性和与上游文档一致性,输出结构化评审报告。
|
||||
---
|
||||
|
||||
# Review Tasks
|
||||
|
||||
当用户调用 `/rt` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/tasks.md` - 目标文档(必须存在)
|
||||
2. `doc/UIDesign.md` - 上游参照文档
|
||||
3. `doc/DevelopmentPlan.md` - 上游参照文档
|
||||
|
||||
如果 tasks.md 不存在,提示用户:
|
||||
> tasks.md 不存在,请先使用 `/wt` 生成任务列表。
|
||||
|
||||
## 2. 评审维度
|
||||
|
||||
### 2.1 与上游文档一致性检查
|
||||
|
||||
- 任务是否覆盖 DevelopmentPlan 所有开发项
|
||||
- 任务是否覆盖 UIDesign 所有页面实现
|
||||
- 任务优先级是否与功能优先级匹配
|
||||
|
||||
### 2.2 任务完整性检查
|
||||
|
||||
- 每个任务是否有明确的描述
|
||||
- 任务粒度是否合适(不过大也不过小)
|
||||
- 任务依赖关系是否明确
|
||||
- 验收标准是否清晰
|
||||
|
||||
### 2.3 可执行性检查
|
||||
|
||||
- 任务是否可直接开始执行
|
||||
- 是否有阻塞项未说明
|
||||
- 估时是否合理(如有)
|
||||
|
||||
## 3. 生成评审报告
|
||||
|
||||
输出到 `doc/review-tasks.md`,结构如下:
|
||||
|
||||
```markdown
|
||||
# Tasks 评审报告
|
||||
|
||||
## 概要
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 评审时间 | {YYYY-MM-DD HH:MM} |
|
||||
| 目标文档 | doc/tasks.md |
|
||||
| 参照文档 | doc/UIDesign.md, doc/DevelopmentPlan.md |
|
||||
| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 |
|
||||
|
||||
## 覆盖度分析
|
||||
|
||||
### DevelopmentPlan 覆盖
|
||||
|
||||
| 开发项 | 对应任务 | 状态 |
|
||||
|--------|----------|------|
|
||||
| {开发项} | {任务ID/名称} | ✅/⚠️/❌ |
|
||||
|
||||
### UIDesign 覆盖
|
||||
|
||||
| UI 页面 | 对应任务 | 状态 |
|
||||
|---------|----------|------|
|
||||
| {页面名} | {任务ID/名称} | ✅/⚠️/❌ |
|
||||
|
||||
**总覆盖率**: X/Y
|
||||
|
||||
## 任务质量分析
|
||||
|
||||
| 检查项 | 通过数 | 总数 |
|
||||
|--------|--------|------|
|
||||
| 有明确描述 | X | Y |
|
||||
| 有验收标准 | X | Y |
|
||||
| 粒度合适 | X | Y |
|
||||
|
||||
## 问题清单
|
||||
|
||||
### 严重问题 (Critical)
|
||||
{问题列表,含位置引用}
|
||||
|
||||
### 一般问题 (Major)
|
||||
{问题列表,含位置引用}
|
||||
|
||||
### 改进建议 (Minor)
|
||||
{建议列表}
|
||||
|
||||
## 评审结论
|
||||
|
||||
{通过 / 需修改后通过 / 不通过}
|
||||
|
||||
### 下一步行动
|
||||
- [ ] {待办事项}
|
||||
```
|
||||
|
||||
## 4. 输出规范
|
||||
|
||||
- 输出语言:中文
|
||||
- 问题分级:Critical / Major / Minor
|
||||
- 包含文件引用(如 `doc/tasks.md:12`)
|
||||
- 任务问题需说明对开发执行的影响
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 只做评审,不修改原文档
|
||||
- 重点检查任务覆盖度和可执行性
|
||||
- tasks.md 是文档链末端,必须覆盖所有上游功能
|
||||
- 评审报告保存后,建议用户根据问题运行 `/mt` 修改
|
||||
@ -1,105 +0,0 @@
|
||||
---
|
||||
name: ru
|
||||
description: 评审 UIDesign.md,对比 DevelopmentPlan 检查设计一致性,输出结构化评审报告。
|
||||
---
|
||||
|
||||
# Review UIDesign
|
||||
|
||||
当用户调用 `/ru` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/UIDesign.md` - 目标文档(必须存在)
|
||||
2. `doc/DevelopmentPlan.md` - 上游参照文档
|
||||
|
||||
如果 UIDesign.md 不存在,提示用户:
|
||||
> UIDesign.md 不存在,请先使用 `/wu` 生成 UI 设计文档。
|
||||
|
||||
## 2. 评审维度
|
||||
|
||||
### 2.1 与 DevelopmentPlan 一致性检查
|
||||
|
||||
- UI 页面是否覆盖所有功能模块
|
||||
- 交互流程是否与开发计划匹配
|
||||
- 页面结构是否支撑功能需求
|
||||
|
||||
### 2.2 设计完整性检查
|
||||
|
||||
- 页面列表是否完整
|
||||
- 每个页面是否有清晰的布局描述
|
||||
- 交互说明是否充分
|
||||
- 状态变化是否考虑全面(加载、错误、空状态等)
|
||||
|
||||
### 2.3 可用性检查
|
||||
|
||||
- 用户流程是否顺畅
|
||||
- 信息架构是否合理
|
||||
- 是否有一致的设计规范
|
||||
|
||||
## 3. 生成评审报告
|
||||
|
||||
输出到 `doc/review-UIDesign.md`,结构如下:
|
||||
|
||||
```markdown
|
||||
# UIDesign 评审报告
|
||||
|
||||
## 概要
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 评审时间 | {YYYY-MM-DD HH:MM} |
|
||||
| 目标文档 | doc/UIDesign.md |
|
||||
| 参照文档 | doc/DevelopmentPlan.md |
|
||||
| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 |
|
||||
|
||||
## 页面覆盖分析
|
||||
|
||||
| DevelopmentPlan 功能 | UIDesign 页面 | 状态 |
|
||||
|----------------------|---------------|------|
|
||||
| {功能名} | {对应页面} | ✅/⚠️/❌ |
|
||||
|
||||
**覆盖率**: X/Y 完全覆盖
|
||||
|
||||
## 设计一致性检查
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|--------|------|
|
||||
| 页面命名规范 | ✅/❌ |
|
||||
| 布局风格统一 | ✅/❌ |
|
||||
| 交互模式一致 | ✅/❌ |
|
||||
|
||||
## 问题清单
|
||||
|
||||
### 严重问题 (Critical)
|
||||
{问题列表,含位置引用}
|
||||
|
||||
### 一般问题 (Major)
|
||||
{问题列表,含位置引用}
|
||||
|
||||
### 改进建议 (Minor)
|
||||
{建议列表}
|
||||
|
||||
## 评审结论
|
||||
|
||||
{通过 / 需修改后通过 / 不通过}
|
||||
|
||||
### 下一步行动
|
||||
- [ ] {待办事项}
|
||||
```
|
||||
|
||||
## 4. 输出规范
|
||||
|
||||
- 输出语言:中文
|
||||
- 问题分级:Critical / Major / Minor
|
||||
- 包含文件引用(如 `doc/UIDesign.md:45`)
|
||||
- 设计问题需说明影响的用户体验
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 只做评审,不修改原文档
|
||||
- 重点检查页面覆盖度和设计一致性
|
||||
- 评审报告保存后,建议用户根据问题运行 `/mu` 修改
|
||||
@ -1,78 +0,0 @@
|
||||
---
|
||||
name: update
|
||||
aliases: [up]
|
||||
description: 收集用户反馈并更新最近使用的 skill。在用完某个 skill 后调用此命令来优化该 skill。
|
||||
disable-model-invocation: true
|
||||
argument-hint: [skill-name]
|
||||
---
|
||||
|
||||
# Skill 更新助手
|
||||
|
||||
当用户请求使用 `update` 或 `up` skill 时,执行以下步骤:
|
||||
|
||||
## 1. 识别目标 Skill
|
||||
|
||||
**如果用户提供了参数 `$ARGUMENTS`**:
|
||||
- 直接使用指定的 skill 名称作为更新目标
|
||||
|
||||
**如果没有提供参数**:
|
||||
分析当前对话历史,找出最近使用的 skill:
|
||||
- 从最近几轮对话中识别调用过的 skill 名称
|
||||
- 如果找到多个 skill,让用户确认要更新哪一个
|
||||
- 如果没有找到任何 skill 调用记录,提示用户先使用一个 skill
|
||||
|
||||
## 2. 收集用户反馈
|
||||
|
||||
直接向用户询问以下问题:
|
||||
|
||||
**问题 1:这次使用体验如何?**
|
||||
- 很好,skill 完全满足需求
|
||||
- 基本满足,但有改进空间
|
||||
- 不太满意,需要较大调整
|
||||
|
||||
**问题 2:具体需要改进的方面?**(多选)
|
||||
- 执行步骤不够清晰
|
||||
- 缺少某些功能
|
||||
- 输出格式需要调整
|
||||
- 提示词需要优化
|
||||
- 其他(用户自定义输入)
|
||||
|
||||
## 3. 分析优化点
|
||||
|
||||
基于用户反馈和本次 skill 使用过程,分析以下方面:
|
||||
|
||||
1. **执行流程**:哪些步骤可以简化或合并?
|
||||
2. **指令清晰度**:哪些指令描述不够明确?
|
||||
3. **遗漏功能**:有哪些场景没有覆盖到?
|
||||
4. **输出质量**:输出格式是否符合用户预期?
|
||||
|
||||
## 4. 定位 Skill 文件
|
||||
|
||||
按以下优先级搜索 skill 文件:
|
||||
1. 项目级:`.codex/skills/<skill-name>/SKILL.md`
|
||||
2. 用户级:`~/.codex/skills/<skill-name>/SKILL.md`
|
||||
|
||||
## 5. 更新 Skill
|
||||
|
||||
读取现有的 SKILL.md 文件内容,根据分析结果进行更新:
|
||||
|
||||
- 保持 frontmatter 格式不变(除非需要修改 description)
|
||||
- 优化执行步骤的描述
|
||||
- 添加缺失的功能说明
|
||||
- 改进提示词的表达方式
|
||||
- 添加必要的注意事项或边界情况处理
|
||||
|
||||
## 6. 确认更新
|
||||
|
||||
在更新前,向用户展示:
|
||||
- 修改前后的对比(diff 格式)
|
||||
- 说明每处修改的原因
|
||||
|
||||
用户确认后才执行实际的文件更新。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 如果 skill 文件不存在或路径无法确定,提示用户手动指定路径
|
||||
- 更新时保持 skill 的原有风格和结构
|
||||
- 重大修改需要用户明确确认
|
||||
- 保留原有的有效内容,只做增量优化
|
||||
@ -1,323 +0,0 @@
|
||||
---
|
||||
name: wd
|
||||
description: 从上游文档生成 DevelopmentPlan.md,包含技术方案和开发排期。
|
||||
---
|
||||
|
||||
# Write DevelopmentPlan
|
||||
|
||||
> **文档定位**:DevelopmentPlan 是「执行蓝图」文档,偏技术语言和时间约束。定义技术架构、实现方案、开发阶段、里程碑,是开发团队的行动指南。
|
||||
|
||||
当用户调用 `/wd` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取源文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/RequirementsDoc.md` - 必须存在
|
||||
2. `doc/PRD.md` - 必须存在
|
||||
3. `doc/FeatureSummary.md` - 必须存在
|
||||
|
||||
如果文件不存在,提示用户:
|
||||
> 缺少上游文档,请先确保 RequirementsDoc.md、PRD.md 和 FeatureSummary.md 存在。
|
||||
|
||||
如果已存在 `doc/DevelopmentPlan.md`,同时读取作为参考(保持风格一致)。
|
||||
|
||||
## 2. 分析开发需求
|
||||
|
||||
从上游文档中提取以下信息:
|
||||
|
||||
### 2.1 功能需求
|
||||
|
||||
- 从 FeatureSummary 获取功能清单和契约
|
||||
- 从 PRD 获取功能详情和验收标准
|
||||
|
||||
### 2.2 技术约束
|
||||
|
||||
- 从 PRD 获取技术约束
|
||||
- 从 RequirementsDoc 获取技术决策
|
||||
|
||||
### 2.3 优先级排序
|
||||
|
||||
- 按 P0 → P1 → P2 顺序规划开发
|
||||
- 考虑功能依赖关系
|
||||
|
||||
## 3. 生成 DevelopmentPlan
|
||||
|
||||
按以下结构生成文档:
|
||||
|
||||
```markdown
|
||||
# {产品名称} - 开发计划
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 版本 | v1.0 |
|
||||
| 创建日期 | {YYYY-MM-DD} |
|
||||
| 来源文档 | FeatureSummary.md |
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 项目目标
|
||||
|
||||
{从 PRD 提取的项目目标}
|
||||
|
||||
### 1.2 技术栈
|
||||
|
||||
| 层级 | 技术选型 | 版本 | 说明 |
|
||||
|------|----------|------|------|
|
||||
| 前端 | {技术} | {版本} | {说明} |
|
||||
| 后端 | {技术} | {版本} | {说明} |
|
||||
| 数据库 | {技术} | {版本} | {说明} |
|
||||
| 基础设施 | {技术} | {版本} | {说明} |
|
||||
|
||||
### 1.3 开发原则
|
||||
|
||||
{开发规范和原则}
|
||||
|
||||
## 2. 技术架构
|
||||
|
||||
### 2.1 系统架构图
|
||||
|
||||
**【必须】使用架构图展示系统整体结构:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 客户端层 │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Web App │ │ Mobile App │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ │
|
||||
└─────────┼─────────────────┼─────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ API 网关层 │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ API Gateway / Load Balancer │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 服务层 │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ 服务 A │ │ 服务 B │ │ 服务 C │ │
|
||||
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
|
||||
└────────┼───────────────┼───────────────┼────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 数据层 │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ 数据库 │ │ 缓存 │ │ 消息队列 │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 模块依赖图
|
||||
|
||||
**【必须】使用依赖图展示模块间关系:**
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ 模块 A │
|
||||
│ (核心模块) │
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌───┴───┐
|
||||
▼ ▼
|
||||
┌──────┐ ┌──────┐
|
||||
│模块B │ │模块C │
|
||||
└──┬───┘ └──┬───┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐
|
||||
│ 模块 D │
|
||||
│ (基础设施) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### 2.3 数据流图
|
||||
|
||||
**【必须】使用数据流图展示关键数据流转:**
|
||||
|
||||
```
|
||||
用户请求 ──▶ API Gateway ──▶ 服务A ──▶ 数据库
|
||||
│
|
||||
▼
|
||||
缓存层
|
||||
│
|
||||
▼
|
||||
服务B ──▶ 外部API
|
||||
```
|
||||
|
||||
## 3. 开发阶段
|
||||
|
||||
### 3.1 阶段时间线
|
||||
|
||||
**【必须】使用时间线展示开发阶段:**
|
||||
|
||||
```
|
||||
Phase 1 Phase 2 Phase 3
|
||||
│ │ │
|
||||
{起止日期} {起止日期} {起止日期}
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 基础 │ ────▶ │ 核心 │ ────▶ │ 优化 │
|
||||
│ 架构 │ │ 功能 │ │ 扩展 │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
|
||||
交付物: 交付物: 交付物:
|
||||
• {交付1} • {交付1} • {交付1}
|
||||
• {交付2} • {交付2} • {交付2}
|
||||
```
|
||||
|
||||
### 3.2 Phase 1: {阶段名称}
|
||||
|
||||
**目标**: {阶段目标}
|
||||
|
||||
**时间**: {起止日期}
|
||||
|
||||
| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 |
|
||||
|--------|------|------|------|--------|----------|
|
||||
| T-001 | {任务名} | {描述} | - | P0 | F-001 |
|
||||
| T-002 | {任务名} | {描述} | T-001 | P0 | F-002 |
|
||||
|
||||
**阶段依赖图:**
|
||||
|
||||
```
|
||||
T-001 ──▶ T-002 ──▶ T-003
|
||||
│
|
||||
└──▶ T-004
|
||||
```
|
||||
|
||||
{重复以上结构覆盖所有阶段}
|
||||
|
||||
## 4. 技术方案
|
||||
|
||||
### 4.1 {模块名称}
|
||||
|
||||
**功能**: {功能描述}
|
||||
|
||||
**技术选型**:
|
||||
|
||||
| 组件 | 技术 | 选型理由 |
|
||||
|------|------|----------|
|
||||
| {组件} | {技术} | {理由} |
|
||||
|
||||
**架构设计**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ {模块名称} │
|
||||
├─────────────────────────────────────┤
|
||||
│ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ 组件A │ ───▶ │ 组件B │ │
|
||||
│ └─────────┘ └─────────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ 数据层 │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**接口设计**:
|
||||
|
||||
| 接口 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| {接口名} | GET/POST | /api/xxx | {说明} |
|
||||
|
||||
**实现要点**:
|
||||
|
||||
- {技术要点1}
|
||||
- {技术要点2}
|
||||
|
||||
{重复以上结构覆盖所有模块}
|
||||
|
||||
## 5. 风险管理
|
||||
|
||||
| 风险 | 可能性 | 影响 | 应对措施 | 负责人 |
|
||||
|------|--------|------|----------|--------|
|
||||
| {风险} | 高/中/低 | 高/中/低 | {措施} | {负责人} |
|
||||
|
||||
## 6. 里程碑
|
||||
|
||||
**【必须】使用里程碑图展示关键节点:**
|
||||
|
||||
```
|
||||
M1 M2 M3 M4
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
◆───────────────◆───────────────◆───────────────◆
|
||||
│ │ │ │
|
||||
{日期} {日期} {日期} {日期}
|
||||
{里程碑名} {里程碑名} {里程碑名} {里程碑名}
|
||||
```
|
||||
|
||||
| 里程碑 | 日期 | 目标 | 交付物 | 验收标准 |
|
||||
|--------|------|------|--------|----------|
|
||||
| M1 | {日期} | {目标} | {交付物} | {标准} |
|
||||
|
||||
## 7. 资源需求
|
||||
|
||||
| 角色 | 人数 | 职责 | 参与阶段 |
|
||||
|------|------|------|----------|
|
||||
| {角色} | {人数} | {职责} | Phase 1-2 |
|
||||
```
|
||||
|
||||
## 4. 保存文档
|
||||
|
||||
将生成的 DevelopmentPlan 保存到 `doc/DevelopmentPlan.md`。
|
||||
|
||||
如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。
|
||||
|
||||
## 5. 输出摘要
|
||||
|
||||
向用户展示生成摘要:
|
||||
|
||||
- DevelopmentPlan 文件路径
|
||||
- 开发阶段数量
|
||||
- 技术方案模块数量
|
||||
- 建议的下一步操作(运行 `/rd` 评审)
|
||||
|
||||
---
|
||||
|
||||
## 可视化输出要求
|
||||
|
||||
DevelopmentPlan 作为「执行蓝图」文档,需要清晰传达技术方案和时间安排,**必须包含**:
|
||||
|
||||
| 章节 | 可视化形式 | 必要性 |
|
||||
|------|------------|--------|
|
||||
| 2.1 系统架构图 | 架构图(ASCII) | **必须** |
|
||||
| 2.2 模块依赖图 | 依赖图(ASCII) | **必须** |
|
||||
| 2.3 数据流图 | 数据流图(ASCII) | **必须** |
|
||||
| 3.1 阶段时间线 | 时间线(ASCII) | **必须** |
|
||||
| 3.x 阶段依赖图 | 任务依赖图 | **必须** |
|
||||
| 4.x 模块架构 | 模块架构图 | 建议 |
|
||||
| 6. 里程碑 | 里程碑图 | **必须** |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- DevelopmentPlan 使用**技术语言**,面向开发团队
|
||||
- 开发计划必须覆盖 FeatureSummary 所有功能
|
||||
- 技术方案要具体可执行,避免过于抽象
|
||||
- 阶段划分要合理,考虑依赖关系
|
||||
- 时间安排要务实,预留缓冲
|
||||
- 风险评估要全面,有应对措施
|
||||
|
||||
## 质量检查
|
||||
|
||||
生成 DevelopmentPlan 后,自查以下项目:
|
||||
|
||||
- [ ] 覆盖 FeatureSummary 所有功能
|
||||
- [ ] **系统架构图清晰展示整体结构**
|
||||
- [ ] **模块依赖图清晰展示依赖关系**
|
||||
- [ ] **数据流图展示关键数据流转**
|
||||
- [ ] **开发阶段有时间线图**
|
||||
- [ ] **每个阶段有任务依赖图**
|
||||
- [ ] **里程碑有里程碑图**
|
||||
- [ ] 技术方案具体可执行
|
||||
- [ ] 任务 ID 唯一(T-xxx)
|
||||
- [ ] 任务与功能 ID 关联
|
||||
@ -1,234 +0,0 @@
|
||||
---
|
||||
name: wf
|
||||
description: 从 RequirementsDoc.md 和 PRD.md 生成 FeatureSummary.md,提供功能全貌概览。
|
||||
---
|
||||
|
||||
# Write FeatureSummary
|
||||
|
||||
> **文档定位**:FeatureSummary 是「功能契约」文档,是产品与开发的桥梁。精确定义功能边界、输入输出、依赖关系,确保双方对"做什么"达成共识。
|
||||
|
||||
当用户调用 `/wf` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取源文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/RequirementsDoc.md` - 必须存在
|
||||
2. `doc/PRD.md` - 必须存在
|
||||
|
||||
如果文件不存在,提示用户:
|
||||
> 缺少上游文档,请先确保 RequirementsDoc.md 和 PRD.md 存在。
|
||||
|
||||
如果已存在 `doc/FeatureSummary.md`,同时读取作为参考(保持风格一致)。
|
||||
|
||||
## 2. 分析功能需求
|
||||
|
||||
从 PRD 中提取以下信息:
|
||||
|
||||
### 2.1 功能模块
|
||||
|
||||
- 从 PRD 3.1 功能架构提取模块结构
|
||||
- 从 PRD 3.2 功能详情提取各模块功能点
|
||||
|
||||
### 2.2 功能分类
|
||||
|
||||
按以下维度整理功能:
|
||||
|
||||
- 按模块分组
|
||||
- 按优先级标注(P0/P1/P2)
|
||||
- 按用户角色关联
|
||||
|
||||
### 2.3 功能边界
|
||||
|
||||
明确每个功能的:
|
||||
|
||||
- 输入:触发条件、输入数据
|
||||
- 输出:预期结果、输出数据
|
||||
- 边界:不包含什么、异常情况
|
||||
|
||||
## 3. 生成 FeatureSummary
|
||||
|
||||
按以下结构生成文档:
|
||||
|
||||
```markdown
|
||||
# {产品名称} - 功能摘要
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 版本 | v1.0 |
|
||||
| 创建日期 | {YYYY-MM-DD} |
|
||||
| 来源文档 | PRD.md |
|
||||
|
||||
## 1. 功能总览
|
||||
|
||||
### 1.1 功能统计
|
||||
|
||||
| 类别 | 数量 |
|
||||
|------|------|
|
||||
| 功能模块 | X 个 |
|
||||
| P0 功能 | X 个 |
|
||||
| P1 功能 | X 个 |
|
||||
| P2 功能 | X 个 |
|
||||
|
||||
### 1.2 功能架构图
|
||||
|
||||
**【必须】使用模块图展示功能架构和模块关系:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ {产品名称} │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 模块A │ │ 模块B │ │ 模块C │ │
|
||||
│ │ ──────── │ │ ──────── │ │ ──────── │ │
|
||||
│ │ • 功能1 │ │ • 功能1 │ │ • 功能1 │ │
|
||||
│ │ • 功能2 │ │ • 功能2 │ │ │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 模块依赖关系
|
||||
|
||||
**【必须】使用依赖图展示模块间关系:**
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ 模块A │
|
||||
└────┬─────┘
|
||||
│ 依赖
|
||||
▼
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ 模块B │ ◀── │ 模块C │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## 2. 功能清单
|
||||
|
||||
### 2.1 {模块名称}
|
||||
|
||||
**模块职责**: {一句话描述模块职责}
|
||||
|
||||
#### 功能列表
|
||||
|
||||
| ID | 功能 | 描述 | 优先级 | 关联用户故事 |
|
||||
|----|------|------|--------|--------------|
|
||||
| F-001 | {功能名} | {简要描述} | P0 | US-xxx |
|
||||
|
||||
#### 功能契约详情
|
||||
|
||||
**F-001: {功能名}**
|
||||
|
||||
| 契约项 | 说明 |
|
||||
|--------|------|
|
||||
| **触发条件** | {什么情况下触发此功能} |
|
||||
| **输入** | {输入数据/参数} |
|
||||
| **处理逻辑** | {核心处理步骤} |
|
||||
| **输出** | {输出结果/返回值} |
|
||||
| **异常情况** | {可能的错误及处理} |
|
||||
| **边界说明** | {不包含什么、限制条件} |
|
||||
|
||||
{重复以上结构覆盖所有功能}
|
||||
|
||||
{重复以上结构覆盖所有模块}
|
||||
|
||||
## 3. 功能依赖矩阵
|
||||
|
||||
**【必须】使用矩阵表格展示功能间依赖:**
|
||||
|
||||
| 功能 | 依赖 F-001 | 依赖 F-002 | 依赖 F-003 |
|
||||
|------|------------|------------|------------|
|
||||
| F-001 | - | | |
|
||||
| F-002 | ✓ | - | |
|
||||
| F-003 | | ✓ | - |
|
||||
|
||||
说明:
|
||||
- ✓ 表示行功能依赖列功能
|
||||
- 空白表示无依赖
|
||||
|
||||
## 4. 功能流程图
|
||||
|
||||
**【必须】使用流程图展示核心功能流程:**
|
||||
|
||||
### 4.1 {核心流程名称}
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ F-001 │ ──▶ │ F-002 │ ──▶ │ F-003 │ ──▶ │ 完成 │
|
||||
│ {功能} │ │ {功能} │ │ {功能} │ │ │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
│
|
||||
▼ 异常
|
||||
┌─────────┐
|
||||
│ 错误处理 │
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
## 5. 版本规划
|
||||
|
||||
| 版本 | 包含功能 | 功能ID | 目标 |
|
||||
|------|----------|--------|------|
|
||||
| MVP | {功能列表} | F-001, F-002 | {目标} |
|
||||
| v1.1 | {功能列表} | F-003, F-004 | {目标} |
|
||||
| v2.0 | {功能列表} | F-005+ | {目标} |
|
||||
|
||||
## 6. 接口契约预览
|
||||
|
||||
> 详细接口定义在 DevelopmentPlan 中,此处仅列出关键接口
|
||||
|
||||
| 功能 | 接口类型 | 简要说明 |
|
||||
|------|----------|----------|
|
||||
| F-001 | API | {说明} |
|
||||
| F-002 | Event | {说明} |
|
||||
```
|
||||
|
||||
## 4. 保存文档
|
||||
|
||||
将生成的 FeatureSummary 保存到 `doc/FeatureSummary.md`。
|
||||
|
||||
如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。
|
||||
|
||||
## 5. 输出摘要
|
||||
|
||||
向用户展示生成摘要:
|
||||
|
||||
- FeatureSummary 文件路径
|
||||
- 功能模块数量
|
||||
- 各优先级功能数量
|
||||
- 建议的下一步操作(运行 `/rf` 评审)
|
||||
|
||||
---
|
||||
|
||||
## 可视化输出要求
|
||||
|
||||
FeatureSummary 作为「功能契约」文档,需要精确传达功能定义,**必须包含**:
|
||||
|
||||
| 章节 | 可视化形式 | 必要性 |
|
||||
|------|------------|--------|
|
||||
| 1.2 功能架构图 | 模块图(ASCII) | **必须** |
|
||||
| 1.3 模块依赖关系 | 依赖图(ASCII) | **必须** |
|
||||
| 3. 功能依赖矩阵 | 矩阵表格 | **必须** |
|
||||
| 4. 功能流程图 | 流程图(ASCII) | **必须** |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- FeatureSummary 是产品与开发的**桥梁**,语言要精确、无歧义
|
||||
- 功能摘要必须完全来源于 PRD,不要臆造功能
|
||||
- 每个功能必须有明确的**输入、输出、边界**
|
||||
- 功能 ID 必须唯一(F-xxx 格式)
|
||||
- 优先级必须与 PRD 一致
|
||||
- 功能依赖关系必须明确,避免循环依赖
|
||||
|
||||
## 质量检查
|
||||
|
||||
生成 FeatureSummary 后,自查以下项目:
|
||||
|
||||
- [ ] 所有功能都有唯一 ID(F-xxx)
|
||||
- [ ] 所有功能都有契约详情(输入/输出/边界)
|
||||
- [ ] **功能架构图清晰展示模块结构**
|
||||
- [ ] **模块依赖图清晰展示依赖关系**
|
||||
- [ ] **功能依赖矩阵完整**
|
||||
- [ ] **核心流程有流程图**
|
||||
- [ ] 优先级与 PRD 一致
|
||||
- [ ] 无遗漏 PRD 中的功能
|
||||
@ -1,318 +0,0 @@
|
||||
---
|
||||
name: wp
|
||||
description: 从 RequirementsDoc.md 生成 PRD.md,将需求文档转化为结构化的产品需求文档。
|
||||
---
|
||||
|
||||
# Write PRD
|
||||
|
||||
> **文档定位**:PRD 是「价值主张」文档,使用业务语言描述产品要解决什么问题、为谁创造什么价值。面向产品、业务、管理层沟通。
|
||||
|
||||
当用户调用 `/wp` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取源文档
|
||||
|
||||
读取 `doc/RequirementsDoc.md` 文件。
|
||||
|
||||
如果文件不存在,提示用户:
|
||||
> RequirementsDoc.md 不存在,请先创建需求文档。
|
||||
|
||||
如果已存在 `doc/PRD.md`,同时读取作为参考(保持风格一致)。
|
||||
|
||||
## 2. 分析需求文档
|
||||
|
||||
从 RequirementsDoc 中提取以下信息:
|
||||
|
||||
### 2.1 产品定位
|
||||
|
||||
- 产品名称
|
||||
- 目标用户
|
||||
- 核心价值主张
|
||||
- 竞品对比(如有)
|
||||
|
||||
### 2.2 功能需求
|
||||
|
||||
- 功能模块划分
|
||||
- 各模块详细需求
|
||||
- 功能优先级(P0/P1/P2)
|
||||
|
||||
### 2.3 非功能需求
|
||||
|
||||
- 性能要求
|
||||
- 安全要求
|
||||
- 兼容性要求
|
||||
- 可用性要求
|
||||
|
||||
### 2.4 约束条件
|
||||
|
||||
- 技术约束
|
||||
- 业务约束
|
||||
- 时间约束
|
||||
|
||||
## 3. 生成 PRD
|
||||
|
||||
按以下结构生成 PRD 文档:
|
||||
|
||||
```markdown
|
||||
# {产品名称} - 产品需求文档 (PRD)
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 版本 | v1.0 |
|
||||
| 创建日期 | {YYYY-MM-DD} |
|
||||
| 状态 | 草稿 |
|
||||
|
||||
## 1. 产品概述
|
||||
|
||||
### 1.1 产品背景
|
||||
|
||||
{从 RequirementsDoc 提取,说明产品解决的问题和市场机会}
|
||||
|
||||
### 1.2 产品定位
|
||||
|
||||
{目标用户、核心价值、差异化优势}
|
||||
|
||||
### 1.3 产品目标
|
||||
|
||||
| 目标 | 指标 | 衡量方式 |
|
||||
|------|------|----------|
|
||||
| {业务目标} | {量化指标} | {如何衡量} |
|
||||
|
||||
## 2. 用户故事
|
||||
|
||||
PRD 以用户故事为核心驱动,所有功能需求都应对应到具体的用户故事。
|
||||
|
||||
### 2.1 用户角色定义
|
||||
|
||||
| 角色 | 描述 | 核心目标 | 痛点 |
|
||||
|------|------|----------|------|
|
||||
| {角色1} | {角色描述} | {核心目标} | {当前痛点} |
|
||||
|
||||
### 2.2 用户故事列表
|
||||
|
||||
按优先级排列的用户故事:
|
||||
|
||||
#### P0 - 核心故事
|
||||
|
||||
| ID | 用户故事 | 验收标准 |
|
||||
|----|----------|----------|
|
||||
| US-001 | 作为{角色},我想要{功能},以便{价值} | {验收标准} |
|
||||
|
||||
#### P1 - 重要故事
|
||||
|
||||
| ID | 用户故事 | 验收标准 |
|
||||
|----|----------|----------|
|
||||
| US-xxx | 作为{角色},我想要{功能},以便{价值} | {验收标准} |
|
||||
|
||||
#### P2 - 次要故事
|
||||
|
||||
| ID | 用户故事 | 验收标准 |
|
||||
|----|----------|----------|
|
||||
| US-xxx | 作为{角色},我想要{功能},以便{价值} | {验收标准} |
|
||||
|
||||
### 2.3 用户旅程
|
||||
|
||||
**【必须】使用流程图展示核心用户旅程:**
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 触发点 │ ──▶ │ 关键步骤 │ ──▶ │ 目标达成 │
|
||||
│ {描述} │ │ {描述} │ │ {描述} │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
{用户感受} {用户感受} {用户感受}
|
||||
```
|
||||
|
||||
{描述用户完成核心任务的完整流程,从触发点到目标达成}
|
||||
|
||||
## 3. 功能需求
|
||||
|
||||
> 功能需求与用户故事的对应关系
|
||||
|
||||
### 3.1 功能架构
|
||||
|
||||
**【必须】使用树状图或模块图展示功能架构:**
|
||||
|
||||
```
|
||||
{产品名称}
|
||||
├── {模块A}
|
||||
│ ├── {功能A1}
|
||||
│ └── {功能A2}
|
||||
├── {模块B}
|
||||
│ ├── {功能B1}
|
||||
│ └── {功能B2}
|
||||
└── {模块C}
|
||||
└── {功能C1}
|
||||
```
|
||||
|
||||
### 3.2 功能详情
|
||||
|
||||
#### 3.2.1 {模块名称}
|
||||
|
||||
| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 |
|
||||
|--------|------|--------------|--------|----------|
|
||||
| {功能1} | {描述} | US-001 | P0 | {标准} |
|
||||
|
||||
{重复以上结构覆盖所有模块}
|
||||
|
||||
## 4. 非功能需求
|
||||
|
||||
### 4.1 性能需求
|
||||
|
||||
| 指标 | 要求 | 说明 |
|
||||
|------|------|------|
|
||||
| {响应时间} | {要求} | {场景说明} |
|
||||
|
||||
### 4.2 安全需求
|
||||
|
||||
{数据安全、访问控制、合规要求}
|
||||
|
||||
### 4.3 兼容性需求
|
||||
|
||||
| 平台/环境 | 支持版本 |
|
||||
|-----------|----------|
|
||||
| {平台} | {版本} |
|
||||
|
||||
### 4.4 可用性需求
|
||||
|
||||
{SLA、故障恢复、监控告警}
|
||||
|
||||
## 5. 数据需求
|
||||
|
||||
### 5.1 数据模型
|
||||
|
||||
**【建议】使用 ER 图或表格展示核心实体关系:**
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ 实体A │ 1───n │ 实体B │
|
||||
├──────────┤ ├──────────┤
|
||||
│ 字段1 │ │ 字段1 │
|
||||
│ 字段2 │ │ 字段2 │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### 5.2 数据规范
|
||||
|
||||
| 字段 | 类型 | 说明 | 校验规则 |
|
||||
|------|------|------|----------|
|
||||
| {字段名} | {类型} | {说明} | {规则} |
|
||||
|
||||
## 6. 接口需求
|
||||
|
||||
### 6.1 外部接口
|
||||
|
||||
| 接口 | 用途 | 提供方 |
|
||||
|------|------|--------|
|
||||
| {接口名} | {用途} | {第三方} |
|
||||
|
||||
### 6.2 内部接口
|
||||
|
||||
{模块间接口规范}
|
||||
|
||||
## 7. 约束与依赖
|
||||
|
||||
### 7.1 技术约束
|
||||
|
||||
| 约束 | 说明 | 影响 |
|
||||
|------|------|------|
|
||||
| {约束} | {说明} | {影响范围} |
|
||||
|
||||
### 7.2 业务约束
|
||||
|
||||
{法规、政策、合同限制}
|
||||
|
||||
### 7.3 外部依赖
|
||||
|
||||
{第三方服务、团队依赖}
|
||||
|
||||
## 8. 里程碑规划
|
||||
|
||||
**【建议】使用时间线展示里程碑:**
|
||||
|
||||
```
|
||||
Phase 1 Phase 2 Phase 3
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────┐ ┌──────┐ ┌──────┐
|
||||
│ MVP │ ────▶ │ v1.1 │ ────▶ │ v2.0 │
|
||||
└──────┘ └──────┘ └──────┘
|
||||
{日期} {日期} {日期}
|
||||
```
|
||||
|
||||
| 阶段 | 目标 | 交付物 |
|
||||
|------|------|--------|
|
||||
| {阶段1} | {目标} | {交付物} |
|
||||
|
||||
## 9. 风险评估
|
||||
|
||||
| 风险 | 可能性 | 影响 | 应对措施 |
|
||||
|------|--------|------|----------|
|
||||
| {风险1} | 高/中/低 | 高/中/低 | {措施} |
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 术语表
|
||||
|
||||
| 术语 | 定义 |
|
||||
|------|------|
|
||||
| {术语} | {定义} |
|
||||
|
||||
### B. 参考文档
|
||||
|
||||
- RequirementsDoc.md
|
||||
```
|
||||
|
||||
## 4. 保存文档
|
||||
|
||||
将生成的 PRD 保存到 `doc/PRD.md`。
|
||||
|
||||
如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。
|
||||
|
||||
## 5. 输出摘要
|
||||
|
||||
向用户展示生成摘要:
|
||||
|
||||
- PRD 文件路径
|
||||
- 包含的功能模块数量
|
||||
- 主要章节概览
|
||||
- 建议的下一步操作(运行 `/rp` 评审)
|
||||
|
||||
---
|
||||
|
||||
## 可视化输出要求
|
||||
|
||||
PRD 作为「价值主张」文档,需要便于业务沟通理解,**必须包含**:
|
||||
|
||||
| 章节 | 可视化形式 | 必要性 |
|
||||
|------|------------|--------|
|
||||
| 2.3 用户旅程 | 流程图(ASCII) | **必须** |
|
||||
| 3.1 功能架构 | 树状图/模块图 | **必须** |
|
||||
| 5.1 数据模型 | ER 图 | 建议 |
|
||||
| 8. 里程碑规划 | 时间线 | 建议 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- PRD 使用**业务语言**,避免过多技术术语
|
||||
- PRD 内容必须完全来源于 RequirementsDoc,不要臆造需求
|
||||
- 如果 RequirementsDoc 信息不完整,在对应章节标注"待补充"
|
||||
- 保持语言风格与现有文档一致
|
||||
- 优先级标注遵循 P0 > P1 > P2 规则
|
||||
- 验收标准要具体可测试
|
||||
|
||||
## 质量检查
|
||||
|
||||
生成 PRD 后,自查以下项目:
|
||||
|
||||
- [ ] 所有用户故事都有唯一 ID(US-xxx)
|
||||
- [ ] 所有用户故事都符合格式:作为{角色},我想要{功能},以便{价值}
|
||||
- [ ] 所有功能点都关联到用户故事
|
||||
- [ ] 所有功能点都有明确的优先级
|
||||
- [ ] 所有功能点都有验收标准
|
||||
- [ ] **用户旅程有流程图**
|
||||
- [ ] **功能架构有模块图**
|
||||
- [ ] 非功能需求有量化指标
|
||||
- [ ] 无遗漏 RequirementsDoc 中的重要需求
|
||||
- [ ] 文档结构完整,无空章节(或标注"待补充")
|
||||
@ -1,128 +0,0 @@
|
||||
---
|
||||
name: wt
|
||||
description: 从上游文档生成 tasks.md,创建可直接执行的任务列表。
|
||||
---
|
||||
|
||||
# Write Tasks
|
||||
|
||||
当用户调用 `/wt` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取源文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/RequirementsDoc.md` - 必须存在
|
||||
2. `doc/PRD.md` - 必须存在
|
||||
3. `doc/FeatureSummary.md` - 必须存在
|
||||
4. `doc/DevelopmentPlan.md` - 必须存在
|
||||
5. `doc/UIDesign.md` - 必须存在
|
||||
|
||||
如果文件不存在,提示用户:
|
||||
> 缺少上游文档,请确保所有上游文档存在。
|
||||
|
||||
如果已存在 `doc/tasks.md`,同时读取作为参考(保持风格一致)。
|
||||
|
||||
## 2. 分析任务需求
|
||||
|
||||
从上游文档中提取以下信息:
|
||||
|
||||
### 2.1 开发任务
|
||||
|
||||
- 从 DevelopmentPlan 获取开发阶段和任务
|
||||
- 从 UIDesign 获取页面实现任务
|
||||
|
||||
### 2.2 任务依赖
|
||||
|
||||
- 分析任务间的依赖关系
|
||||
- 确定任务执行顺序
|
||||
|
||||
### 2.3 验收标准
|
||||
|
||||
- 从 PRD 获取功能验收标准
|
||||
- 转化为任务级别的完成标准
|
||||
|
||||
## 3. 生成 Tasks
|
||||
|
||||
按以下结构生成文档:
|
||||
|
||||
```markdown
|
||||
# {产品名称} - 任务列表
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 版本 | v1.0 |
|
||||
| 创建日期 | {YYYY-MM-DD} |
|
||||
| 来源文档 | UIDesign.md, DevelopmentPlan.md |
|
||||
|
||||
## 1. 任务总览
|
||||
|
||||
| 统计项 | 数量 |
|
||||
|--------|------|
|
||||
| 总任务数 | X |
|
||||
| P0 任务 | X |
|
||||
| P1 任务 | X |
|
||||
| P2 任务 | X |
|
||||
|
||||
## 2. Phase 1 任务
|
||||
|
||||
### 2.1 {模块/功能名}
|
||||
|
||||
| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 |
|
||||
|----|------|------|--------|------|----------|
|
||||
| T-001 | {任务名} | {描述} | P0 | - | {标准} |
|
||||
| T-002 | {任务名} | {描述} | P0 | T-001 | {标准} |
|
||||
|
||||
{重复以上结构覆盖所有模块}
|
||||
|
||||
## 3. Phase 2 任务
|
||||
|
||||
{同上结构}
|
||||
|
||||
## 4. Phase N 任务
|
||||
|
||||
{同上结构}
|
||||
|
||||
## 5. 任务依赖图
|
||||
|
||||
```
|
||||
T-001 (基础设施)
|
||||
├── T-002 (功能A)
|
||||
│ └── T-005 (功能A优化)
|
||||
└── T-003 (功能B)
|
||||
└── T-004 (功能B扩展)
|
||||
```
|
||||
|
||||
## 6. 执行检查清单
|
||||
|
||||
- [ ] T-001: {任务名}
|
||||
- [ ] T-002: {任务名}
|
||||
{所有任务的检查清单}
|
||||
```
|
||||
|
||||
## 4. 保存文档
|
||||
|
||||
将生成的 tasks 保存到 `doc/tasks.md`。
|
||||
|
||||
如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。
|
||||
|
||||
## 5. 输出摘要
|
||||
|
||||
向用户展示生成摘要:
|
||||
|
||||
- tasks 文件路径
|
||||
- 任务总数
|
||||
- 各阶段任务分布
|
||||
- 建议的下一步操作(运行 `/rt` 评审)
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 任务必须覆盖 DevelopmentPlan 和 UIDesign 所有内容
|
||||
- 任务 ID 必须唯一(T-001, T-002...)
|
||||
- 每个任务必须有明确的验收标准
|
||||
- 任务粒度要适中,可在合理时间内完成
|
||||
- 依赖关系要明确,避免循环依赖
|
||||
- 任务应可直接执行,无歧义
|
||||
@ -1,352 +0,0 @@
|
||||
---
|
||||
name: wu
|
||||
description: 从上游文档生成 UIDesign.md,覆盖所有用户界面设计。
|
||||
---
|
||||
|
||||
# Write UIDesign
|
||||
|
||||
> **文档定位**:UIDesign 是「界面蓝图」文档,用 ASCII 原型图精确传达页面布局、组件结构、交互流程,是前端开发的直接参考。
|
||||
|
||||
当用户调用 `/wu` 时,执行以下步骤:
|
||||
|
||||
## 1. 读取源文档
|
||||
|
||||
读取以下文件:
|
||||
|
||||
1. `doc/RequirementsDoc.md` - 必须存在
|
||||
2. `doc/PRD.md` - 必须存在
|
||||
3. `doc/FeatureSummary.md` - 必须存在
|
||||
4. `doc/DevelopmentPlan.md` - 必须存在
|
||||
|
||||
如果文件不存在,提示用户:
|
||||
> 缺少上游文档,请确保所有上游文档存在。
|
||||
|
||||
如果已存在 `doc/UIDesign.md`,同时读取作为参考(保持风格一致)。
|
||||
|
||||
## 2. 分析 UI 需求
|
||||
|
||||
从上游文档中提取以下信息:
|
||||
|
||||
### 2.1 页面需求
|
||||
|
||||
- 从 PRD 用户旅程分析所需页面
|
||||
- 从 FeatureSummary 获取功能对应的界面
|
||||
- 从 DevelopmentPlan 获取技术实现约束
|
||||
|
||||
### 2.2 用户流程
|
||||
|
||||
- 主要用户旅程
|
||||
- 页面跳转关系
|
||||
- 交互流程
|
||||
|
||||
## 3. 生成 UIDesign
|
||||
|
||||
按以下结构生成文档:
|
||||
|
||||
```markdown
|
||||
# {产品名称} - UI 设计文档
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 版本 | v1.0 |
|
||||
| 创建日期 | {YYYY-MM-DD} |
|
||||
| 来源文档 | DevelopmentPlan.md |
|
||||
|
||||
## 1. 设计概述
|
||||
|
||||
### 1.1 设计原则
|
||||
|
||||
{UI 设计原则和规范}
|
||||
|
||||
### 1.2 页面总览
|
||||
|
||||
| 页面ID | 页面名称 | 描述 | 对应功能 | 优先级 |
|
||||
|--------|----------|------|----------|--------|
|
||||
| P-001 | {页面名} | {描述} | F-001 | P0 |
|
||||
|
||||
### 1.3 页面导航图
|
||||
|
||||
**【必须】使用导航图展示页面跳转关系:**
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 首页 │
|
||||
│ P-001 │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 功能A页 │ │ 功能B页 │ │ 设置页 │
|
||||
│ P-002 │ │ P-003 │ │ P-004 │
|
||||
└──────┬──────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ 详情页 │
|
||||
│ P-005 │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## 2. 页面设计
|
||||
|
||||
### 2.1 P-001: {页面名称}
|
||||
|
||||
**页面信息**
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 页面ID | P-001 |
|
||||
| 对应功能 | F-001, F-002 |
|
||||
| 入口 | {从哪些页面可进入} |
|
||||
| 出口 | {可跳转到哪些页面} |
|
||||
|
||||
**【必须】页面布局 - ASCII 原型图**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Header │ │
|
||||
│ │ [Logo] [Nav Item] [Nav Item] [用户]│ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
├────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌───────────────────────────┐ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Sidebar │ │ Main Content │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Menu Item 1 │ │ ┌─────────────────────┐ │ │
|
||||
│ │ • Menu Item 2 │ │ │ Card 1 │ │ │
|
||||
│ │ • Menu Item 3 │ │ │ [Title] │ │ │
|
||||
│ │ │ │ │ [Description...] │ │ │
|
||||
│ │ │ │ │ [Action Button] │ │ │
|
||||
│ │ │ │ └─────────────────────┘ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ ┌─────────────────────┐ │ │
|
||||
│ │ │ │ │ Card 2 │ │ │
|
||||
│ │ │ │ └─────────────────────┘ │ │
|
||||
│ │ │ │ │ │
|
||||
│ └──────────────────┘ └───────────────────────────┘ │
|
||||
│ │
|
||||
├────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Footer │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**组件清单**
|
||||
|
||||
| 组件ID | 组件名称 | 类型 | 说明 | 交互 |
|
||||
|--------|----------|------|------|------|
|
||||
| C-001 | Header | 导航栏 | 顶部固定 | 点击 Logo 回首页 |
|
||||
| C-002 | Sidebar | 侧边栏 | 左侧固定 | 点击菜单切换内容 |
|
||||
| C-003 | Card | 卡片 | 内容展示 | 点击进入详情 |
|
||||
|
||||
**交互说明**
|
||||
|
||||
| 触发 | 动作 | 结果 |
|
||||
|------|------|------|
|
||||
| 点击 Card | 跳转 | 进入详情页 P-005 |
|
||||
| 点击 Menu Item | 切换 | 更新 Main Content |
|
||||
|
||||
**页面状态**
|
||||
|
||||
| 状态 | 说明 | 展示 |
|
||||
|------|------|------|
|
||||
| 默认 | 正常加载完成 | 显示数据列表 |
|
||||
| 加载中 | 数据请求中 | 骨架屏/Loading |
|
||||
| 空状态 | 无数据 | 空状态插图+引导文案 |
|
||||
| 错误 | 请求失败 | 错误提示+重试按钮 |
|
||||
|
||||
**空状态原型**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ (空图标) │ │
|
||||
│ └─────────────┘ │
|
||||
│ │
|
||||
│ 暂无数据 │
|
||||
│ │
|
||||
│ [去添加数据] │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
{重复以上结构覆盖所有页面}
|
||||
|
||||
## 3. 用户流程
|
||||
|
||||
### 3.1 {流程名称}
|
||||
|
||||
**【必须】使用流程图展示用户操作流程:**
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Step 1 │ ──▶ │ Step 2 │ ──▶ │ Step 3 │ ──▶ │ 完成 │
|
||||
│ {操作} │ │ {操作} │ │ {操作} │ │ │
|
||||
│ P-001 │ │ P-002 │ │ P-003 │ │ P-004 │
|
||||
└─────────┘ └────┬────┘ └─────────┘ └─────────┘
|
||||
│
|
||||
▼ 取消
|
||||
┌─────────┐
|
||||
│ 返回 │
|
||||
│ P-001 │
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
**流程步骤**
|
||||
|
||||
| 步骤 | 页面 | 用户操作 | 系统响应 |
|
||||
|------|------|----------|----------|
|
||||
| 1 | P-001 | {操作} | {响应} |
|
||||
| 2 | P-002 | {操作} | {响应} |
|
||||
|
||||
## 4. 组件规范
|
||||
|
||||
### 4.1 基础组件
|
||||
|
||||
**Button 按钮**
|
||||
|
||||
```
|
||||
主按钮: ┌──────────────┐
|
||||
│ 确认提交 │ (填充色背景)
|
||||
└──────────────┘
|
||||
|
||||
次按钮: ┌──────────────┐
|
||||
│ 取消 │ (边框样式)
|
||||
└──────────────┘
|
||||
|
||||
禁用态: ┌──────────────┐
|
||||
│ 不可点击 │ (灰色)
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
**Input 输入框**
|
||||
|
||||
```
|
||||
默认态: ┌────────────────────────┐
|
||||
│ 请输入... │
|
||||
└────────────────────────┘
|
||||
|
||||
聚焦态: ┌────────────────────────┐
|
||||
│ 输入内容 │ (高亮边框)
|
||||
└────────────────────────┘
|
||||
|
||||
错误态: ┌────────────────────────┐
|
||||
│ 错误输入 │ (红色边框)
|
||||
└────────────────────────┘
|
||||
⚠ 错误提示信息
|
||||
```
|
||||
|
||||
### 4.2 业务组件
|
||||
|
||||
{项目特有的业务组件}
|
||||
|
||||
## 5. 设计规范
|
||||
|
||||
### 5.1 色彩规范
|
||||
|
||||
| 用途 | 色值 | 示例 |
|
||||
|------|------|------|
|
||||
| 主色 | #1890FF | 主按钮、链接 |
|
||||
| 成功 | #52C41A | 成功提示 |
|
||||
| 警告 | #FAAD14 | 警告提示 |
|
||||
| 错误 | #FF4D4F | 错误提示 |
|
||||
| 文字主色 | #262626 | 标题 |
|
||||
| 文字次色 | #8C8C8C | 描述 |
|
||||
|
||||
### 5.2 字体规范
|
||||
|
||||
| 用途 | 字号 | 字重 |
|
||||
|------|------|------|
|
||||
| 大标题 | 24px | Bold |
|
||||
| 标题 | 18px | Medium |
|
||||
| 正文 | 14px | Regular |
|
||||
| 辅助文字 | 12px | Regular |
|
||||
|
||||
### 5.3 间距规范
|
||||
|
||||
| 间距 | 值 | 用途 |
|
||||
|------|-----|------|
|
||||
| xs | 4px | 紧凑间距 |
|
||||
| sm | 8px | 小间距 |
|
||||
| md | 16px | 标准间距 |
|
||||
| lg | 24px | 大间距 |
|
||||
| xl | 32px | 特大间距 |
|
||||
|
||||
### 5.4 响应式断点
|
||||
|
||||
| 断点 | 宽度 | 布局说明 |
|
||||
|------|------|----------|
|
||||
| Mobile | < 768px | 单栏布局 |
|
||||
| Tablet | 768px - 1024px | 双栏布局 |
|
||||
| Desktop | > 1024px | 多栏布局 |
|
||||
```
|
||||
|
||||
## 4. 保存文档
|
||||
|
||||
将生成的 UIDesign 保存到 `doc/UIDesign.md`。
|
||||
|
||||
如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。
|
||||
|
||||
## 5. 输出摘要
|
||||
|
||||
向用户展示生成摘要:
|
||||
|
||||
- UIDesign 文件路径
|
||||
- 页面数量
|
||||
- 用户流程数量
|
||||
- 建议的下一步操作(运行 `/ru` 评审)
|
||||
|
||||
---
|
||||
|
||||
## 可视化输出要求
|
||||
|
||||
UIDesign 作为「界面蓝图」文档,**必须大量使用 ASCII 原型图**:
|
||||
|
||||
| 章节 | 可视化形式 | 必要性 |
|
||||
|------|------------|--------|
|
||||
| 1.3 页面导航图 | 导航关系图(ASCII) | **必须** |
|
||||
| 2.x 页面布局 | **ASCII 原型图** | **必须(每页)** |
|
||||
| 2.x 空状态 | ASCII 原型图 | 建议 |
|
||||
| 3.x 用户流程 | 流程图(ASCII) | **必须** |
|
||||
| 4.x 组件规范 | 组件示意图 | **必须** |
|
||||
|
||||
**ASCII 原型图要求**:
|
||||
|
||||
- 每个页面**必须**有完整的布局原型图
|
||||
- 原型图要体现:页面结构、组件位置、内容区域
|
||||
- 使用 `┌ ┐ └ ┘ ─ │ ├ ┤ ┬ ┴ ┼` 等字符绘制边框
|
||||
- 使用 `[ ]` 表示按钮
|
||||
- 使用 `▼ ▶ ◀ ▲` 表示方向/展开
|
||||
- 关键交互点要标注
|
||||
|
||||
## 注意事项
|
||||
|
||||
- UI 设计必须覆盖 DevelopmentPlan 所有功能模块
|
||||
- **每个页面必须有 ASCII 原型图**
|
||||
- 页面设计要考虑各种状态(默认、加载、空、错误)
|
||||
- 交互说明要清晰具体
|
||||
- 设计规范要统一一致
|
||||
- 页面 ID 格式:P-xxx
|
||||
- 组件 ID 格式:C-xxx
|
||||
|
||||
## 质量检查
|
||||
|
||||
生成 UIDesign 后,自查以下项目:
|
||||
|
||||
- [ ] 覆盖 DevelopmentPlan 所有功能模块
|
||||
- [ ] **页面导航图清晰展示页面关系**
|
||||
- [ ] **每个页面都有 ASCII 原型图**
|
||||
- [ ] **原型图展示了完整的页面结构**
|
||||
- [ ] **用户流程有流程图**
|
||||
- [ ] 每个页面都有状态说明
|
||||
- [ ] 组件清单完整
|
||||
- [ ] 交互说明清晰
|
||||
- [ ] 设计规范统一
|
||||
25
.gitignore
vendored
25
.gitignore
vendored
@ -1,34 +1,13 @@
|
||||
# 文档目录(各项目自己生成)
|
||||
/doc/
|
||||
doc/
|
||||
|
||||
# 其他开发文件
|
||||
write-skills/
|
||||
.codex/*
|
||||
!.codex/skills/
|
||||
!.codex/skills/**
|
||||
.agents/*
|
||||
!.agents/skills/
|
||||
.agents/skills/*
|
||||
!.agents/skills/issue/
|
||||
!.agents/skills/issue/SKILL.md
|
||||
!.agents/skills/issue-drive/
|
||||
!.agents/skills/issue-drive/SKILL.md
|
||||
!.agents/skills/issue-drive/scripts/
|
||||
!.agents/skills/issue-drive/scripts/create_gitea_issues.py
|
||||
!.agents/skills/gitea/
|
||||
!.agents/skills/gitea/SKILL.md
|
||||
!.agents/skills/gitea/scripts/
|
||||
!.agents/skills/gitea/scripts/common.py
|
||||
!.agents/skills/gitea/scripts/push_gitea.py
|
||||
!.agents/skills/gitea/scripts/pr_gitea.py
|
||||
.codex/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
58
AGENTS.md
58
AGENTS.md
@ -1,58 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
把这个文件放在项目根目录,用来把已安装的 Codex skills 注册给 Codex。项目级约束可以追加在文末的 `Repo notes` 一节。
|
||||
|
||||
## Coding 宪法
|
||||
|
||||
### 最优路径优先
|
||||
|
||||
- 先基于当前目标、约束和上下文判断最优路径,再沿着这条路径推进。
|
||||
- 默认先收敛到一个最优方案,不先为了自我防御同时铺退路、保守版、兼容版或降级版。
|
||||
- 只有在最优路径被事实阻塞、风险或成本发生实质变化、或用户明确要求方案比较时,才展开 fallback 与备选方案。
|
||||
- 提问和澄清只用于解决会改变最优路径判断的问题,不能把提问当成免责动作。
|
||||
|
||||
## Skills
|
||||
|
||||
### Available skills
|
||||
|
||||
- `rr`: 评审 RequirementsDoc.md,检查需求文档的完整性、清晰度和可执行性,输出结构化评审报告。 (file: `./.codex/skills/rr/SKILL.md`)
|
||||
- `rp`: 评审 PRD.md,对比 RequirementsDoc 检查一致性,输出结构化评审报告。 (file: `./.codex/skills/rp/SKILL.md`)
|
||||
- `rf`: 评审 FeatureSummary.md,对比 PRD 检查一致性,输出结构化评审报告。 (file: `./.codex/skills/rf/SKILL.md`)
|
||||
- `rd`: 评审 DevelopmentPlan.md,检查技术可行性和与上游文档一致性,输出结构化评审报告。 (file: `./.codex/skills/rd/SKILL.md`)
|
||||
- `ru`: 评审 UIDesign.md,对比 DevelopmentPlan 检查设计一致性,输出结构化评审报告。 (file: `./.codex/skills/ru/SKILL.md`)
|
||||
- `rt`: 评审 tasks.md,检查任务完整性和与上游文档一致性,输出结构化评审报告。 (file: `./.codex/skills/rt/SKILL.md`)
|
||||
- `wp`: 从 RequirementsDoc.md 生成 PRD.md,将需求文档转化为结构化的产品需求文档。 (file: `./.codex/skills/wp/SKILL.md`)
|
||||
- `wf`: 从 RequirementsDoc.md 和 PRD.md 生成 FeatureSummary.md,提供功能全貌概览。 (file: `./.codex/skills/wf/SKILL.md`)
|
||||
- `wd`: 从上游文档生成 DevelopmentPlan.md,包含技术方案和开发排期。 (file: `./.codex/skills/wd/SKILL.md`)
|
||||
- `wu`: 从上游文档生成 UIDesign.md,覆盖所有用户界面设计。 (file: `./.codex/skills/wu/SKILL.md`)
|
||||
- `wt`: 从上游文档生成 tasks.md,创建可直接执行的任务列表。 (file: `./.codex/skills/wt/SKILL.md`)
|
||||
- `mr`: 增量修改 RequirementsDoc.md,根据用户指令在现有内容基础上更新需求文档。 (file: `./.codex/skills/mr/SKILL.md`)
|
||||
- `mp`: 增量修改 PRD.md,根据用户指令在现有内容基础上更新产品需求文档。 (file: `./.codex/skills/mp/SKILL.md`)
|
||||
- `mf`: 增量修改 FeatureSummary.md,根据用户指令在现有内容基础上更新功能摘要。 (file: `./.codex/skills/mf/SKILL.md`)
|
||||
- `md`: 增量修改 DevelopmentPlan.md,根据用户指令在现有内容基础上更新开发计划。 (file: `./.codex/skills/md/SKILL.md`)
|
||||
- `mu`: 增量修改 UIDesign.md,根据用户指令在现有内容基础上更新 UI 设计文档。 (file: `./.codex/skills/mu/SKILL.md`)
|
||||
- `mt`: 增量修改 tasks.md,根据用户指令在现有内容基础上更新任务列表。 (file: `./.codex/skills/mt/SKILL.md`)
|
||||
- `go`: 终极执行按钮,激进模式一口气完成开发任务,兼容 0->1 和 1->100 场景。 (file: `./.codex/skills/go/SKILL.md`)
|
||||
- `iter`: 迭代变更入口,调研问题后更新 PRD.md 和 tasks.md,支持 Bug 修复、功能迭代、技术重构。 (file: `./.codex/skills/iter/SKILL.md`)
|
||||
- `doc`: 渐进式文档生成器。首次只写精炼梗概(≤300字),后续通过迭代不断完善。 (file: `./.codex/skills/doc/SKILL.md`)
|
||||
- `capture`: 复刻一次成功任务的经验,输出到用户指定目录。调用方式 `/capture <目录>` 或 `$capture <目录>`,生成只含 fenced YAML 的 Markdown 记录。 (file: `./.codex/skills/capture/SKILL.md`)
|
||||
- `update`: 收集用户反馈并更新最近使用的 skill。别名:`up`。 (file: `./.codex/skills/up/SKILL.md`)
|
||||
- `deploy`: Drone CI + 服务器 CD 全流程引导:从基础设施检查到生成配置文件到验证部署,交互式完成。 (file: `./.codex/skills/deploy/SKILL.md`)
|
||||
- `gitea`: 统一 Gitea 总入口,支持 issue 查询、issue 拆单创建、git push 和 PR 基础操作,优先从当前仓库 origin 自动识别目标仓库。 (file: `./.codex/skills/gitea/SKILL.md`)
|
||||
- `issue`: 查看当前仓库或任意 Gitea 仓库的 issue 列表和单条详情,支持自动识别 git origin、用户指定仓库和格式化输出。 (file: `./.codex/skills/issue/SKILL.md`)
|
||||
- `issue-drive`: 归集证据并把问题拆成 1 到多张 Gitea issue,支持从当前仓库 origin 自动识别仓库或用户显式指定。 (file: `./.codex/skills/issue-drive/SKILL.md`)
|
||||
- `changelog`: 一键发版:生成更新日志 → commit → 打 tag,全流程自动化。 (file: `./.codex/skills/changelog/SKILL.md`)
|
||||
|
||||
### How to use skills
|
||||
|
||||
- Discovery: 以上列表就是当前仓库提供给 Codex 的 skills。
|
||||
- Trigger rules: 如果用户显式提到 skill 名称(例如 `/rr`、`$rr`、`rr skill`、`用 rr 评审`),或任务明显匹配 skill 描述,优先使用对应 skill。
|
||||
- Codex usage: 在 Codex 中优先使用 `/skill-name`;兼容历史 `$skill-name` 写法,也支持自然语言触发。
|
||||
- Missing/blocked: 如果某个 skill 文件不存在或无法读取,简短说明并回退到普通实现方式。
|
||||
- Context hygiene: 只按需打开 `SKILL.md`,不要一次性加载整个 skill 仓库。
|
||||
|
||||
### Repo notes
|
||||
|
||||
- `./.claude/skills/` 保留给 Claude Code。
|
||||
- `./.codex/skills/` 是 Codex 的实际安装源。
|
||||
- 迁移或新增 skill 时,优先同步更新 `README.md`、`AGENTS.md`、`AGENTS.md.template`。
|
||||
@ -1,56 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
把这个文件放在项目根目录,用来把已安装的 Codex skills 注册给 Codex。项目级约束可以追加在文末的 `Project notes` 一节。
|
||||
|
||||
## Coding 宪法
|
||||
|
||||
### 最优路径优先
|
||||
|
||||
- 先基于当前目标、约束和上下文判断最优路径,再沿着这条路径推进。
|
||||
- 默认先收敛到一个最优方案,不先为了自我防御同时铺退路、保守版、兼容版或降级版。
|
||||
- 只有在最优路径被事实阻塞、风险或成本发生实质变化、或用户明确要求方案比较时,才展开 fallback 与备选方案。
|
||||
- 提问和澄清只用于解决会改变最优路径判断的问题,不能把提问当成免责动作。
|
||||
|
||||
## Skills
|
||||
|
||||
### Available skills
|
||||
|
||||
- `rr`: 评审 RequirementsDoc.md,检查需求文档的完整性、清晰度和可执行性,输出结构化评审报告。 (file: `./.codex/skills/rr/SKILL.md`)
|
||||
- `rp`: 评审 PRD.md,对比 RequirementsDoc 检查一致性,输出结构化评审报告。 (file: `./.codex/skills/rp/SKILL.md`)
|
||||
- `rf`: 评审 FeatureSummary.md,对比 PRD 检查一致性,输出结构化评审报告。 (file: `./.codex/skills/rf/SKILL.md`)
|
||||
- `rd`: 评审 DevelopmentPlan.md,检查技术可行性和与上游文档一致性,输出结构化评审报告。 (file: `./.codex/skills/rd/SKILL.md`)
|
||||
- `ru`: 评审 UIDesign.md,对比 DevelopmentPlan 检查设计一致性,输出结构化评审报告。 (file: `./.codex/skills/ru/SKILL.md`)
|
||||
- `rt`: 评审 tasks.md,检查任务完整性和与上游文档一致性,输出结构化评审报告。 (file: `./.codex/skills/rt/SKILL.md`)
|
||||
- `wp`: 从 RequirementsDoc.md 生成 PRD.md,将需求文档转化为结构化的产品需求文档。 (file: `./.codex/skills/wp/SKILL.md`)
|
||||
- `wf`: 从 RequirementsDoc.md 和 PRD.md 生成 FeatureSummary.md,提供功能全貌概览。 (file: `./.codex/skills/wf/SKILL.md`)
|
||||
- `wd`: 从上游文档生成 DevelopmentPlan.md,包含技术方案和开发排期。 (file: `./.codex/skills/wd/SKILL.md`)
|
||||
- `wu`: 从上游文档生成 UIDesign.md,覆盖所有用户界面设计。 (file: `./.codex/skills/wu/SKILL.md`)
|
||||
- `wt`: 从上游文档生成 tasks.md,创建可直接执行的任务列表。 (file: `./.codex/skills/wt/SKILL.md`)
|
||||
- `mr`: 增量修改 RequirementsDoc.md,根据用户指令在现有内容基础上更新需求文档。 (file: `./.codex/skills/mr/SKILL.md`)
|
||||
- `mp`: 增量修改 PRD.md,根据用户指令在现有内容基础上更新产品需求文档。 (file: `./.codex/skills/mp/SKILL.md`)
|
||||
- `mf`: 增量修改 FeatureSummary.md,根据用户指令在现有内容基础上更新功能摘要。 (file: `./.codex/skills/mf/SKILL.md`)
|
||||
- `md`: 增量修改 DevelopmentPlan.md,根据用户指令在现有内容基础上更新开发计划。 (file: `./.codex/skills/md/SKILL.md`)
|
||||
- `mu`: 增量修改 UIDesign.md,根据用户指令在现有内容基础上更新 UI 设计文档。 (file: `./.codex/skills/mu/SKILL.md`)
|
||||
- `mt`: 增量修改 tasks.md,根据用户指令在现有内容基础上更新任务列表。 (file: `./.codex/skills/mt/SKILL.md`)
|
||||
- `go`: 终极执行按钮,激进模式一口气完成开发任务,兼容 0->1 和 1->100 场景。 (file: `./.codex/skills/go/SKILL.md`)
|
||||
- `iter`: 迭代变更入口,调研问题后更新 PRD.md 和 tasks.md,支持 Bug 修复、功能迭代、技术重构。 (file: `./.codex/skills/iter/SKILL.md`)
|
||||
- `doc`: 渐进式文档生成器。首次只写精炼梗概(≤300字),后续通过迭代不断完善。 (file: `./.codex/skills/doc/SKILL.md`)
|
||||
- `capture`: 复刻一次成功任务的经验,输出到用户指定目录。调用方式 `/capture <目录>` 或 `$capture <目录>`,生成只含 fenced YAML 的 Markdown 记录。 (file: `./.codex/skills/capture/SKILL.md`)
|
||||
- `update`: 收集用户反馈并更新最近使用的 skill。别名:`up`。 (file: `./.codex/skills/up/SKILL.md`)
|
||||
- `deploy`: Drone CI + 服务器 CD 全流程引导:从基础设施检查到生成配置文件到验证部署,交互式完成。 (file: `./.codex/skills/deploy/SKILL.md`)
|
||||
- `gitea`: 统一 Gitea 总入口,支持 issue 查询、issue 拆单创建、git push 和 PR 基础操作,优先从当前仓库 origin 自动识别目标仓库。 (file: `./.codex/skills/gitea/SKILL.md`)
|
||||
- `issue`: 查看当前仓库或任意 Gitea 仓库的 issue 列表和单条详情,支持自动识别 git origin、用户指定仓库和格式化输出。 (file: `./.codex/skills/issue/SKILL.md`)
|
||||
- `issue-drive`: 归集证据并把问题拆成 1 到多张 Gitea issue,支持从当前仓库 origin 自动识别仓库或用户显式指定。 (file: `./.codex/skills/issue-drive/SKILL.md`)
|
||||
- `changelog`: 一键发版:生成更新日志 → commit → 打 tag,全流程自动化。 (file: `./.codex/skills/changelog/SKILL.md`)
|
||||
|
||||
### How to use skills
|
||||
|
||||
- Discovery: 以上列表就是当前项目注册给 Codex 的 skills。
|
||||
- Trigger rules: 如果用户显式提到 skill 名称(例如 `/rr`、`$rr`、`rr skill`、`用 rr 评审`),或任务明显匹配 skill 描述,优先使用对应 skill。
|
||||
- Codex usage: 在 Codex 中优先使用 `/skill-name`;兼容历史 `$skill-name` 写法,也支持自然语言触发。
|
||||
- Missing/blocked: 如果某个 skill 文件不存在或无法读取,简短说明并回退到普通实现方式。
|
||||
- Context hygiene: 只按需打开 `SKILL.md`,不要一次性加载整个 skill 仓库。
|
||||
|
||||
## Project notes
|
||||
|
||||
- 在这里补充项目特定约束,例如技术栈、测试命令、代码风格、提交流程。
|
||||
@ -5,14 +5,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 最重要的事情
|
||||
|
||||
1. **最优路径优先** - 先判断当前约束下的最优路径,锁定后直接推进;不先为了自我防御铺退路、保守版或兼容版
|
||||
2. **TDD 先行** - fix/feat 必须先写失败测试,红黄绿循环
|
||||
3. **原子提交** - 每个 commit 只做一件事,可独立回滚
|
||||
4. **文档驱动** - feat 改动关联 doc/ 下文档,多输出表格、流程图、ASCII 原型图
|
||||
5. **知识沉淀** - 有价值的迭代沉淀到 CLAUDE.md(拿捏不准主动问我)
|
||||
6. **利用现有工具** - 不重复造轮子,会开车 > 会修车
|
||||
7. **有头有尾** - 头:只问会改变最优路径判断的问题,锁定后立刻动手;尾:自己跑验证,不把验证甩给用户
|
||||
8. **任务结束后追加** - 主人,用不用我沉淀 or git 提交?
|
||||
1. **TDD 先行** - fix/feat 必须先写失败测试,红黄绿循环
|
||||
2. **原子提交** - 每个 commit 只做一件事,可独立回滚
|
||||
3. **文档驱动** - feat 改动关联 doc/ 下文档,多输出表格、流程图、ASCII 原型图
|
||||
4. **知识沉淀** - 有价值的迭代沉淀到 CLAUDE.md(拿捏不准主动问我)
|
||||
5. **利用现有工具** - 不重复造轮子,会开车 > 会修车
|
||||
6. **任务结束后追加** - 主人,用不用我沉淀 or git 提交?
|
||||
|
||||
## 项目概述
|
||||
|
||||
@ -84,28 +82,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- 新功能实现方案 → 更新组件职责、数据流等章节
|
||||
- 踩坑经验和解决方案 → 添加到踩坑经验章节
|
||||
- API 使用技巧和注意事项 → 更新相关技术栈说明
|
||||
- **成功经验复刻**: 一次对话形成可复用结果后,用 `/capture <目录>` 或 `$capture <目录>` 输出结构化记录;另留单独位置保存“味道”
|
||||
|
||||
{{在此添加项目特定的开发约定}}
|
||||
|
||||
## 交互准则
|
||||
|
||||
### 任务有头有尾
|
||||
|
||||
**头 — 先锁定最优路径再动手**:
|
||||
- 收到任务后,先复述理解、列出不确定的点
|
||||
- 只追问会改变最优路径判断的问题;能基于上下文和环境判断的,不拿来做前置防御
|
||||
- 一旦最优路径锁定,立即沿该路径推进,不先铺退路、折中版或兼容版
|
||||
- 确认范围边界:做什么、不做什么、验收标准
|
||||
|
||||
**尾 — 自己验证,说到做到**:
|
||||
- 任务完成后,自己执行验证(跑测试、构建、截图、检查输出等)
|
||||
- 把验证结果直接展示给用户,而不是列一堆步骤让用户自己验
|
||||
- 验证不通过就自己修,循环直到通过
|
||||
- 最终交付物 = 已通过的验证结果
|
||||
|
||||
### 其他
|
||||
|
||||
- 任务彻底结束后,追加一句:**主人,用不用我沉淀 or git 提交?**
|
||||
|
||||
## 踩坑经验
|
||||
|
||||
320
README.md
320
README.md
@ -1,290 +1,126 @@
|
||||
# Spec Coding Skills
|
||||
|
||||
一套同时支持 Claude Code 和 Codex 的 Skills,覆盖产品文档工作流的完整生命周期管理。
|
||||
|
||||
## 平台适配
|
||||
|
||||
| 平台 | 安装目录 | 触发方式 |
|
||||
|------|----------|----------|
|
||||
| Claude Code | `.claude/skills/` | `/rr`、`/wp`、`/go` 这类 slash commands |
|
||||
| Codex | `.codex/skills/` | `/rr`、`/wp` 这类 slash commands,或直接自然语言说明“用 rr skill 评审需求文档” |
|
||||
|
||||
> Codex 额外需要项目根目录存在 `AGENTS.md`。本仓库已提供 `AGENTS.md.template`,安装脚本在 Codex 模式下会自动生成。
|
||||
一套 Claude Code Skills,支持产品文档工作流的完整生命周期管理。
|
||||
|
||||
## 功能概览
|
||||
|
||||
```
|
||||
RequirementsDoc ──▶ PRD ──▶ FeatureSummary ──▶ DevelopmentPlan ──▶ UIDesign ──▶ tasks
|
||||
│ │ │ │ │ │
|
||||
rr rp rf rd ru rt ← Review
|
||||
mr mp mf md mu mt ← Modify
|
||||
wp wf wd wu wt ← Write
|
||||
/rr /rp /rf /rd /ru /rt ← Review
|
||||
/mr /mp /mf /md /mu /mt ← Modify
|
||||
/wp /wf /wd /wu /wt ← Write
|
||||
```
|
||||
|
||||
### Skills 列表
|
||||
|
||||
| 类型 | Skill | Claude Code | Codex | 描述 |
|
||||
|------|-------|-------------|-------|------|
|
||||
| **Review** | `rr` | `/rr` | `/rr` | 评审 RequirementsDoc.md |
|
||||
| | `rp` | `/rp` | `/rp` | 评审 PRD.md |
|
||||
| | `rf` | `/rf` | `/rf` | 评审 FeatureSummary.md |
|
||||
| | `rd` | `/rd` | `/rd` | 评审 DevelopmentPlan.md |
|
||||
| | `ru` | `/ru` | `/ru` | 评审 UIDesign.md |
|
||||
| | `rt` | `/rt` | `/rt` | 评审 tasks.md |
|
||||
| **Write** | `wp` | `/wp` | `/wp` | 从 RequirementsDoc 生成 PRD |
|
||||
| | `wf` | `/wf` | `/wf` | 从 PRD 生成 FeatureSummary |
|
||||
| | `wd` | `/wd` | `/wd` | 从 FeatureSummary 生成 DevelopmentPlan |
|
||||
| | `wu` | `/wu` | `/wu` | 从 DevelopmentPlan 生成 UIDesign |
|
||||
| | `wt` | `/wt` | `/wt` | 从 DevelopmentPlan 生成 tasks |
|
||||
| **Modify** | `mr` | `/mr` | `/mr` | 增量修改 RequirementsDoc |
|
||||
| | `mp` | `/mp` | `/mp` | 增量修改 PRD(自动读取评审报告) |
|
||||
| | `mf` | `/mf` | `/mf` | 增量修改 FeatureSummary |
|
||||
| | `md` | `/md` | `/md` | 增量修改 DevelopmentPlan |
|
||||
| | `mu` | `/mu` | `/mu` | 增量修改 UIDesign |
|
||||
| | `mt` | `/mt` | `/mt` | 增量修改 tasks |
|
||||
| **执行** | `go` | `/go` | `/go` | 🚀 发射按钮,激进模式一口气完成开发 |
|
||||
| **辅助** | `iter` | `/iter` | `/iter` | 迭代变更入口(Bug/功能/重构) |
|
||||
| | `doc` | `/doc` | `/doc` | 渐进式文档生成器,先写梗概再迭代完善 |
|
||||
| | `capture` | `/capture` | `/capture` | 把一次成功任务复刻成结构化经验记录,输出到指定目录 |
|
||||
| | `update` | `/up` | `/up` | Skill 升级优化 |
|
||||
| | `deploy` | `/deploy` | `/deploy` | Drone CI/CD 全流程部署引导 |
|
||||
| | `gitea` | `/gitea` | `/gitea` | 统一 Gitea 入口(issue / push / PR) |
|
||||
| | `issue` | `/issue` | `/issue` | Gitea issue 只读专用入口(支持当前仓库自动识别) |
|
||||
| | `issue-drive` | `/issue-drive` | `/issue-drive` | Gitea issue 拆单与批量创建专用入口 |
|
||||
| | `changelog` | `/changelog` | `/changelog` | 一键发版(日志 + commit + tag) |
|
||||
| 类型 | 命令 | 描述 |
|
||||
|------|------|------|
|
||||
| **Review** | `/rr` | 评审 RequirementsDoc.md |
|
||||
| | `/rp` | 评审 PRD.md |
|
||||
| | `/rf` | 评审 FeatureSummary.md |
|
||||
| | `/rd` | 评审 DevelopmentPlan.md |
|
||||
| | `/ru` | 评审 UIDesign.md |
|
||||
| | `/rt` | 评审 tasks.md |
|
||||
| **Write** | `/wp` | 从 RequirementsDoc 生成 PRD |
|
||||
| | `/wf` | 从 PRD 生成 FeatureSummary |
|
||||
| | `/wd` | 从 FeatureSummary 生成 DevelopmentPlan |
|
||||
| | `/wu` | 从 DevelopmentPlan 生成 UIDesign |
|
||||
| | `/wt` | 从 DevelopmentPlan 生成 tasks |
|
||||
| **Modify** | `/mr` | 增量修改 RequirementsDoc |
|
||||
| | `/mp` | 增量修改 PRD(自动读取评审报告) |
|
||||
| | `/mf` | 增量修改 FeatureSummary |
|
||||
| | `/md` | 增量修改 DevelopmentPlan |
|
||||
| | `/mu` | 增量修改 UIDesign |
|
||||
| | `/mt` | 增量修改 tasks |
|
||||
| **执行** | `/go` | 🚀 发射按钮,激进模式一口气完成开发 |
|
||||
| **辅助** | `/iter` | 迭代变更入口(Bug/功能/重构) |
|
||||
| | `/up` | 文档升级迁移 |
|
||||
|
||||
> Codex 兼容历史 `$skill` 写法,但本文档统一以 `/skill` 作为主入口。
|
||||
|
||||
## 安装 & 更新
|
||||
|
||||
一行命令搞定安装和更新。脚本会智能处理:新 skill 直接装,已有的对比更新,本地魔改自动备份;项目根目录的 `AGENTS.md` / `CLAUDE.md` 仅在缺失时生成,已存在则跳过。
|
||||
## 安装方式
|
||||
### 方式 1:直接复制
|
||||
|
||||
```bash
|
||||
# Claude Code + Codex(推荐)
|
||||
bash <(curl -sL https://git.internal.intelligrow.cn/zhangfucai/spec-coding-skills/raw/branch/main/install.sh) both
|
||||
# 克隆仓库
|
||||
git clone https://git.internal.intelligrow.cn/zhangfucai/spec-coding-skills.git /tmp/spec-coding-skills
|
||||
|
||||
# Codex only
|
||||
bash <(curl -sL https://git.internal.intelligrow.cn/zhangfucai/spec-coding-skills/raw/branch/main/install.sh) codex
|
||||
# 复制 skills 到你的项目
|
||||
mkdir -p .claude
|
||||
cp -r /tmp/spec-coding-skills/.claude/skills .claude/
|
||||
|
||||
# Claude Code only
|
||||
bash <(curl -sL https://git.internal.intelligrow.cn/zhangfucai/spec-coding-skills/raw/branch/main/install.sh) claude
|
||||
# 清理
|
||||
rm -rf /tmp/spec-coding-skills
|
||||
```
|
||||
|
||||
或者先下载再执行:
|
||||
### 方式 2:Git Submodule
|
||||
|
||||
```bash
|
||||
curl -sL https://git.internal.intelligrow.cn/zhangfucai/spec-coding-skills/raw/branch/main/install.sh -o /tmp/install-skills.sh
|
||||
bash /tmp/install-skills.sh both
|
||||
# 在你的项目根目录执行
|
||||
git submodule add https://git.internal.intelligrow.cn/zhangfucai/spec-coding-skills.git .spec-coding-skills
|
||||
|
||||
# 创建符号链接
|
||||
mkdir -p .claude
|
||||
ln -s ../.spec-coding-skills/.claude/skills .claude/skills
|
||||
```
|
||||
|
||||
### 安装脚本行为
|
||||
```bash
|
||||
# 提交变更
|
||||
git add .gitmodules .spec-coding-skills .claude/skills
|
||||
git commit -m "Add spec-coding-skills as submodule"
|
||||
```
|
||||
|
||||
- `both` 模式:同时安装 `.codex/skills/` 和 `.claude/skills/`,并生成缺失的 `AGENTS.md` + `CLAUDE.md`;已有则跳过
|
||||
- `codex` 模式:安装到 `.codex/skills/`,并在项目根目录生成缺失的 `AGENTS.md`;已有则跳过
|
||||
- `claude` 模式:安装到 `.claude/skills/`,并在项目根目录生成缺失的 `CLAUDE.md`;已有则跳过
|
||||
- 如果传入自定义目标目录,脚本会优先安装 Codex 版 skills;目标路径包含 `.claude/skills` 时自动切到 Claude 版
|
||||
|
||||
### 推荐用法
|
||||
|
||||
- 团队项目、模板仓库:用 `both`,一次安装双端,最省心
|
||||
- 只有你自己在 Codex 里用:用 `codex`
|
||||
- 仓库明确只服务 Claude Code:用 `claude`
|
||||
## 更新 Skills
|
||||
|
||||
### 更新策略
|
||||
### Submodule 方式
|
||||
|
||||
| 情况 | 处理方式 |
|
||||
|------|---------|
|
||||
| 新 skill(本地没有) | 直接安装 |
|
||||
| 本地未改 + 上游有更新 | 直接覆盖 |
|
||||
| 本地魔改过 + 上游有更新 | 写入上游新版,本地版备份为 `SKILL.md.local.bak` |
|
||||
| 本地和上游一致 | 跳过 |
|
||||
```bash
|
||||
# 更新到最新版本
|
||||
git submodule update --remote .spec-coding-skills
|
||||
```
|
||||
|
||||
项目根目录 guide 文件单独处理:
|
||||
### 直接复制方式
|
||||
|
||||
| 情况 | 处理方式 |
|
||||
|------|---------|
|
||||
| `AGENTS.md` / `CLAUDE.md` 不存在 | 从模板创建 |
|
||||
| `AGENTS.md` / `CLAUDE.md` 已存在 | 跳过,保持原样 |
|
||||
|
||||
恢复本地版本:`mv SKILL.md.local.bak SKILL.md`
|
||||
对比差异:`diff SKILL.md SKILL.md.local.bak`
|
||||
重新执行复制命令覆盖即可。
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 0->1 阶段:从需求到开发
|
||||
|
||||
Claude Code:
|
||||
|
||||
```bash
|
||||
/wp
|
||||
/wf
|
||||
/wd
|
||||
/wt
|
||||
/go
|
||||
```
|
||||
# 1. 准备文档
|
||||
/wp # 生成 PRD
|
||||
/wf # 生成 FeatureSummary
|
||||
/wd # 生成 DevelopmentPlan
|
||||
/wt # 生成 tasks
|
||||
|
||||
Codex:
|
||||
|
||||
```text
|
||||
/wp
|
||||
/wf
|
||||
/wd
|
||||
/wt
|
||||
/go
|
||||
# 2. 发射!
|
||||
/go # 激进模式,一口气完成所有任务
|
||||
```
|
||||
|
||||
### 1->100 阶段:持续迭代
|
||||
|
||||
Claude Code:
|
||||
|
||||
```bash
|
||||
/iter
|
||||
/go
|
||||
# 1. 描述变更需求
|
||||
/iter # 调研 → 澄清 → 更新 PRD 和 tasks
|
||||
|
||||
# 2. 执行变更
|
||||
/go # 自动识别新任务并执行
|
||||
```
|
||||
|
||||
Codex:
|
||||
|
||||
```text
|
||||
/iter
|
||||
/go
|
||||
```
|
||||
|
||||
也可以直接说自然语言,例如:
|
||||
|
||||
```text
|
||||
请用 rr skill 评审 doc/RequirementsDoc.md
|
||||
请用 wf skill 根据 PRD 生成 FeatureSummary
|
||||
请用 go skill 按 doc/tasks.md 执行未完成任务
|
||||
请用 doc skill 为认证模块写一份 300 字以内的使用说明
|
||||
请用 capture skill 把这次成功任务复刻到 ./docs/lessons
|
||||
请用 gitea skill 看当前仓库 open issues
|
||||
请用 gitea skill 给当前分支开 PR 到 main
|
||||
请用 gitea skill push 当前分支到远端
|
||||
请用 issue-drive skill 把当前 bug 拆成两张 Gitea issue
|
||||
```
|
||||
|
||||
### 复刻一次成功经验
|
||||
|
||||
Claude Code:
|
||||
|
||||
```bash
|
||||
/capture ./docs/lessons
|
||||
```
|
||||
|
||||
Codex:
|
||||
|
||||
```text
|
||||
/capture ./docs/lessons
|
||||
$capture /tmp/经验库
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 目录由调用时显式指定,不绑定 `doc/`
|
||||
- 输出文件名:`YYYY-MM-DD-task-slug.md`
|
||||
- 文件内容只包含一个 fenced `yaml` code block,便于复用和提取
|
||||
|
||||
### 统一 Gitea 入口
|
||||
|
||||
先配置环境变量:
|
||||
|
||||
```bash
|
||||
export GITEA_TOKEN=your_gitea_token
|
||||
export GITEA_BASE_URL=https://git.example.com # 可选;owner/repo 简写或 SSH origin 时推荐配置
|
||||
```
|
||||
|
||||
Claude Code:
|
||||
|
||||
```bash
|
||||
/gitea 看当前仓库 open issues
|
||||
/gitea 查 issue 7
|
||||
/gitea 把这个 bug 拆成两张 issue
|
||||
/gitea push
|
||||
/gitea 给当前分支开 PR 到 main
|
||||
/gitea 看当前仓库 open PR
|
||||
/gitea 在 PR 12 留言:已完成回归
|
||||
```
|
||||
|
||||
Codex:
|
||||
|
||||
```text
|
||||
/gitea 看当前仓库 open issues
|
||||
/gitea 查 issue 7
|
||||
/gitea 把这个 bug 拆成两张 issue
|
||||
/gitea push
|
||||
/gitea 给当前分支开 PR 到 main
|
||||
/gitea 看当前仓库 open PR
|
||||
/gitea 在 PR 12 留言:已完成回归
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `gitea` 是统一入口,会按自然语言自动路由到 issue、issue-drive、push 或 PR
|
||||
- push 默认先预检,再执行;只有明确要求 force push 时才会强推
|
||||
- PR v1 只支持同仓库 list / create / comment
|
||||
- 直接 `/gitea 看 issue 7` 或 `/gitea push` 时,skill 会优先从当前项目的 `git origin` 自动识别仓库
|
||||
- 传 `owner/repo` 时,需要 `GITEA_BASE_URL`
|
||||
- 当前仓库 `origin` 是 SSH 地址时,建议配置 `GITEA_BASE_URL`,避免 Web/API 域名与 SSH 域名不一致
|
||||
|
||||
### 专用入口:查询 Gitea Issues
|
||||
|
||||
Claude Code:
|
||||
|
||||
```bash
|
||||
/issue
|
||||
/issue 7
|
||||
/issue owner/repo
|
||||
/issue https://git.example.com/owner/repo
|
||||
/issue https://git.example.com/owner/repo --state=all --limit=20
|
||||
/issue https://git.example.com/owner/repo 7
|
||||
```
|
||||
|
||||
Codex:
|
||||
|
||||
```text
|
||||
/issue
|
||||
/issue 7
|
||||
/issue owner/repo
|
||||
/issue https://git.example.com/owner/repo
|
||||
/issue https://git.example.com/owner/repo --state=all --limit=20
|
||||
/issue https://git.example.com/owner/repo 7
|
||||
```
|
||||
|
||||
### 专用入口:创建 Gitea Issues
|
||||
|
||||
Claude Code:
|
||||
|
||||
```bash
|
||||
/issue-drive
|
||||
```
|
||||
|
||||
Codex:
|
||||
|
||||
```text
|
||||
/issue-drive
|
||||
```
|
||||
|
||||
自然语言也可以:
|
||||
|
||||
```text
|
||||
请用 issue-drive skill 把当前仓库这个线上 bug 拆成两张 Gitea issue
|
||||
请用 issue-drive skill 给 owner/repo 创建一张工程任务 issue,内容是补齐告警和回归测试
|
||||
```
|
||||
|
||||
`issue-drive` 会优先从当前仓库 `origin` 推断目标仓库;如果你显式指定其他仓库,skill 会按用户输入创建 issue。
|
||||
|
||||
### 工作流总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 0->1 全新项目 │
|
||||
│ │
|
||||
│ 需求 → wp → wf → wd → wt → go 🚀 │
|
||||
│ 需求 → /wp → /wf → /wd → /wt → /go 🚀 │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 1->100 持续迭代 │
|
||||
│ │
|
||||
│ 发现问题 → iter → go 🚀 │
|
||||
│ 发现问题 → /iter → /go 🚀 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@ -303,26 +139,12 @@ Codex:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── AGENTS.md # ← Codex 项目入口(Codex 模式)
|
||||
├── CLAUDE.md # ← Claude Code 项目入口(Claude 模式)
|
||||
├── .codex/
|
||||
│ └── skills/ # ← Codex Skills 安装位置
|
||||
│ ├── rr/
|
||||
│ ├── rp/
|
||||
│ ├── ...
|
||||
│ ├── iter/
|
||||
│ ├── capture/
|
||||
│ ├── gitea/
|
||||
│ └── issue-drive/
|
||||
├── .claude/
|
||||
│ └── skills/ # ← Claude Code Skills 安装位置
|
||||
│ └── skills/ # ← Skills 安装位置
|
||||
│ ├── rr/
|
||||
│ ├── rp/
|
||||
│ ├── ...
|
||||
│ ├── iter/
|
||||
│ ├── capture/
|
||||
│ ├── gitea/
|
||||
│ └── issue-drive/
|
||||
│ └── iter/
|
||||
├── doc/ # ← 文档输出位置
|
||||
│ ├── RequirementsDoc.md
|
||||
│ ├── PRD.md
|
||||
|
||||
@ -1,641 +0,0 @@
|
||||
# CI/CD 集成方案(最终落地版)
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录抖音评论管理系统的 CI/CD 方案:**Drone CI + 私有 Docker Registry + Docker Compose 自动部署**。
|
||||
|
||||
目标:
|
||||
|
||||
- 每天凌晨 0 点(北京时间)自动拉取 `main` 最新代码并部署
|
||||
- 创建 Tag(如 `v1.9.0210.7`)时自动构建并部署
|
||||
- 不在每次 `push` 时触发
|
||||
- 生产环境继续沿用 `docker-compose.prod.yml`
|
||||
|
||||
---
|
||||
|
||||
## 基础设施
|
||||
|
||||
| 角色 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| Gitea 服务器 | 腾讯云 `git.internal.intelligrow.cn` | 代码仓库 |
|
||||
| Drone CI 服务器 | Ubuntu x64 `192.168.31.107` | 执行构建任务 + 运行私有 Docker Registry |
|
||||
| 生产服务器 | Ubuntu x64 `192.168.31.48` | 运行 Docker Compose 生产服务,SSH 端口 3141 |
|
||||
|
||||
---
|
||||
|
||||
## 架构流程
|
||||
|
||||
```text
|
||||
定时触发(cron)或 创建 tag
|
||||
|
|
||||
v
|
||||
Drone 拉取代码
|
||||
|
|
||||
+-- 构建 backend 镜像并推送 Registry
|
||||
+-- 构建 frontend 镜像并推送 Registry
|
||||
v
|
||||
SSH 到生产服务器执行 deploy-remote.sh
|
||||
|
|
||||
+-- pull 新镜像
|
||||
+-- 停止 celery_beat
|
||||
+-- 更新 backend + celery_worker
|
||||
+-- 健康检查(容器内 /health)
|
||||
+-- alembic upgrade head(失败即中断)
|
||||
+-- 启动 celery_beat + frontend
|
||||
+-- 最终健康检查
|
||||
v
|
||||
发送企业微信通知(成功/失败)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```text
|
||||
.drone.yml # Drone CI 流水线
|
||||
scripts/
|
||||
+-- deploy.sh # 原有手动部署脚本
|
||||
+-- deploy-remote.sh # Drone 远程调用的自动部署脚本
|
||||
+-- server-setup.sh # 原有服务器初始化脚本
|
||||
docker-compose.prod.yml # 生产环境 compose 配置
|
||||
docs/
|
||||
+-- cicd_integration_updated.md # 本文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:Drone CI 服务器准备(一次性)
|
||||
|
||||
### 1.1 Drone CI 服务 (docker-compose.yml)
|
||||
|
||||
在 Drone CI 服务器 `~/drone/docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
drone-server:
|
||||
image: drone/drone:2
|
||||
container_name: drone-server
|
||||
restart: always
|
||||
ports:
|
||||
- "3080:80"
|
||||
environment:
|
||||
- DRONE_GITEA_SERVER=https://git.internal.intelligrow.cn
|
||||
- DRONE_GITEA_CLIENT_ID=<your-client-id>
|
||||
- DRONE_GITEA_CLIENT_SECRET=<your-client-secret>
|
||||
- DRONE_SERVER_HOST=drone.internal.intelligrow.cn
|
||||
- DRONE_SERVER_PROTO=https
|
||||
- DRONE_RPC_SECRET=<your-rpc-secret>
|
||||
- DRONE_USER_CREATE=username:<your-gitea-username>,admin:true
|
||||
volumes:
|
||||
- ./data:/data
|
||||
|
||||
drone-runner:
|
||||
image: drone/drone-runner-docker:1
|
||||
container_name: drone-runner
|
||||
restart: always
|
||||
depends_on:
|
||||
- drone-server
|
||||
environment:
|
||||
- DRONE_RPC_PROTO=http
|
||||
- DRONE_RPC_HOST=drone-server
|
||||
- DRONE_RPC_SECRET=<your-rpc-secret>
|
||||
- DRONE_RUNNER_CAPACITY=2
|
||||
- DRONE_RUNNER_NAME=drone-runner-1
|
||||
- DRONE_RUNNER_PRIVILEGED_IMAGES=plugins/docker
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
```
|
||||
|
||||
关键配置说明:
|
||||
|
||||
- **`DRONE_RPC_PROTO=http`**:Runner 在 Docker 内网直连 drone-server 容器的 80 端口,不走 HTTPS。外部通过反向代理 `drone.internal.intelligrow.cn` 访问时才用 HTTPS。
|
||||
- **`DRONE_USER_CREATE`**:`username` 必须与 Gitea 登录用户名完全一致(不是邮箱),否则管理员权限不生效。
|
||||
- **`DRONE_RUNNER_PRIVILEGED_IMAGES=plugins/docker`**:允许 plugins/docker 以特权模式运行(本方案最终未使用 plugins/docker,但保留配置以备后用)。
|
||||
|
||||
### 1.2 启动私有 Registry
|
||||
|
||||
在 Drone CI 服务器执行:
|
||||
|
||||
```bash
|
||||
docker run -d --name registry \
|
||||
-p 5000:5000 \
|
||||
-v /opt/registry-data:/var/lib/registry \
|
||||
--restart always \
|
||||
registry:2
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/v2/_catalog
|
||||
# 预期输出: {"repositories":[]}
|
||||
```
|
||||
|
||||
### 1.3 配置 insecure registry
|
||||
|
||||
**Drone CI 服务器** `/etc/docker/daemon.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.1panel.live",
|
||||
"https://docker.1panel.dev",
|
||||
"https://docker.1ms.run"
|
||||
],
|
||||
"insecure-registries": [
|
||||
"docker.internal.intelligrow.cn:5000",
|
||||
"192.168.31.107:5000"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**生产服务器** `/etc/docker/daemon.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.1panel.live",
|
||||
"https://docker.1panel.dev",
|
||||
"https://docker.1ms.run"
|
||||
],
|
||||
"insecure-registries": ["docker.internal.intelligrow.cn:5000"]
|
||||
}
|
||||
```
|
||||
|
||||
修改后重启 Docker:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
> 注意:`insecure-registries` 中不要带 `http://` 前缀,直接写 `host:port` 格式。
|
||||
|
||||
### 1.4 配置 SSH 免密
|
||||
|
||||
在 Drone CI 服务器生成密钥并添加到生产服务器:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "drone-ci" -f ~/.ssh/drone_deploy -N ""
|
||||
ssh-copy-id -i ~/.ssh/drone_deploy.pub -p 3141 miaosi@192.168.31.48
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/drone_deploy -p 3141 miaosi@192.168.31.48 "echo ok"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:Drone 仓库设置
|
||||
|
||||
### 2.1 开启 Trusted 模式
|
||||
|
||||
在 Drone 面板 → 仓库 Settings → General → Project Settings → 勾选 **Trusted**。
|
||||
|
||||
> 需要管理员权限。如果看不到 Trusted 选项,检查 `DRONE_USER_CREATE` 的 username 是否与 Gitea 用户名一致。
|
||||
|
||||
### 2.2 配置 Secrets
|
||||
|
||||
在 Drone 面板(仓库设置 → Secrets)添加:
|
||||
|
||||
| Secret 名称 | 用途 | 实际值示例 |
|
||||
|-------------|------|-----------|
|
||||
| `backend_repo` | 后端镜像完整仓库地址 | `docker.internal.intelligrow.cn:5000/douyin-backend` |
|
||||
| `frontend_repo` | 前端镜像完整仓库地址 | `docker.internal.intelligrow.cn:5000/douyin-frontend` |
|
||||
| `deploy_host` | 生产服务器 IP | `192.168.31.48` |
|
||||
| `deploy_user` | SSH 用户 | `miaosi` |
|
||||
| `deploy_ssh_key` | SSH 私钥内容 | `-----BEGIN OPENSSH PRIVATE KEY-----...-----END OPENSSH PRIVATE KEY-----` |
|
||||
| `deploy_path` | 生产服务器部署目录 | `/opt/docker/douyin_comments_management` |
|
||||
| `wecom_webhook` | 企业微信 Webhook(可选) | `https://qyapi.weixin.qq.com/...` |
|
||||
|
||||
> 注意:`backend_repo` 和 `frontend_repo` 的 Registry 地址必须与生产服务器 `.env` 中的 `DOCKER_REGISTRY` 使用相同的主机名(都用域名或都用 IP),否则 Docker 会认为是不同的镜像。
|
||||
|
||||
### 2.3 配置 Cron Job
|
||||
|
||||
在 Drone 面板(仓库设置 → Cron Jobs)添加:
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|------|
|
||||
| Name | `nightly-build` |
|
||||
| Branch | `main` |
|
||||
| Schedule | `0 16 * * *` |
|
||||
|
||||
说明:
|
||||
|
||||
- Drone 默认按 UTC 解释 Cron
|
||||
- `0 16 * * *` = UTC 16:00 = 北京时间次日 00:00
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:配置文件(最终落地版本)
|
||||
|
||||
### 3.1 `.drone.yml`
|
||||
|
||||
```yaml
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-deploy
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
- cron
|
||||
|
||||
volumes:
|
||||
- name: dockersock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
steps:
|
||||
- name: build-backend
|
||||
image: docker:27-cli
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
BACKEND_REPO:
|
||||
from_secret: backend_repo
|
||||
commands:
|
||||
- '[ -n "$BACKEND_REPO" ] || (echo "backend_repo secret is empty" && exit 1)'
|
||||
- echo "Building backend image tag:${DRONE_TAG:-latest}"
|
||||
- docker build -t "$BACKEND_REPO:${DRONE_TAG:-latest}" -t "$BACKEND_REPO:latest" ./backend
|
||||
- docker push "$BACKEND_REPO:${DRONE_TAG:-latest}"
|
||||
- docker push "$BACKEND_REPO:latest"
|
||||
|
||||
- name: build-frontend
|
||||
image: docker:27-cli
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
FRONTEND_REPO:
|
||||
from_secret: frontend_repo
|
||||
commands:
|
||||
- '[ -n "$FRONTEND_REPO" ] || (echo "frontend_repo secret is empty" && exit 1)'
|
||||
- echo "Building frontend image tag:${DRONE_TAG:-latest}"
|
||||
- docker build -t "$FRONTEND_REPO:${DRONE_TAG:-latest}" -t "$FRONTEND_REPO:latest" ./frontend
|
||||
- docker push "$FRONTEND_REPO:${DRONE_TAG:-latest}"
|
||||
- docker push "$FRONTEND_REPO:latest"
|
||||
|
||||
- name: deploy
|
||||
image: appleboy/drone-ssh
|
||||
environment:
|
||||
DEPLOY_PATH:
|
||||
from_secret: deploy_path
|
||||
settings:
|
||||
host:
|
||||
from_secret: deploy_host
|
||||
username:
|
||||
from_secret: deploy_user
|
||||
key:
|
||||
from_secret: deploy_ssh_key
|
||||
port: 3141
|
||||
command_timeout: 1800s
|
||||
script_stop: true
|
||||
envs:
|
||||
- DRONE_TAG
|
||||
- DEPLOY_PATH
|
||||
script:
|
||||
- IMAGE_TAG="$DRONE_TAG"; [ -n "$IMAGE_TAG" ] || IMAGE_TAG="latest"
|
||||
- cd "$DEPLOY_PATH"
|
||||
- bash scripts/deploy-remote.sh "$IMAGE_TAG"
|
||||
|
||||
- name: notify-success
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
WECOM_WEBHOOK:
|
||||
from_secret: wecom_webhook
|
||||
commands:
|
||||
- |
|
||||
if [ -n "${WECOM_WEBHOOK:-}" ]; then
|
||||
VERSION="$DRONE_TAG"
|
||||
[ -n "$VERSION" ] || VERSION="nightly-$(date +%Y%m%d)"
|
||||
curl -sS -X POST "$WECOM_WEBHOOK" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"✅ 部署成功\\n版本: ${VERSION}\\n仓库: ${DRONE_REPO}\\n时间: $(date '+%Y-%m-%d %H:%M:%S')\"}}"
|
||||
fi
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
|
||||
- name: notify-failure
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
WECOM_WEBHOOK:
|
||||
from_secret: wecom_webhook
|
||||
commands:
|
||||
- |
|
||||
if [ -n "${WECOM_WEBHOOK:-}" ]; then
|
||||
VERSION="$DRONE_TAG"
|
||||
[ -n "$VERSION" ] || VERSION="nightly-$(date +%Y%m%d)"
|
||||
curl -sS -X POST "$WECOM_WEBHOOK" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"❌ 部署失败\\n版本: ${VERSION}\\n仓库: ${DRONE_REPO}\\n构建: ${DRONE_BUILD_LINK}\"}}"
|
||||
fi
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
```
|
||||
|
||||
### 3.2 `scripts/deploy-remote.sh`
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# ========================================
|
||||
# 远程部署脚本 - 被 Drone CI SSH 调用
|
||||
# ========================================
|
||||
# 用法: bash scripts/deploy-remote.sh [image_tag]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
|
||||
|
||||
DEPLOY_PATH="${DEPLOY_PATH:-/opt/docker/douyin_comments_management}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}"
|
||||
IMAGE_TAG="${1:-latest}"
|
||||
LOCK_FILE="/tmp/douyin-deploy.lock"
|
||||
MAX_ATTEMPTS="${MAX_ATTEMPTS:-30}"
|
||||
|
||||
cleanup_lock() {
|
||||
rm -f "$LOCK_FILE"
|
||||
}
|
||||
|
||||
acquire_lock() {
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
local old_pid
|
||||
old_pid=$(cat "$LOCK_FILE" 2>/dev/null || true)
|
||||
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
|
||||
log_error "已有部署进程运行中 (PID: $old_pid)"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$LOCK_FILE"
|
||||
fi
|
||||
|
||||
echo "$$" > "$LOCK_FILE"
|
||||
trap cleanup_lock EXIT
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose -f "$COMPOSE_FILE" "$@"
|
||||
}
|
||||
|
||||
wait_backend_healthy() {
|
||||
local attempt=1
|
||||
|
||||
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
||||
if compose exec -T backend python -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3);sys.exit(0)" >/dev/null 2>&1; then
|
||||
log_info "后端健康检查通过"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "等待后端就绪... (${attempt}/${MAX_ATTEMPTS})"
|
||||
sleep 3
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
acquire_lock
|
||||
|
||||
[ -d "$DEPLOY_PATH" ] || { log_error "部署目录不存在: $DEPLOY_PATH"; exit 1; }
|
||||
cd "$DEPLOY_PATH"
|
||||
|
||||
[ -f "$COMPOSE_FILE" ] || { log_error "Compose 文件不存在: $COMPOSE_FILE"; exit 1; }
|
||||
|
||||
export IMAGE_TAG
|
||||
export VERSION="$IMAGE_TAG"
|
||||
|
||||
log_info "开始部署,镜像版本: $IMAGE_TAG"
|
||||
|
||||
log_info "拉取最新镜像..."
|
||||
compose pull backend celery_worker celery_beat frontend
|
||||
|
||||
log_info "停止 celery_beat..."
|
||||
compose stop celery_beat || true
|
||||
|
||||
log_info "更新 backend 与 celery_worker..."
|
||||
compose up -d --no-deps backend celery_worker
|
||||
|
||||
if ! wait_backend_healthy; then
|
||||
log_error "后端未在预期时间内就绪"
|
||||
compose logs --tail=200 backend || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "执行数据库迁移..."
|
||||
compose exec -T backend alembic upgrade head
|
||||
|
||||
log_info "启动 celery_beat..."
|
||||
compose up -d --no-deps celery_beat
|
||||
|
||||
log_info "更新 frontend..."
|
||||
compose up -d --no-deps frontend
|
||||
|
||||
log_info "清理孤儿容器..."
|
||||
compose up -d --remove-orphans
|
||||
|
||||
if ! wait_backend_healthy; then
|
||||
log_error "部署完成后健康检查失败"
|
||||
compose logs --tail=200 backend || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker image prune -f >/dev/null 2>&1 || log_warn "镜像清理失败,已跳过"
|
||||
|
||||
log_info "部署完成!版本: $IMAGE_TAG"
|
||||
compose ps
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
> 注意:脚本同时 export `IMAGE_TAG` 和 `VERSION`,因为生产服务器的 `docker-compose.prod.yml` 使用 `${VERSION:-latest}` 作为镜像 tag 变量。
|
||||
|
||||
---
|
||||
|
||||
## 生产服务器 `.env` 配置
|
||||
|
||||
确保 `/opt/docker/douyin_comments_management/.env` 至少包含:
|
||||
|
||||
```bash
|
||||
DOCKER_REGISTRY=docker.internal.intelligrow.cn:5000
|
||||
|
||||
POSTGRES_PASSWORD=xxx
|
||||
REDIS_PASSWORD=xxx
|
||||
RABBITMQ_PASSWORD=xxx
|
||||
# 其余生产配置保持现有值
|
||||
```
|
||||
|
||||
> `DOCKER_REGISTRY` 的值必须与 Drone Secret 中 `backend_repo` / `frontend_repo` 的 Registry 主机名一致。
|
||||
|
||||
---
|
||||
|
||||
## 回滚方案
|
||||
|
||||
### 方式 1:推送旧版本 tag 触发重新部署
|
||||
|
||||
```bash
|
||||
git tag v1.8.1-rollback v1.8.1
|
||||
git push origin v1.8.1-rollback
|
||||
```
|
||||
|
||||
### 方式 2:生产服务器手动回滚
|
||||
|
||||
```bash
|
||||
ssh -p 3141 miaosi@192.168.31.48
|
||||
cd /opt/docker/douyin_comments_management
|
||||
bash scripts/deploy-remote.sh v1.8.1
|
||||
```
|
||||
|
||||
### 方式 3:手动 compose 回滚
|
||||
|
||||
```bash
|
||||
cd /opt/docker/douyin_comments_management
|
||||
export VERSION=v1.8.1
|
||||
docker compose -f docker-compose.prod.yml pull backend celery_worker celery_beat frontend
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps backend celery_worker celery_beat frontend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证方式
|
||||
|
||||
### 验证 Registry
|
||||
|
||||
```bash
|
||||
# Drone CI 服务器
|
||||
curl http://localhost:5000/v2/_catalog
|
||||
curl http://localhost:5000/v2/douyin-backend/tags/list
|
||||
curl http://localhost:5000/v2/douyin-frontend/tags/list
|
||||
```
|
||||
|
||||
### 验证构建与部署触发
|
||||
|
||||
```bash
|
||||
# 方式1:Tag 触发
|
||||
git tag v1.9.0210.8
|
||||
git push origin v1.9.0210.8
|
||||
|
||||
# 方式2:Drone 面板手动触发 cron 或点击 NEW BUILD
|
||||
```
|
||||
|
||||
### 验证生产服务
|
||||
|
||||
```bash
|
||||
# 检查容器状态
|
||||
ssh -p 3141 miaosi@192.168.31.48 "cd /opt/docker/douyin_comments_management && docker compose -f docker-compose.prod.yml ps"
|
||||
|
||||
# 后端健康检查(容器内)
|
||||
ssh -p 3141 miaosi@192.168.31.48 "cd /opt/docker/douyin_comments_management && docker compose -f docker-compose.prod.yml exec -T backend python -c \"import urllib.request;urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3);print('ok')\""
|
||||
|
||||
# 数据库迁移状态
|
||||
ssh -p 3141 miaosi@192.168.31.48 "cd /opt/docker/douyin_comments_management && docker compose -f docker-compose.prod.yml exec -T backend alembic current"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 问题 | 排查方式 |
|
||||
|------|---------|
|
||||
| YAML 解析错误 | 检查 `.drone.yml` 语法,Drone 对 `volumes`/`environment` 格式敏感 |
|
||||
| 构建失败 | Drone 面板查看 pipeline 日志 |
|
||||
| 镜像推送失败 (HTTPS) | 确认两台服务器 `insecure-registries` 配置正确(不带 `http://` 前缀) |
|
||||
| Secret 为空 | 使用 `environment: { VAR: { from_secret: name } }` 而非 `secrets` 字段 |
|
||||
| Drone 变量替换冲突 | `${DRONE_TAG}` 是 Drone 变量可直接使用;自定义 shell 变量用 `$$VAR` 转义 |
|
||||
| Tag 不触发构建 | 检查 Gitea Webhook 是否勾选"创建"事件;`.drone.yml` trigger 不要加 `cron: [name]` |
|
||||
| Step is pending | 检查 Runner 是否连通 Server;仓库是否开启 Trusted |
|
||||
| DinD 启动失败 | 改用 `docker:27-cli` + 挂载宿主机 Docker socket(本方案采用的方式) |
|
||||
| deploy 找不到脚本 | 确认 `deploy-remote.sh` 已复制到生产服务器部署目录的 `scripts/` 下 |
|
||||
| Docker 权限不足 | 生产服务器执行 `sudo usermod -aG docker <user>` 后重新登录 |
|
||||
| 镜像名 invalid reference | 检查生产服务器 `.env` 中 `DOCKER_REGISTRY` 变量是否正确设置 |
|
||||
| 数据库迁移失败 | `docker compose -f docker-compose.prod.yml logs -f backend` |
|
||||
| Cron 未触发 | 核对 Drone Cron 名称、分支是否 `main`、Schedule 是否正确 |
|
||||
|
||||
---
|
||||
|
||||
## 踩坑记录
|
||||
|
||||
以下是实际部署过程中遇到的问题及解决方案,供后续参考:
|
||||
|
||||
### 1. Drone YAML 解析错误 (`cannot unmarshal !!map into string`)
|
||||
|
||||
**原因**:Drone Docker pipeline 对 `environment` 中 `from_secret` 语法和 `volumes` 格式有特定要求。早期版本同时使用 `volumes` + `environment: from_secret` 会触发解析错误。
|
||||
|
||||
**解决**:确保仓库开启 Trusted 模式后,`volumes` 和 `environment: from_secret` 可以正常共存。
|
||||
|
||||
### 2. Tag 推送不触发构建
|
||||
|
||||
**原因**:`.drone.yml` 中同时配置了 `event: [tag, cron]` 和 `cron: [nightly-build]`,Drone 将触发条件做 AND 运算。Tag 事件无法满足 cron 条件,导致永远不触发。
|
||||
|
||||
**解决**:移除 `cron: [nightly-build]` 过滤,只保留 `event: [tag, cron]`。
|
||||
|
||||
### 3. plugins/docker DinD 启动失败
|
||||
|
||||
**原因**:`plugins/docker` 插件内部启动 Docker 守护进程(Docker-in-Docker),可能因 cgroup/存储驱动兼容性问题无法启动。
|
||||
|
||||
**解决**:放弃 `plugins/docker`,改用 `docker:27-cli` 镜像 + 挂载宿主机 Docker socket 的方式构建镜像。
|
||||
|
||||
### 4. plugins/docker 要求 semver 格式 tag
|
||||
|
||||
**原因**:`auto_tag: true` 配置要求 Git tag 符合语义化版本(如 `v1.0.0`),非标准格式(如 `v1.9.0210.1`)会解析失败。
|
||||
|
||||
**解决**:移除 `auto_tag`,改用 `${DRONE_TAG:-latest}` 手动指定镜像 tag。
|
||||
|
||||
### 5. Drone 变量替换与 Shell 变量冲突
|
||||
|
||||
**原因**:Drone 会在执行前对 `${VAR}` 语法做自身的变量替换。自定义 shell 变量(如 `TAG="xxx"; echo ${TAG}`)中的 `${TAG}` 会被 Drone 替换为空。
|
||||
|
||||
**解决**:直接使用 Drone 内置变量 `${DRONE_TAG:-latest}`,避免中间 shell 变量。
|
||||
|
||||
### 6. `secrets` 字段注入环境变量不生效
|
||||
|
||||
**原因**:Drone 步骤级 `secrets:` 字段在某些场景下不会将 secret 注入为环境变量。
|
||||
|
||||
**解决**:改用 `environment: { VAR: { from_secret: name } }` 显式声明。
|
||||
|
||||
### 7. Runner 连接 Server 失败
|
||||
|
||||
**原因**:Runner 配置 `DRONE_RPC_PROTO=https` 但 drone-server 容器内部只监听 80 端口(HTTPS 由外部反向代理终止)。
|
||||
|
||||
**解决**:Runner 通过外部域名 `drone.internal.intelligrow.cn` 连接(走反向代理的 HTTPS),而非直连容器内网。
|
||||
|
||||
### 8. 管理员权限不生效(看不到 Trusted 选项)
|
||||
|
||||
**原因**:`DRONE_USER_CREATE=username:zhanghuayu@intelligrow.ai,admin:true` 中的 username 使用了邮箱而非 Gitea 登录用户名。
|
||||
|
||||
**解决**:改为 `username:zhanghuayu,admin:true`(与 Gitea 用户名完全一致)。
|
||||
|
||||
### 9. 生产服务器镜像名 invalid reference
|
||||
|
||||
**原因**:`docker-compose.prod.yml` 使用 `${DOCKER_REGISTRY}` 变量,但 `.env` 中未设置或变量名不匹配(曾误设为 `REGISTRY_HOST`)。
|
||||
|
||||
**解决**:在 `.env` 中添加 `DOCKER_REGISTRY=docker.internal.intelligrow.cn:5000`,确保变量名与 compose 文件一致。
|
||||
|
||||
### 10. insecure-registries 格式错误
|
||||
|
||||
**原因**:`/etc/docker/daemon.json` 中 `insecure-registries` 配置了 `http://docker.internal.intelligrow.cn:5000`(带协议前缀),Docker 不识别。
|
||||
|
||||
**解决**:去掉 `http://` 前缀,直接写 `docker.internal.intelligrow.cn:5000`。
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Registry 无认证**:仅建议内网使用;公网请加 TLS + 认证(或使用 Harbor)。
|
||||
2. **Docker Socket 挂载**:构建步骤挂载宿主机 Docker socket,需仓库开启 Trusted 模式。
|
||||
3. **并发部署保护**:`deploy-remote.sh` 使用 `/tmp/douyin-deploy.lock` 防止并发部署。
|
||||
4. **迁移失败即中断**:`alembic upgrade head` 失败会使整个部署失败,防止"假成功"。
|
||||
5. **健康检查不依赖 curl**:采用容器内 Python 请求 `/health`,与当前镜像一致。
|
||||
6. **deploy-remote.sh 需手动同步**:该脚本存放在生产服务器上,代码更新后需手动复制或通过部署流程同步。
|
||||
7. **VERSION 与 IMAGE_TAG**:`deploy-remote.sh` 同时 export 两个变量,兼容不同 compose 文件的命名。
|
||||
@ -1,947 +0,0 @@
|
||||
# Drone CI/CD 通用部署指南
|
||||
|
||||
基于 **Drone CI + 私有 Docker Registry + Docker Compose** 的自动化部署方案。适用于任何基于 Docker 镜像部署的项目。
|
||||
|
||||
本指南基于抖音评论管理系统的实际落地经验编写,涵盖从零搭建到生产可用的全流程。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [前提条件](#前提条件)
|
||||
- [架构概览](#架构概览)
|
||||
- [第一步:基础设施准备(一次性)](#第一步基础设施准备一次性)
|
||||
- [第二步:项目接入 Drone CI](#第二步项目接入-drone-ci)
|
||||
- [第三步:编写 .drone.yml](#第三步编写-droneyml)
|
||||
- [第四步:编写部署脚本](#第四步编写部署脚本)
|
||||
- [第五步:生产服务器配置](#第五步生产服务器配置)
|
||||
- [第六步:配置 Drone 仓库设置](#第六步配置-drone-仓库设置)
|
||||
- [第七步:验证](#第七步验证)
|
||||
- [回滚方案](#回滚方案)
|
||||
- [多项目管理](#多项目管理)
|
||||
- [踩坑清单](#踩坑清单)
|
||||
- [故障排查速查表](#故障排查速查表)
|
||||
- [附录:完整模板文件](#附录完整模板文件)
|
||||
|
||||
---
|
||||
|
||||
## 前提条件
|
||||
|
||||
| 组件 | 要求 |
|
||||
|------|------|
|
||||
| Gitea 服务器 | 已有,可通过 HTTPS 访问 |
|
||||
| Drone CI 服务器 | Ubuntu x64,已安装 Docker,已部署 Drone Server + Runner |
|
||||
| 生产服务器 | Ubuntu x64,已安装 Docker + Docker Compose |
|
||||
| 项目 | 包含 Dockerfile,可通过 `docker compose` 部署 |
|
||||
|
||||
如果 Drone CI 尚未部署,参见 [附录 A:Drone CI 部署](#附录-a-drone-ci-部署)。
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
```text
|
||||
触发方式:
|
||||
A) 创建 Git Tag(如 v1.0.0)→ 自动构建部署指定版本
|
||||
B) Drone Cron 定时任务 → 自动构建部署 latest
|
||||
|
||||
流水线:
|
||||
Drone CI 拉取代码
|
||||
|
|
||||
+-- 构建镜像 1 (如 backend) → 推送到 Registry
|
||||
+-- 构建镜像 2 (如 frontend) → 推送到 Registry
|
||||
+-- 构建镜像 N ...
|
||||
|
|
||||
v
|
||||
SSH 到生产服务器 → 执行部署脚本
|
||||
|
|
||||
+-- docker compose pull (拉取新镜像)
|
||||
+-- docker compose up -d (滚动更新)
|
||||
+-- 健康检查
|
||||
+-- 数据库迁移(如有)
|
||||
|
|
||||
v
|
||||
发送通知(企业微信/钉钉/飞书,可选)
|
||||
```
|
||||
|
||||
三台服务器各司其职:
|
||||
|
||||
| 角色 | 职责 |
|
||||
|------|------|
|
||||
| Gitea 服务器 | 代码仓库,通过 Webhook 触发 Drone |
|
||||
| Drone CI 服务器 | 执行构建 + 运行私有 Docker Registry |
|
||||
| 生产服务器 | 运行 Docker Compose 部署的生产服务 |
|
||||
|
||||
---
|
||||
|
||||
## 第一步:基础设施准备(一次性)
|
||||
|
||||
以下操作只需做一次,所有项目共享。
|
||||
|
||||
### 1.1 启动私有 Docker Registry
|
||||
|
||||
在 **Drone CI 服务器**上执行:
|
||||
|
||||
```bash
|
||||
docker run -d --name registry \
|
||||
-p 5000:5000 \
|
||||
-v /opt/registry-data:/var/lib/registry \
|
||||
--restart always \
|
||||
registry:2
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/v2/_catalog
|
||||
# 预期输出: {"repositories":[]}
|
||||
```
|
||||
|
||||
### 1.2 配置 insecure-registries
|
||||
|
||||
由于 Registry 未配置 TLS,需要在 **所有需要访问 Registry 的服务器** 上配置 insecure-registries。
|
||||
|
||||
编辑 `/etc/docker/daemon.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"insecure-registries": ["<DRONE_CI_IP_OR_DOMAIN>:5000"]
|
||||
}
|
||||
```
|
||||
|
||||
然后重启 Docker:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
> **重要**:
|
||||
> - `insecure-registries` 的值 **不要** 带 `http://` 前缀,直接写 `host:port`
|
||||
> - 如果已有 `registry-mirrors` 等其他配置,合并到同一个 JSON 文件中,不要覆盖
|
||||
> - Drone CI 服务器和生产服务器 **都需要** 配置
|
||||
|
||||
**示例**(假设 Drone CI 服务器 IP 为 `192.168.1.100`,域名为 `ci.example.com`):
|
||||
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.1panel.live"
|
||||
],
|
||||
"insecure-registries": [
|
||||
"ci.example.com:5000",
|
||||
"192.168.1.100:5000"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 配置 SSH 免密登录
|
||||
|
||||
在 **Drone CI 服务器**上生成部署专用密钥:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "drone-ci-deploy" -f ~/.ssh/drone_deploy -N ""
|
||||
```
|
||||
|
||||
将公钥添加到 **生产服务器**:
|
||||
|
||||
```bash
|
||||
# 如果 SSH 端口是 22
|
||||
ssh-copy-id -i ~/.ssh/drone_deploy.pub <user>@<production-server-ip>
|
||||
|
||||
# 如果 SSH 端口不是 22(如 3141)
|
||||
ssh-copy-id -i ~/.ssh/drone_deploy.pub -p <port> <user>@<production-server-ip>
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/drone_deploy -p <port> <user>@<production-server-ip> "echo ok"
|
||||
```
|
||||
|
||||
### 1.4 确保生产服务器用户有 Docker 权限
|
||||
|
||||
```bash
|
||||
# 在生产服务器上
|
||||
sudo usermod -aG docker <deploy-user>
|
||||
# 需要重新登录生效
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第二步:项目接入 Drone CI
|
||||
|
||||
### 2.1 在 Drone 面板激活仓库
|
||||
|
||||
1. 打开 Drone CI 面板(如 `https://drone.example.com`)
|
||||
2. 点击 **SYNC** 同步 Gitea 仓库列表
|
||||
3. 找到目标仓库,点击 **ACTIVATE**
|
||||
|
||||
### 2.2 开启 Trusted 模式
|
||||
|
||||
仓库 Settings → General → Project Settings → 勾选 **Trusted**。
|
||||
|
||||
> 这是必须的,因为构建步骤需要挂载宿主机 Docker socket。如果看不到 Trusted 选项,说明当前用户不是 Drone 管理员。检查 Drone Server 的 `DRONE_USER_CREATE` 环境变量,`username` 必须与 Gitea 登录用户名完全一致(不是邮箱)。
|
||||
|
||||
### 2.3 配置 Secrets
|
||||
|
||||
在仓库 Settings → Secrets 添加以下密钥:
|
||||
|
||||
| Secret 名称 | 说明 | 填写示例 |
|
||||
|-------------|------|---------|
|
||||
| `image_repo_<service>` | 每个需构建的服务的完整镜像地址 | `ci.example.com:5000/myproject-backend` |
|
||||
| `deploy_host` | 生产服务器 IP 或域名 | `192.168.1.200` |
|
||||
| `deploy_user` | SSH 登录用户名 | `ubuntu` |
|
||||
| `deploy_ssh_key` | SSH **私钥**完整内容 | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
|
||||
| `deploy_path` | 生产服务器上的项目部署目录 | `/opt/myproject` |
|
||||
| `wecom_webhook` | 通知 Webhook URL(可选) | `https://qyapi.weixin.qq.com/...` |
|
||||
|
||||
> **关于 `image_repo_<service>`**:每个需要构建的服务单独一个 secret。例如,项目有 backend 和 frontend,就创建 `backend_repo` 和 `frontend_repo`。Registry 地址必须与生产服务器 `.env` 中的镜像地址一致。
|
||||
|
||||
### 2.4 配置 Cron 定时构建(可选)
|
||||
|
||||
仓库 Settings → Cron Jobs 添加:
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|------|------|------|
|
||||
| Name | `nightly-build` | 任务名称 |
|
||||
| Branch | `main` | 构建分支 |
|
||||
| Schedule | `0 16 * * *` | UTC 16:00 = 北京时间 00:00 |
|
||||
|
||||
> Drone 使用 UTC 时区解释 Cron 表达式。北京时间 = UTC + 8。
|
||||
|
||||
---
|
||||
|
||||
## 第三步:编写 .drone.yml
|
||||
|
||||
在项目根目录创建 `.drone.yml`。
|
||||
|
||||
### 核心原则
|
||||
|
||||
1. **使用 `docker:27-cli` + 宿主机 Docker socket**,不用 `plugins/docker` DinD(避免 DinD 启动失败问题)
|
||||
2. **使用 `environment: from_secret`** 注入密钥,不用 `secrets:` 字段
|
||||
3. **使用 `${DRONE_TAG:-latest}`** 作为镜像 tag,不自定义中间变量(避免 Drone 变量替换冲突)
|
||||
4. **触发条件只用 `event`**,不叠加 `cron: [name]`(Drone 触发条件是 AND 运算)
|
||||
|
||||
### 模板
|
||||
|
||||
```yaml
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-deploy
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag # Git Tag 触发
|
||||
- cron # 定时触发
|
||||
|
||||
volumes:
|
||||
- name: dockersock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
steps:
|
||||
# ==========================================
|
||||
# 构建步骤 - 每个服务一个 step
|
||||
# ==========================================
|
||||
- name: build-<service-1>
|
||||
image: docker:27-cli
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
IMAGE_REPO:
|
||||
from_secret: <service-1>_repo
|
||||
commands:
|
||||
- '[ -n "$IMAGE_REPO" ] || (echo "<service-1>_repo secret is empty" && exit 1)'
|
||||
- echo "Building <service-1>, tag=${DRONE_TAG:-latest}"
|
||||
- docker build -t "$IMAGE_REPO:${DRONE_TAG:-latest}" -t "$IMAGE_REPO:latest" ./<service-1-context>
|
||||
- docker push "$IMAGE_REPO:${DRONE_TAG:-latest}"
|
||||
- docker push "$IMAGE_REPO:latest"
|
||||
|
||||
- name: build-<service-2>
|
||||
image: docker:27-cli
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
IMAGE_REPO:
|
||||
from_secret: <service-2>_repo
|
||||
commands:
|
||||
- '[ -n "$IMAGE_REPO" ] || (echo "<service-2>_repo secret is empty" && exit 1)'
|
||||
- echo "Building <service-2>, tag=${DRONE_TAG:-latest}"
|
||||
- docker build -t "$IMAGE_REPO:${DRONE_TAG:-latest}" -t "$IMAGE_REPO:latest" ./<service-2-context>
|
||||
- docker push "$IMAGE_REPO:${DRONE_TAG:-latest}"
|
||||
- docker push "$IMAGE_REPO:latest"
|
||||
|
||||
# ==========================================
|
||||
# 部署步骤
|
||||
# ==========================================
|
||||
- name: deploy
|
||||
image: appleboy/drone-ssh
|
||||
environment:
|
||||
DEPLOY_PATH:
|
||||
from_secret: deploy_path
|
||||
settings:
|
||||
host:
|
||||
from_secret: deploy_host
|
||||
username:
|
||||
from_secret: deploy_user
|
||||
key:
|
||||
from_secret: deploy_ssh_key
|
||||
port: 22 # ← 改成实际 SSH 端口
|
||||
command_timeout: 1800s
|
||||
script_stop: true
|
||||
envs:
|
||||
- DRONE_TAG
|
||||
- DEPLOY_PATH
|
||||
script:
|
||||
- IMAGE_TAG="$DRONE_TAG"; [ -n "$IMAGE_TAG" ] || IMAGE_TAG="latest"
|
||||
- cd "$DEPLOY_PATH"
|
||||
- bash scripts/deploy-remote.sh "$IMAGE_TAG"
|
||||
|
||||
# ==========================================
|
||||
# 通知步骤(可选)
|
||||
# ==========================================
|
||||
- name: notify-success
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
WEBHOOK_URL:
|
||||
from_secret: wecom_webhook
|
||||
commands:
|
||||
- |
|
||||
if [ -n "${WEBHOOK_URL:-}" ]; then
|
||||
VERSION="${DRONE_TAG:-nightly-$(date +%Y%m%d)}"
|
||||
curl -sS -X POST "$WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"✅ 部署成功\nProject: ${DRONE_REPO}\n版本: ${VERSION}\n时间: $(date '+%Y-%m-%d %H:%M:%S')\"}}"
|
||||
fi
|
||||
when:
|
||||
status: [success]
|
||||
|
||||
- name: notify-failure
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
WEBHOOK_URL:
|
||||
from_secret: wecom_webhook
|
||||
commands:
|
||||
- |
|
||||
if [ -n "${WEBHOOK_URL:-}" ]; then
|
||||
VERSION="${DRONE_TAG:-nightly-$(date +%Y%m%d)}"
|
||||
curl -sS -X POST "$WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"❌ 部署失败\nProject: ${DRONE_REPO}\n版本: ${VERSION}\n构建: ${DRONE_BUILD_LINK}\"}}"
|
||||
fi
|
||||
when:
|
||||
status: [failure]
|
||||
```
|
||||
|
||||
**替换占位符**:
|
||||
|
||||
| 占位符 | 替换为 | 示例 |
|
||||
|--------|--------|------|
|
||||
| `<service-1>`, `<service-2>` | 你的服务名 | `backend`, `frontend`, `api`, `web` |
|
||||
| `<service-1-context>` | Docker build context 路径 | `./backend`, `./frontend`, `.` |
|
||||
| `port: 22` | 生产服务器 SSH 端口 | `22`, `3141` |
|
||||
|
||||
---
|
||||
|
||||
## 第四步:编写部署脚本
|
||||
|
||||
在项目中创建 `scripts/deploy-remote.sh`,这个脚本由 Drone SSH 步骤在生产服务器上调用。
|
||||
|
||||
### 通用模板
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# ========================================
|
||||
# 远程部署脚本 - 被 Drone CI SSH 调用
|
||||
# ========================================
|
||||
# 用法: bash scripts/deploy-remote.sh [image_tag]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 日志 ----------
|
||||
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
|
||||
|
||||
# ---------- 配置(可通过环境变量覆盖) ----------
|
||||
DEPLOY_PATH="${DEPLOY_PATH:-/opt/myproject}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}"
|
||||
IMAGE_TAG="${1:-latest}"
|
||||
LOCK_FILE="/tmp/$(basename "$DEPLOY_PATH")-deploy.lock"
|
||||
MAX_ATTEMPTS="${MAX_ATTEMPTS:-30}"
|
||||
HEALTH_CHECK_INTERVAL="${HEALTH_CHECK_INTERVAL:-3}"
|
||||
|
||||
# ---------- 部署锁 ----------
|
||||
cleanup_lock() { rm -f "$LOCK_FILE"; }
|
||||
|
||||
acquire_lock() {
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
local old_pid
|
||||
old_pid=$(cat "$LOCK_FILE" 2>/dev/null || true)
|
||||
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
|
||||
log_error "已有部署进程运行中 (PID: $old_pid)"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$LOCK_FILE"
|
||||
fi
|
||||
echo "$$" > "$LOCK_FILE"
|
||||
trap cleanup_lock EXIT
|
||||
}
|
||||
|
||||
# ---------- 工具函数 ----------
|
||||
compose() {
|
||||
docker compose -f "$COMPOSE_FILE" "$@"
|
||||
}
|
||||
|
||||
# 健康检查 - 根据项目实际情况修改
|
||||
# 方式 1:容器内用 python 请求(适用于 Python 后端镜像)
|
||||
wait_healthy_python() {
|
||||
local service="$1"
|
||||
local url="${2:-http://127.0.0.1:8000/health}"
|
||||
local attempt=1
|
||||
|
||||
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
||||
if compose exec -T "$service" python -c \
|
||||
"import sys,urllib.request;urllib.request.urlopen('${url}', timeout=3);sys.exit(0)" \
|
||||
>/dev/null 2>&1; then
|
||||
log_info "${service} 健康检查通过"
|
||||
return 0
|
||||
fi
|
||||
log_info "等待 ${service} 就绪... (${attempt}/${MAX_ATTEMPTS})"
|
||||
sleep "$HEALTH_CHECK_INTERVAL"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# 方式 2:容器内用 curl 请求(适用于内置 curl 的镜像)
|
||||
wait_healthy_curl() {
|
||||
local service="$1"
|
||||
local url="${2:-http://127.0.0.1:8000/health}"
|
||||
local attempt=1
|
||||
|
||||
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
||||
if compose exec -T "$service" curl -sf "$url" >/dev/null 2>&1; then
|
||||
log_info "${service} 健康检查通过"
|
||||
return 0
|
||||
fi
|
||||
log_info "等待 ${service} 就绪... (${attempt}/${MAX_ATTEMPTS})"
|
||||
sleep "$HEALTH_CHECK_INTERVAL"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# 方式 3:从宿主机请求(适用于有端口映射的服务)
|
||||
wait_healthy_host() {
|
||||
local url="${1:-http://127.0.0.1:8000/health}"
|
||||
local attempt=1
|
||||
|
||||
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
||||
if curl -sf "$url" >/dev/null 2>&1; then
|
||||
log_info "健康检查通过: $url"
|
||||
return 0
|
||||
fi
|
||||
log_info "等待服务就绪... (${attempt}/${MAX_ATTEMPTS})"
|
||||
sleep "$HEALTH_CHECK_INTERVAL"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
main() {
|
||||
acquire_lock
|
||||
|
||||
[ -d "$DEPLOY_PATH" ] || { log_error "部署目录不存在: $DEPLOY_PATH"; exit 1; }
|
||||
cd "$DEPLOY_PATH"
|
||||
[ -f "$COMPOSE_FILE" ] || { log_error "Compose 文件不存在: $COMPOSE_FILE"; exit 1; }
|
||||
|
||||
# 导出镜像 tag 变量 - 根据 compose 文件中使用的变量名按需调整
|
||||
export IMAGE_TAG
|
||||
export VERSION="$IMAGE_TAG"
|
||||
# export TAG="$IMAGE_TAG" # 如果你的 compose 用 ${TAG}
|
||||
|
||||
log_info "========== 开始部署 =========="
|
||||
log_info "镜像版本: $IMAGE_TAG"
|
||||
log_info "部署目录: $DEPLOY_PATH"
|
||||
log_info "Compose: $COMPOSE_FILE"
|
||||
|
||||
# --- 1. 拉取新镜像 ---
|
||||
log_info "[1/5] 拉取新镜像..."
|
||||
compose pull
|
||||
# 或者只拉取特定服务: compose pull backend frontend
|
||||
|
||||
# --- 2. 滚动更新服务 ---
|
||||
log_info "[2/5] 更新服务..."
|
||||
compose up -d --remove-orphans
|
||||
# 如果需要控制更新顺序,拆分为多步:
|
||||
# compose up -d --no-deps backend
|
||||
# compose up -d --no-deps frontend
|
||||
|
||||
# --- 3. 健康检查 ---
|
||||
log_info "[3/5] 健康检查..."
|
||||
# 根据你的项目选择合适的健康检查方式,以下是示例:
|
||||
# wait_healthy_python "backend" "http://127.0.0.1:8000/health"
|
||||
# wait_healthy_curl "api" "http://127.0.0.1:3000/health"
|
||||
# wait_healthy_host "http://127.0.0.1:8080/health"
|
||||
#
|
||||
# 如果健康检查失败,取消注释下面的代码:
|
||||
# if ! wait_healthy_python "backend"; then
|
||||
# log_error "健康检查失败"
|
||||
# compose logs --tail=200 backend || true
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# --- 4. 数据库迁移(如有) ---
|
||||
log_info "[4/5] 数据库迁移..."
|
||||
# 根据项目使用的迁移工具选择:
|
||||
# compose exec -T backend alembic upgrade head # Python Alembic
|
||||
# compose exec -T api npx prisma migrate deploy # Node Prisma
|
||||
# compose exec -T api npm run migrate # 自定义脚本
|
||||
# compose exec -T backend python manage.py migrate # Django
|
||||
# 如果不需要迁移,注释掉即可
|
||||
|
||||
# --- 5. 清理 ---
|
||||
log_info "[5/5] 清理旧镜像..."
|
||||
docker image prune -f >/dev/null 2>&1 || log_warn "镜像清理失败,已跳过"
|
||||
|
||||
log_info "========== 部署完成 =========="
|
||||
log_info "版本: $IMAGE_TAG"
|
||||
compose ps
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
### 脚本要点
|
||||
|
||||
1. **部署锁**:通过 PID 文件防止并发部署
|
||||
2. **`set -euo pipefail`**:任何命令失败立即退出,防止"假成功"
|
||||
3. **变量导出**:同时 export `IMAGE_TAG` 和 `VERSION`,兼容不同 compose 文件的命名习惯
|
||||
4. **健康检查**:提供三种方式,根据项目镜像内置的工具选择
|
||||
5. **数据库迁移**:在健康检查通过后执行,迁移失败会中断部署
|
||||
|
||||
---
|
||||
|
||||
## 第五步:生产服务器配置
|
||||
|
||||
### 5.1 目录结构
|
||||
|
||||
确保生产服务器上部署目录结构如下:
|
||||
|
||||
```text
|
||||
/opt/myproject/ # DEPLOY_PATH
|
||||
├── docker-compose.prod.yml # 生产 compose 配置
|
||||
├── .env # 环境变量(镜像地址、数据库密码等)
|
||||
└── scripts/
|
||||
└── deploy-remote.sh # 部署脚本(从代码仓库复制)
|
||||
```
|
||||
|
||||
### 5.2 docker-compose.prod.yml 中的镜像引用
|
||||
|
||||
compose 文件中使用变量引用 Registry 中的镜像:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
image: ${DOCKER_REGISTRY}/myproject-backend:${VERSION:-latest}
|
||||
# ...
|
||||
|
||||
frontend:
|
||||
image: ${DOCKER_REGISTRY}/myproject-frontend:${VERSION:-latest}
|
||||
# ...
|
||||
```
|
||||
|
||||
### 5.3 .env 文件
|
||||
|
||||
```bash
|
||||
# 镜像仓库地址 - 必须与 Drone Secret 中的 image_repo 前缀一致
|
||||
DOCKER_REGISTRY=ci.example.com:5000
|
||||
|
||||
# 其他生产环境配置
|
||||
POSTGRES_PASSWORD=xxx
|
||||
REDIS_PASSWORD=xxx
|
||||
# ...
|
||||
```
|
||||
|
||||
> **变量一致性**:假设 Drone Secret `backend_repo` = `ci.example.com:5000/myproject-backend`,那么 `.env` 中的 `DOCKER_REGISTRY` 必须是 `ci.example.com:5000`,compose 中的镜像名必须是 `${DOCKER_REGISTRY}/myproject-backend:${VERSION:-latest}`。三者拼起来的镜像全名必须完全一致。
|
||||
|
||||
### 5.4 首次部署
|
||||
|
||||
首次部署需要手动初始化:
|
||||
|
||||
```bash
|
||||
cd /opt/myproject
|
||||
|
||||
# 确认 .env 和 compose 文件就位
|
||||
ls -la .env docker-compose.prod.yml
|
||||
|
||||
# 手动拉取并启动
|
||||
export VERSION=latest
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 运行数据库迁移(如有)
|
||||
docker compose -f docker-compose.prod.yml exec -T backend alembic upgrade head
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第六步:配置 Drone 仓库设置
|
||||
|
||||
### Secrets 快速清单
|
||||
|
||||
针对你的新项目,在 Drone 面板创建以下 Secrets:
|
||||
|
||||
```text
|
||||
backend_repo = <registry-host>:5000/<project>-backend
|
||||
frontend_repo = <registry-host>:5000/<project>-frontend # 如果有
|
||||
deploy_host = <production-server-ip>
|
||||
deploy_user = <ssh-username>
|
||||
deploy_ssh_key = <SSH 私钥完整内容,cat ~/.ssh/drone_deploy>
|
||||
deploy_path = /opt/<project>
|
||||
wecom_webhook = <通知 webhook url> # 可选
|
||||
```
|
||||
|
||||
> `deploy_ssh_key` 是第一步中生成的 SSH 私钥内容,所有项目可以共用同一个密钥(只要目标生产服务器相同)。
|
||||
|
||||
### Cron 配置
|
||||
|
||||
如果需要定时构建:
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|------|
|
||||
| Name | `nightly-build` |
|
||||
| Branch | `main` |
|
||||
| Schedule | `0 16 * * *`(北京时间 00:00) |
|
||||
|
||||
---
|
||||
|
||||
## 第七步:验证
|
||||
|
||||
### 7.1 Registry 验证
|
||||
|
||||
```bash
|
||||
# 在 Drone CI 服务器上
|
||||
curl http://localhost:5000/v2/_catalog
|
||||
```
|
||||
|
||||
### 7.2 Tag 触发构建
|
||||
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
在 Drone 面板观察 pipeline 执行状态。
|
||||
|
||||
### 7.3 生产服务验证
|
||||
|
||||
```bash
|
||||
# 检查容器状态
|
||||
ssh -p <port> <user>@<prod-ip> "cd /opt/myproject && docker compose -f docker-compose.prod.yml ps"
|
||||
|
||||
# 检查镜像版本
|
||||
ssh -p <port> <user>@<prod-ip> "docker images | grep myproject"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 回滚方案
|
||||
|
||||
### 方式 1:手动指定版本回滚
|
||||
|
||||
```bash
|
||||
ssh -p <port> <user>@<prod-ip>
|
||||
cd /opt/myproject
|
||||
bash scripts/deploy-remote.sh v1.0.0 # 指定要回滚到的版本
|
||||
```
|
||||
|
||||
### 方式 2:直接 compose 回滚
|
||||
|
||||
```bash
|
||||
ssh -p <port> <user>@<prod-ip>
|
||||
cd /opt/myproject
|
||||
export VERSION=v1.0.0
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### 方式 3:通过 Git Tag 触发
|
||||
|
||||
```bash
|
||||
git tag v1.0.0-rollback v1.0.0
|
||||
git push origin v1.0.0-rollback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 多项目管理
|
||||
|
||||
当多个项目共用同一套 Drone CI + Registry 基础设施时:
|
||||
|
||||
### 共享部分(不需要重复)
|
||||
|
||||
- Drone CI Server + Runner(已部署)
|
||||
- Docker Registry(已运行在 :5000)
|
||||
- SSH 密钥(可共用同一个 `drone_deploy` 密钥)
|
||||
- `insecure-registries` 配置(已在两台服务器上配置)
|
||||
|
||||
### 每个新项目需要做的
|
||||
|
||||
1. 在 Drone 面板激活仓库 + 开启 Trusted
|
||||
2. 在 Drone 面板添加该仓库的 Secrets(镜像地址、部署路径)
|
||||
3. 项目代码中添加 `.drone.yml` 和 `scripts/deploy-remote.sh`
|
||||
4. 生产服务器创建部署目录 + 放置 compose 文件和 `.env`
|
||||
5. (可选)配置 Cron 定时构建
|
||||
|
||||
### Registry 镜像命名规范
|
||||
|
||||
建议统一命名格式:
|
||||
|
||||
```text
|
||||
<registry-host>:5000/<project-name>-<service>:<tag>
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
ci.example.com:5000/douyin-backend:v1.0.0
|
||||
ci.example.com:5000/douyin-frontend:v1.0.0
|
||||
ci.example.com:5000/crm-api:v2.1.0
|
||||
ci.example.com:5000/crm-web:v2.1.0
|
||||
ci.example.com:5000/blog-app:latest
|
||||
```
|
||||
|
||||
查看所有仓库:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/v2/_catalog
|
||||
```
|
||||
|
||||
查看某个镜像的所有 tag:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/v2/<project>-<service>/tags/list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 踩坑清单
|
||||
|
||||
以下是实际部署中遇到的问题,按出现频率排序:
|
||||
|
||||
### 1. `insecure-registries` 格式错误
|
||||
|
||||
**错误**:Docker push 报 `server gave HTTP response to HTTPS client`
|
||||
|
||||
**原因**:`/etc/docker/daemon.json` 中写了 `http://host:5000`
|
||||
|
||||
**正确写法**:直接写 `host:5000`,不带协议前缀
|
||||
|
||||
### 2. Drone 变量替换冲突
|
||||
|
||||
**错误**:构建命令中的 shell 变量值为空
|
||||
|
||||
**原因**:Drone 在执行命令前会对 `${VAR}` 做自身的变量替换,自定义 shell 变量 `${TAG}` 被 Drone 替换为空
|
||||
|
||||
**解决**:直接使用 Drone 内置变量 `${DRONE_TAG:-latest}`,不要赋值给中间变量
|
||||
|
||||
### 3. Secret 注入不生效
|
||||
|
||||
**错误**:环境变量为空,步骤因 secret 缺失而失败
|
||||
|
||||
**原因**:使用了步骤级 `secrets:` 字段
|
||||
|
||||
**解决**:改用 `environment: { VAR: { from_secret: name } }`
|
||||
|
||||
### 4. plugins/docker DinD 启动失败
|
||||
|
||||
**错误**:`Unable to reach Docker Daemon after 15 attempts`
|
||||
|
||||
**原因**:`plugins/docker` 内部启动 Docker 守护进程失败(cgroup / 存储驱动兼容性)
|
||||
|
||||
**解决**:使用 `docker:27-cli` + 挂载宿主机 `/var/run/docker.sock`
|
||||
|
||||
### 5. Trusted 选项不可见
|
||||
|
||||
**错误**:仓库 Settings 中找不到 Trusted 选项
|
||||
|
||||
**原因**:`DRONE_USER_CREATE` 的 `username` 填了邮箱,不是 Gitea 用户名
|
||||
|
||||
**解决**:`username` 必须与 Gitea 登录用户名完全一致
|
||||
|
||||
### 6. Tag 触发与 Cron 触发互相排斥
|
||||
|
||||
**错误**:推送 Tag 后 Drone 不触发构建
|
||||
|
||||
**原因**:`.drone.yml` 同时配了 `event: [tag, cron]` 和 `cron: [nightly-build]`,Drone 触发条件是 **AND** 运算
|
||||
|
||||
**解决**:只保留 `event: [tag, cron]`,不加 `cron:` 过滤
|
||||
|
||||
### 7. 生产服务器镜像名不匹配
|
||||
|
||||
**错误**:`docker compose pull` 报 invalid reference 或拉取到错误镜像
|
||||
|
||||
**原因**:Drone Secret 中的 Registry 地址(如 IP)与生产服务器 `.env` 中的(如域名)不一致
|
||||
|
||||
**解决**:统一使用同一个地址格式(建议统一用域名或统一用 IP)
|
||||
|
||||
### 8. SSH 端口不对
|
||||
|
||||
**错误**:deploy 步骤超时或连接拒绝
|
||||
|
||||
**原因**:`appleboy/drone-ssh` 默认使用 22 端口
|
||||
|
||||
**解决**:在 `.drone.yml` 的 deploy settings 中显式指定 `port: <actual-port>`
|
||||
|
||||
### 9. Docker 权限不足
|
||||
|
||||
**错误**:生产服务器 `permission denied while trying to connect to the Docker daemon socket`
|
||||
|
||||
**解决**:`sudo usermod -aG docker <user>` 后重新登录
|
||||
|
||||
### 10. daemon.json 被覆盖
|
||||
|
||||
**错误**:修改 `insecure-registries` 时丢失了已有的 `registry-mirrors` 配置
|
||||
|
||||
**预防**:修改前先 `cat /etc/docker/daemon.json` 查看现有内容,合并修改
|
||||
|
||||
---
|
||||
|
||||
## 故障排查速查表
|
||||
|
||||
| 现象 | 检查方向 |
|
||||
|------|---------|
|
||||
| Pipeline 不触发 | Gitea Webhook 是否勾选"创建"事件;`.drone.yml` trigger 配置 |
|
||||
| Step 一直 pending | Runner 是否连通 Server;仓库是否 Trusted |
|
||||
| 构建报 secret 为空 | 使用 `environment: from_secret` 而非 `secrets:` |
|
||||
| Docker build 失败 | Dockerfile 是否正确;build context 路径是否对 |
|
||||
| Docker push 失败 (HTTPS) | 两台服务器 `insecure-registries` 配置 |
|
||||
| Docker push 失败 (连接拒绝) | Registry 容器是否运行:`docker ps \| grep registry` |
|
||||
| SSH 部署失败 | 密钥是否正确;端口是否匹配;用户是否有 Docker 权限 |
|
||||
| 找不到部署脚本 | `deploy-remote.sh` 是否已复制到生产服务器的对应目录 |
|
||||
| 镜像名 invalid reference | 生产服务器 `.env` 的 `DOCKER_REGISTRY` 变量是否正确 |
|
||||
| compose pull 拉到旧镜像 | 检查 Registry 地址和镜像 tag 是否一致 |
|
||||
| 数据库迁移失败 | `docker compose logs -f <service>` 查看报错 |
|
||||
| 健康检查超时 | 增大 `MAX_ATTEMPTS`;检查服务是否正常启动 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:Drone CI 部署
|
||||
|
||||
如果 Drone CI 尚未部署,在 Drone CI 服务器上创建 `~/drone/docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
drone-server:
|
||||
image: drone/drone:2
|
||||
container_name: drone-server
|
||||
restart: always
|
||||
ports:
|
||||
- "3080:80" # Drone Web UI 端口,通过反向代理暴露 HTTPS
|
||||
environment:
|
||||
- DRONE_GITEA_SERVER=https://<your-gitea-domain>
|
||||
- DRONE_GITEA_CLIENT_ID=<gitea-oauth-app-client-id>
|
||||
- DRONE_GITEA_CLIENT_SECRET=<gitea-oauth-app-client-secret>
|
||||
- DRONE_SERVER_HOST=<your-drone-domain>
|
||||
- DRONE_SERVER_PROTO=https
|
||||
- DRONE_RPC_SECRET=<随机生成的长字符串>
|
||||
- DRONE_USER_CREATE=username:<your-gitea-username>,admin:true
|
||||
volumes:
|
||||
- ./data:/data
|
||||
|
||||
drone-runner:
|
||||
image: drone/drone-runner-docker:1
|
||||
container_name: drone-runner
|
||||
restart: always
|
||||
depends_on:
|
||||
- drone-server
|
||||
environment:
|
||||
# Runner 通过 Docker 内网直连 server 的 80 端口
|
||||
- DRONE_RPC_PROTO=http
|
||||
- DRONE_RPC_HOST=drone-server
|
||||
- DRONE_RPC_SECRET=<与 server 相同的 secret>
|
||||
- DRONE_RUNNER_CAPACITY=2
|
||||
- DRONE_RUNNER_NAME=drone-runner-1
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
```
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
cd ~/drone
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Gitea OAuth App 配置
|
||||
|
||||
1. Gitea → 站点管理 → 应用 → 创建 OAuth2 应用
|
||||
2. 重定向 URI:`https://<your-drone-domain>/login`
|
||||
3. 将 Client ID 和 Client Secret 填入上面的 compose 配置
|
||||
|
||||
### 生成 RPC Secret
|
||||
|
||||
```bash
|
||||
openssl rand -hex 16
|
||||
```
|
||||
|
||||
### 反向代理配置(Nginx 示例)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name drone.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:新项目接入 Checklist
|
||||
|
||||
为新项目接入 CI/CD 时,按此清单逐项完成:
|
||||
|
||||
```text
|
||||
□ 基础设施(一次性,已完成则跳过)
|
||||
□ Registry 运行中
|
||||
□ insecure-registries 已配置(Drone CI 服务器 + 生产服务器)
|
||||
□ SSH 密钥已配置
|
||||
|
||||
□ Drone 面板
|
||||
□ 仓库已激活(ACTIVATE)
|
||||
□ Trusted 已勾选
|
||||
□ Secrets 已添加(image_repo, deploy_host, deploy_user, deploy_ssh_key, deploy_path)
|
||||
□ Cron Job 已配置(如需定时构建)
|
||||
|
||||
□ 项目代码
|
||||
□ .drone.yml 已创建
|
||||
□ scripts/deploy-remote.sh 已创建
|
||||
|
||||
□ 生产服务器
|
||||
□ 部署目录已创建
|
||||
□ docker-compose.prod.yml 已放置
|
||||
□ .env 已配置(DOCKER_REGISTRY 等)
|
||||
□ scripts/deploy-remote.sh 已复制到部署目录
|
||||
□ 首次手动部署成功
|
||||
|
||||
□ 验证
|
||||
□ 推送 Tag 后 Drone 自动构建并部署成功
|
||||
□ 生产服务正常运行
|
||||
□ 回滚流程测试通过
|
||||
```
|
||||
216
install.sh
216
install.sh
@ -1,216 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# spec-coding-skills 安装/更新脚本
|
||||
# 用法: bash <(curl -sL <raw-url>/install.sh) [codex|claude|both]
|
||||
# 或: bash install.sh [codex|claude|both|目标目录]
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://git.internal.intelligrow.cn/zhangfucai/spec-coding-skills.git"
|
||||
DEFAULT_MODE="codex"
|
||||
|
||||
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_title() { echo -e "${CYAN}$1${NC}"; }
|
||||
|
||||
MODE=""
|
||||
TARGET=""
|
||||
SKILLS_SRC=""
|
||||
GUIDE_SRC=""
|
||||
GUIDE_DST=""
|
||||
REQUEST="${1:-$DEFAULT_MODE}"
|
||||
TOTAL_NEW=0
|
||||
TOTAL_UPDATED=0
|
||||
TOTAL_SKIPPED=0
|
||||
INSTALLED_TARGETS=""
|
||||
|
||||
resolve_layout() {
|
||||
local input="$1"
|
||||
|
||||
case "$input" in
|
||||
""|codex)
|
||||
MODE="codex"
|
||||
TARGET=".codex/skills"
|
||||
SKILLS_SRC=".codex/skills"
|
||||
GUIDE_SRC="AGENTS.md.template"
|
||||
GUIDE_DST="AGENTS.md"
|
||||
;;
|
||||
claude)
|
||||
MODE="claude"
|
||||
TARGET=".claude/skills"
|
||||
SKILLS_SRC=".claude/skills"
|
||||
GUIDE_SRC="CLAUDE.md.template"
|
||||
GUIDE_DST="CLAUDE.md"
|
||||
;;
|
||||
both)
|
||||
log_warn "both 不是单一布局,请在主流程中单独处理"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
TARGET="$input"
|
||||
case "$TARGET" in
|
||||
*".claude/skills"*)
|
||||
MODE="claude"
|
||||
SKILLS_SRC=".claude/skills"
|
||||
GUIDE_SRC="CLAUDE.md.template"
|
||||
GUIDE_DST="CLAUDE.md"
|
||||
;;
|
||||
*)
|
||||
MODE="codex"
|
||||
SKILLS_SRC=".codex/skills"
|
||||
GUIDE_SRC="AGENTS.md.template"
|
||||
GUIDE_DST="AGENTS.md"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TMP_DIR" EXIT
|
||||
|
||||
sync_file() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
local create_msg="$3"
|
||||
local update_msg="$4"
|
||||
|
||||
if [ ! -f "$dst" ]; then
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
cp "$src" "$dst"
|
||||
log_info "$create_msg"
|
||||
TOTAL_NEW=$((TOTAL_NEW + 1))
|
||||
elif ! diff -q "$src" "$dst" >/dev/null 2>&1; then
|
||||
cp "$dst" "$dst.local.bak"
|
||||
cp "$src" "$dst"
|
||||
log_warn "$update_msg"
|
||||
TOTAL_UPDATED=$((TOTAL_UPDATED + 1))
|
||||
else
|
||||
TOTAL_SKIPPED=$((TOTAL_SKIPPED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
sync_guide_file() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
local create_msg="$3"
|
||||
local skip_msg="$4"
|
||||
|
||||
if [ ! -f "$dst" ]; then
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
cp "$src" "$dst"
|
||||
log_info "$create_msg"
|
||||
TOTAL_NEW=$((TOTAL_NEW + 1))
|
||||
else
|
||||
log_info "$skip_msg"
|
||||
TOTAL_SKIPPED=$((TOTAL_SKIPPED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
install_layout() {
|
||||
local input="$1"
|
||||
local skill_dir
|
||||
local skill_name
|
||||
local src_file
|
||||
local rel_path
|
||||
local dst_dir
|
||||
local dst_file
|
||||
local tpl_file
|
||||
local tpl_name
|
||||
local dst
|
||||
local guide_src_path
|
||||
|
||||
resolve_layout "$input"
|
||||
|
||||
if [ ! -d "$TMP_DIR/$SKILLS_SRC" ]; then
|
||||
log_warn "上游仓库中不存在技能目录: $SKILLS_SRC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "同步 $MODE: $TARGET"
|
||||
mkdir -p "$TARGET"
|
||||
|
||||
for skill_dir in "$TMP_DIR/$SKILLS_SRC"/*/; do
|
||||
[ -d "$skill_dir" ] || continue
|
||||
skill_name=$(basename "$skill_dir")
|
||||
dst_dir="$TARGET/$skill_name"
|
||||
|
||||
[ -f "$skill_dir/SKILL.md" ] || continue
|
||||
|
||||
while IFS= read -r -d '' src_file; do
|
||||
rel_path="${src_file#$skill_dir}"
|
||||
dst_file="$dst_dir/$rel_path"
|
||||
|
||||
sync_file \
|
||||
"$src_file" \
|
||||
"$dst_file" \
|
||||
"✨ 新增: $skill_name/$rel_path ($MODE)" \
|
||||
"🔄 更新: $skill_name/$rel_path ($MODE) (本地版本已备份为 $(basename "$rel_path").local.bak)"
|
||||
done < <(find "$skill_dir" -type f -print0)
|
||||
done
|
||||
|
||||
for tpl_file in "$TMP_DIR/$SKILLS_SRC"/*.template "$TMP_DIR/$SKILLS_SRC"/*.md; do
|
||||
[ -f "$tpl_file" ] || continue
|
||||
tpl_name=$(basename "$tpl_file")
|
||||
dst="$TARGET/$tpl_name"
|
||||
|
||||
sync_file \
|
||||
"$tpl_file" \
|
||||
"$dst" \
|
||||
"✨ 新增模板: $tpl_name ($MODE)" \
|
||||
"🔄 更新模板: $tpl_name ($MODE) (本地版本已备份为 ${tpl_name}.local.bak)"
|
||||
done
|
||||
|
||||
guide_src_path="$TMP_DIR/$GUIDE_SRC"
|
||||
if [ -f "$guide_src_path" ]; then
|
||||
sync_guide_file \
|
||||
"$guide_src_path" \
|
||||
"$GUIDE_DST" \
|
||||
"✨ 新增项目引导: ${GUIDE_DST}" \
|
||||
"⏭️ 跳过项目引导: ${GUIDE_DST}(已存在,保持原样)"
|
||||
fi
|
||||
|
||||
INSTALLED_TARGETS="${INSTALLED_TARGETS}${INSTALLED_TARGETS:+, }$TARGET"
|
||||
}
|
||||
|
||||
# ---------- 拉取最新 ----------
|
||||
log_title "📦 spec-coding-skills 安装/更新"
|
||||
echo ""
|
||||
log_info "拉取最新版本..."
|
||||
git clone --depth 1 --quiet "$REPO_URL" "$TMP_DIR"
|
||||
|
||||
VERSION=$(git -C "$TMP_DIR" describe --tags --always 2>/dev/null || git -C "$TMP_DIR" rev-parse --short HEAD)
|
||||
log_info "版本: $VERSION"
|
||||
|
||||
case "$REQUEST" in
|
||||
both)
|
||||
log_info "模式: both"
|
||||
install_layout codex
|
||||
install_layout claude
|
||||
;;
|
||||
*)
|
||||
resolve_layout "$REQUEST"
|
||||
log_info "模式: $MODE"
|
||||
install_layout "$REQUEST"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ---------- 汇总 ----------
|
||||
echo ""
|
||||
log_title "✅ 完成!"
|
||||
echo ""
|
||||
echo " 🧭 模式: $REQUEST"
|
||||
echo " 📁 目标目录: $INSTALLED_TARGETS"
|
||||
echo " 📦 版本: $VERSION"
|
||||
echo ""
|
||||
echo " ✨ 新增: $TOTAL_NEW"
|
||||
echo " 🔄 更新: ${TOTAL_UPDATED}(本地版本已备份为 .local.bak)"
|
||||
echo " ⏭️ 无变化: $TOTAL_SKIPPED"
|
||||
echo ""
|
||||
|
||||
if [ "$TOTAL_UPDATED" -gt 0 ]; then
|
||||
log_warn "有 ${TOTAL_UPDATED} 个文件被更新,本地修改已备份为 .local.bak"
|
||||
log_warn "如需恢复本地版本: mv SKILL.md.local.bak SKILL.md"
|
||||
log_warn "如需对比差异: diff SKILL.md SKILL.md.local.bak"
|
||||
fi
|
||||
@ -1,164 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
INSTALL_SCRIPT="$REPO_ROOT/install.sh"
|
||||
TMP_ROOT=""
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_file_contains() {
|
||||
local file="$1"
|
||||
local expected="$2"
|
||||
grep -F "$expected" "$file" >/dev/null || fail "expected '$expected' in $file"
|
||||
}
|
||||
|
||||
assert_output_contains() {
|
||||
local output="$1"
|
||||
local expected="$2"
|
||||
[[ "$output" == *"$expected"* ]] || fail "expected output to contain '$expected'"
|
||||
}
|
||||
|
||||
assert_equals() {
|
||||
local actual="$1"
|
||||
local expected="$2"
|
||||
[[ "$actual" == "$expected" ]] || fail "expected '$expected', got '$actual'"
|
||||
}
|
||||
|
||||
make_upstream_repo() {
|
||||
local dir="$1"
|
||||
local version="$2"
|
||||
|
||||
mkdir -p "$dir/.codex/skills/demo" "$dir/.claude/skills/demo"
|
||||
|
||||
cat >"$dir/.codex/skills/demo/SKILL.md" <<EOF
|
||||
# demo codex $version
|
||||
EOF
|
||||
|
||||
cat >"$dir/.claude/skills/demo/SKILL.md" <<EOF
|
||||
# demo claude $version
|
||||
EOF
|
||||
|
||||
cat >"$dir/AGENTS.md.template" <<EOF
|
||||
# AGENTS template $version
|
||||
EOF
|
||||
|
||||
cat >"$dir/CLAUDE.md.template" <<EOF
|
||||
# CLAUDE template $version
|
||||
EOF
|
||||
|
||||
(
|
||||
cd "$dir"
|
||||
git init -q
|
||||
git config user.name "Test User"
|
||||
git config user.email "test@example.com"
|
||||
git add .
|
||||
git commit -qm "upstream $version"
|
||||
)
|
||||
}
|
||||
|
||||
make_git_wrapper() {
|
||||
local wrapper_dir="$1"
|
||||
local real_git="$2"
|
||||
|
||||
mkdir -p "$wrapper_dir"
|
||||
|
||||
cat >"$wrapper_dir/git" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "\${1:-}" == "clone" ]]; then
|
||||
dst="\${@: -1}"
|
||||
exec "$real_git" clone --quiet "\$FAKE_UPSTREAM_REPO" "\$dst"
|
||||
fi
|
||||
|
||||
exec "$real_git" "\$@"
|
||||
EOF
|
||||
|
||||
chmod +x "$wrapper_dir/git"
|
||||
}
|
||||
|
||||
run_install() {
|
||||
local target_dir="$1"
|
||||
local mode="$2"
|
||||
(
|
||||
cd "$target_dir"
|
||||
PATH="$GIT_WRAPPER_DIR:$PATH" FAKE_UPSTREAM_REPO="$UPSTREAM_REPO" bash "$INSTALL_SCRIPT" "$mode"
|
||||
)
|
||||
}
|
||||
|
||||
main() {
|
||||
local output
|
||||
local guide_before
|
||||
|
||||
TMP_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_ROOT"' EXIT
|
||||
|
||||
UPSTREAM_REPO="$TMP_ROOT/upstream"
|
||||
GIT_WRAPPER_DIR="$TMP_ROOT/bin"
|
||||
export UPSTREAM_REPO GIT_WRAPPER_DIR
|
||||
|
||||
make_upstream_repo "$UPSTREAM_REPO" "v1"
|
||||
make_git_wrapper "$GIT_WRAPPER_DIR" "$(command -v git)"
|
||||
|
||||
mkdir -p "$TMP_ROOT/project-codex"
|
||||
output="$(run_install "$TMP_ROOT/project-codex" codex)"
|
||||
assert_file_contains "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md" "demo codex v1"
|
||||
assert_file_contains "$TMP_ROOT/project-codex/AGENTS.md" "AGENTS template v1"
|
||||
|
||||
printf 'custom agents\n' >"$TMP_ROOT/project-codex/AGENTS.md"
|
||||
guide_before="$(cat "$TMP_ROOT/project-codex/AGENTS.md")"
|
||||
(
|
||||
cd "$UPSTREAM_REPO"
|
||||
printf '# demo codex v2\n' > .codex/skills/demo/SKILL.md
|
||||
printf '# AGENTS template v2\n' > AGENTS.md.template
|
||||
git add .codex/skills/demo/SKILL.md AGENTS.md.template
|
||||
git commit -qm "upstream v2"
|
||||
)
|
||||
|
||||
output="$(run_install "$TMP_ROOT/project-codex" codex)"
|
||||
assert_equals "$(cat "$TMP_ROOT/project-codex/AGENTS.md")" "$guide_before"
|
||||
[[ ! -f "$TMP_ROOT/project-codex/AGENTS.md.local.bak" ]] || fail "AGENTS.md.local.bak should not exist"
|
||||
assert_output_contains "$output" "跳过项目引导"
|
||||
assert_file_contains "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md" "demo codex v2"
|
||||
[[ -f "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md.local.bak" ]] || fail "skill backup should exist"
|
||||
|
||||
mkdir -p "$TMP_ROOT/project-claude"
|
||||
output="$(run_install "$TMP_ROOT/project-claude" claude)"
|
||||
assert_file_contains "$TMP_ROOT/project-claude/.claude/skills/demo/SKILL.md" "demo claude v1"
|
||||
assert_file_contains "$TMP_ROOT/project-claude/CLAUDE.md" "CLAUDE template v1"
|
||||
|
||||
printf 'custom claude\n' >"$TMP_ROOT/project-claude/CLAUDE.md"
|
||||
(
|
||||
cd "$UPSTREAM_REPO"
|
||||
printf '# demo claude v2\n' > .claude/skills/demo/SKILL.md
|
||||
printf '# CLAUDE template v2\n' > CLAUDE.md.template
|
||||
git add .claude/skills/demo/SKILL.md CLAUDE.md.template
|
||||
git commit -qm "upstream claude v2"
|
||||
)
|
||||
|
||||
output="$(run_install "$TMP_ROOT/project-claude" claude)"
|
||||
assert_equals "$(cat "$TMP_ROOT/project-claude/CLAUDE.md")" "custom claude"
|
||||
[[ ! -f "$TMP_ROOT/project-claude/CLAUDE.md.local.bak" ]] || fail "CLAUDE.md.local.bak should not exist"
|
||||
assert_output_contains "$output" "跳过项目引导"
|
||||
assert_file_contains "$TMP_ROOT/project-claude/.claude/skills/demo/SKILL.md" "demo claude v2"
|
||||
|
||||
mkdir -p "$TMP_ROOT/project-both"
|
||||
printf 'team agents\n' >"$TMP_ROOT/project-both/AGENTS.md"
|
||||
printf 'team claude\n' >"$TMP_ROOT/project-both/CLAUDE.md"
|
||||
output="$(run_install "$TMP_ROOT/project-both" both)"
|
||||
assert_equals "$(cat "$TMP_ROOT/project-both/AGENTS.md")" "team agents"
|
||||
assert_equals "$(cat "$TMP_ROOT/project-both/CLAUDE.md")" "team claude"
|
||||
assert_file_contains "$TMP_ROOT/project-both/.codex/skills/demo/SKILL.md" "demo codex v2"
|
||||
assert_file_contains "$TMP_ROOT/project-both/.claude/skills/demo/SKILL.md" "demo claude v2"
|
||||
assert_output_contains "$output" "跳过项目引导: AGENTS.md"
|
||||
assert_output_contains "$output" "跳过项目引导: CLAUDE.md"
|
||||
|
||||
echo "PASS"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
x
Reference in New Issue
Block a user