This commit is contained in:
leokun
2026-06-30 10:38:52 +08:00
commit c083be5ec2
312 changed files with 146628 additions and 0 deletions
+559
View File
@@ -0,0 +1,559 @@
version: "3"
includes:
common: ./build/Taskfile.yml
windows: ./build/windows/Taskfile.yml
darwin: ./build/darwin/Taskfile.yml
linux: ./build/linux/Taskfile.yml
vars:
APP_NAME: "Cursor助手"
BIN_DIR: "bin"
APP_VERSION:
sh: 'go run ./scripts/release version -config ./build/config.yml'
RELEASE_REPO: "leookun/cursor-byok"
RELEASE_BASE_NAME: "cursor-byok"
RELEASE_SOURCE_PATH: "release-notes.md"
RELEASE_DIR: '{{.BIN_DIR}}/release/{{.APP_VERSION}}'
RELEASE_NOTES_PATH: '{{.RELEASE_DIR}}/.release-notes.md'
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
SCAN: '{{.SCAN | default "false"}}'
CURRENT_WINDOWS_ARCH: '{{if eq ARCH "386"}}386{{else}}amd64{{end}}'
CURRENT_WINDOWS_NAME: '{{if eq .CURRENT_WINDOWS_ARCH "386"}}windows-32{{else}}windows-64{{end}}'
CURRENT_DARWIN_NAME: '{{if eq ARCH "arm64"}}macos-arm64{{else}}macos-intel{{end}}'
CURRENT_LINUX_ARCH: "amd64"
CURRENT_LINUX_NAME: "linux-amd64"
tasks:
build:
summary: 构建当前系统分发包
preconditions:
- sh: '[ "{{OS}}" = "darwin" ] || [ "{{OS}}" = "windows" ] || [ "{{OS}}" = "linux" ]'
msg: "仅支持在 macOS、Windows 或 Linux 上执行 task build"
cmds:
- task: clean:dist
- task: build:current
run:
summary: 运行当前系统版本
cmds:
- task: "{{OS}}:run"
build:all:
summary: 构建全部 macOS/Windows 分发包(Linux 需在原生 Linux 主机单独构建)
preconditions:
- sh: '[ "{{OS}}" = "darwin" ]'
msg: "task build:all 仅支持在 macOS 上执行;Linux 需在原生 Linux 主机构建。"
cmds:
- task: clean:dist
- task: build:darwin:arm64
- task: build:darwin:amd64
- task: build:windows:386
- task: build:windows:amd64
build:current:
internal: true
cmds:
- task: '{{if eq OS "darwin"}}build:darwin:current{{else if eq OS "linux"}}build:linux:current{{else}}build:windows:current{{end}}'
build:darwin:current:
internal: true
cmds:
- task: darwin:package:dmg
vars:
ARCH: '{{ARCH}}'
BINARY_NAME: '{{.CURRENT_DARWIN_NAME}}'
OUTPUT: '{{.BIN_DIR}}/{{.CURRENT_DARWIN_NAME}}'
APP_BUNDLE: '{{.CURRENT_DARWIN_NAME}}.app'
DMG_NAME: '{{.CURRENT_DARWIN_NAME}}.dmg'
SCAN: '{{.SCAN}}'
build:darwin:arm64:
internal: false
cmds:
- task: darwin:package:dmg
vars:
ARCH: arm64
BINARY_NAME: macos-arm64
OUTPUT: '{{.BIN_DIR}}/macos-arm64'
APP_BUNDLE: macos-arm64.app
DMG_NAME: macos-arm64.dmg
SCAN: '{{.SCAN}}'
build:darwin:amd64:
internal: true
cmds:
- task: darwin:package:dmg
vars:
ARCH: amd64
BINARY_NAME: macos-intel
OUTPUT: '{{.BIN_DIR}}/macos-intel'
APP_BUNDLE: macos-intel.app
DMG_NAME: macos-intel.dmg
SCAN: '{{.SCAN}}'
build:windows:current:
internal: true
cmds:
- task: windows:create:zip
vars:
ARCH: '{{.CURRENT_WINDOWS_ARCH}}'
OUTPUT: '{{.BIN_DIR}}/{{.CURRENT_WINDOWS_NAME}}.exe'
ZIP_NAME: '{{.CURRENT_WINDOWS_NAME}}.zip'
SCAN: '{{.SCAN}}'
build:windows:386:
internal: true
cmds:
- task: windows:create:zip
vars:
ARCH: 386
OUTPUT: '{{.BIN_DIR}}/windows-32.exe'
ZIP_NAME: windows-32.zip
SCAN: '{{.SCAN}}'
build:windows:amd64:
cmds:
- task: windows:create:zip
vars:
ARCH: amd64
OUTPUT: '{{.BIN_DIR}}/windows-64.exe'
ZIP_NAME: windows-64.zip
SCAN: '{{.SCAN}}'
build:linux:current:
internal: true
cmds:
- task: linux:package:archive
vars:
ARCH: '{{.CURRENT_LINUX_ARCH}}'
BINARY_NAME: '{{.CURRENT_LINUX_NAME}}'
OUTPUT: '{{.BIN_DIR}}/{{.CURRENT_LINUX_NAME}}'
ARCHIVE_BINARY_NAME: '{{.APP_NAME}}'
ARCHIVE_PATH: '{{.BIN_DIR}}/{{.CURRENT_LINUX_NAME}}.tar.gz'
SCAN: '{{.SCAN}}'
build:linux:amd64:
cmds:
- task: linux:package:archive
vars:
ARCH: amd64
BINARY_NAME: linux-amd64
OUTPUT: '{{.BIN_DIR}}/linux-amd64'
ARCHIVE_BINARY_NAME: '{{.APP_NAME}}'
ARCHIVE_PATH: '{{.BIN_DIR}}/linux-amd64.tar.gz'
SCAN: '{{.SCAN}}'
cursor-tab-server:docker:amd64:
summary: 构建 cursor-tab-server 的 linux/amd64 Docker 镜像并保存到 cursor-tab-server 目录
vars:
IMAGE_NAME: '{{.IMAGE_NAME | default "cursor-tab-server:linux-amd64"}}'
IMAGE_TAR: '{{.IMAGE_TAR | default "cursor-tab-server/cursor-tab-server-linux-amd64.tar"}}'
CONTEXT_DIR: cursor-tab-server
DOCKERFILE: cursor-tab-server/Dockerfile
preconditions:
- sh: docker info >/dev/null 2>&1
msg: "未检测到可用的 Docker daemon,请先启动 Docker。"
- sh: test -f "{{.DOCKERFILE}}"
msg: "未找到 cursor-tab-server/Dockerfile。"
- sh: test -f "{{.CONTEXT_DIR}}/go.mod"
msg: "未找到 cursor-tab-server/go.mod。"
cmds:
- mkdir -p "{{.CONTEXT_DIR}}"
- docker build --platform linux/amd64 -t "{{.IMAGE_NAME}}" -f "{{.DOCKERFILE}}" "{{.CONTEXT_DIR}}"
- docker save "{{.IMAGE_NAME}}" -o "{{.IMAGE_TAR}}"
- ls -lh "{{.IMAGE_TAR}}"
clean:dist:
internal: true
cmds:
- mkdir -p "{{.BIN_DIR}}"
- rm -f "{{.BIN_DIR}}/macos-arm64.dmg" "{{.BIN_DIR}}/macos-intel.dmg" "{{.BIN_DIR}}/windows-32.zip" "{{.BIN_DIR}}/windows-64.zip" "{{.BIN_DIR}}/linux-amd64.tar.gz"
- rm -f "{{.BIN_DIR}}/macos-arm64" "{{.BIN_DIR}}/macos-intel" "{{.BIN_DIR}}/windows-32.exe" "{{.BIN_DIR}}/windows-64.exe" "{{.BIN_DIR}}/linux-amd64"
- rm -f "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.exe"
- rm -rf "{{.BIN_DIR}}/macos-arm64.app" "{{.BIN_DIR}}/macos-intel.app" "{{.BIN_DIR}}/{{.APP_NAME}}.app" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
dev:
summary: 启动开发模式(前台,终端会保持占用)
cmds:
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
ads:install:
summary: 安装广告页依赖
dir: '{{.TASKFILE_DIR}}/ads-page'
sources:
- package.json
- yarn.lock
generates:
- node_modules
cmds:
- yarn install --frozen-lockfile
ads:dev:
summary: 启动广告页开发服务(http://127.0.0.1:5174/ad/
dir: '{{.TASKFILE_DIR}}/ads-page'
deps:
- task: ads:install
cmds:
- yarn dev
ads:build:
summary: 构建广告 ZIP 产物
dir: '{{.TASKFILE_DIR}}/ads-page'
deps:
- task: ads:install
cmds:
- yarn build
setup:docker:
summary: 构建 Linux 交叉编译 Docker 镜像
cmds:
- task: common:setup:docker
release:clean:
summary: 清理当前版本的发布目录
cmds:
- rm -rf "{{.RELEASE_DIR}}"
- mkdir -p "{{.RELEASE_DIR}}"
release:ensure:dir:
internal: true
cmds:
- mkdir -p "{{.RELEASE_DIR}}"
release:notes:
summary: 从 release-notes.md 生成当前版本的发布说明文件(README.md 仅用于公开仓库同步)
internal: true
preconditions:
- sh: test -f "{{.RELEASE_SOURCE_PATH}}"
msg: "未找到 release-notes.md,请先创建发布日志文件。"
- sh: test -n "$(tr -d '[:space:]' < "{{.RELEASE_SOURCE_PATH}}")"
msg: "release-notes.md 不能为空,请先填写发布日志。"
cmds:
- go run ./scripts/release notes -config ./build/config.yml -out "{{.RELEASE_NOTES_PATH}}" -source "{{.RELEASE_SOURCE_PATH}}"
release:build:macos:arm64:
cmds:
- task: darwin:package:archive
vars:
ARCH: arm64
BINARY_NAME: cursor-release-macos-arm64
OUTPUT: '{{.BIN_DIR}}/cursor-release-macos-arm64'
APP_BUNDLE: '{{.APP_NAME}}.app'
ARCHIVE_PATH: '{{.RELEASE_DIR}}/{{.RELEASE_BASE_NAME}}-{{.APP_VERSION}}-macos-arm64.tar.gz'
SCAN: '{{.SCAN}}'
release:build:macos:amd64:
internal: true
cmds:
- task: darwin:package:archive
vars:
ARCH: amd64
BINARY_NAME: cursor-release-macos-amd64
OUTPUT: '{{.BIN_DIR}}/cursor-release-macos-amd64'
APP_BUNDLE: '{{.APP_NAME}}.app'
ARCHIVE_PATH: '{{.RELEASE_DIR}}/{{.RELEASE_BASE_NAME}}-{{.APP_VERSION}}-macos-amd64.tar.gz'
SCAN: '{{.SCAN}}'
release:build:windows:amd64:
cmds:
- task: windows:create:zip
vars:
ARCH: amd64
OUTPUT: '{{.BIN_DIR}}/{{.RELEASE_BASE_NAME}}-windows-amd64.exe'
ZIP_NAME: 'release/{{.APP_VERSION}}/{{.RELEASE_BASE_NAME}}-{{.APP_VERSION}}-windows-amd64.zip'
SCAN: '{{.SCAN}}'
release:build:linux:amd64:
cmds:
- task: linux:package:archive
vars:
ARCH: amd64
BINARY_NAME: cursor-release-linux-amd64
OUTPUT: '{{.BIN_DIR}}/cursor-release-linux-amd64'
ARCHIVE_BINARY_NAME: '{{.APP_NAME}}'
ARCHIVE_PATH: '{{.RELEASE_DIR}}/{{.RELEASE_BASE_NAME}}-{{.APP_VERSION}}-linux-amd64.tar.gz'
SCAN: '{{.SCAN}}'
release:manifest:
summary: 生成 update.json
internal: true
cmds:
- go run ./scripts/release manifest -config ./build/config.yml -assets-dir "{{.RELEASE_DIR}}" -out "{{.RELEASE_DIR}}/update.json" -repo "{{.RELEASE_REPO}}" -base-name "{{.RELEASE_BASE_NAME}}" -notes "{{.RELEASE_NOTES_PATH}}"
release:prepare:
summary: 在当前主机上生成对应的发布资产
preconditions:
- sh: '[ "{{OS}}" = "darwin" ] || [ "{{OS}}" = "linux" ]'
msg: "release:prepare 仅支持在 macOS 或 Linux 上执行。"
- sh: wails3 version >/dev/null 2>&1
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
cmds:
- task: '{{if eq OS "darwin"}}release:prepare:darwin{{else}}release:prepare:linux{{end}}'
release:prepare:darwin:
summary: 在 macOS 上生成 macOS/Windows/Linux 发布资产
preconditions:
- sh: '[ "{{OS}}" = "darwin" ]'
msg: "release:prepare:darwin 仅支持在 macOS 上执行。"
- sh: wails3 version >/dev/null 2>&1
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
cmds:
- task: release:clean
- task: common:update:build-assets
- task: release:notes
- task: release:build:macos:arm64
- task: release:build:macos:amd64
- task: release:build:windows:amd64
- task: release:build:linux:amd64
release:prepare:linux:
summary: 在当前主机上生成 Linux 发布资产
preconditions:
- sh: '[ "{{OS}}" = "linux" ] || [ "{{OS}}" = "darwin" ]'
msg: "release:prepare:linux 当前仅支持在 Linux 或 macOS 上执行。"
- sh: wails3 version >/dev/null 2>&1
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
cmds:
- task: release:ensure:dir
- task: common:update:build-assets
- task: release:notes
- task: release:build:linux:amd64
release:verify:assets:
summary: 验证当前版本发布资产是否齐全
preconditions:
- sh: test -f "{{.RELEASE_NOTES_PATH}}"
msg: "未找到发布说明,请先执行对应的 release:prepare 任务。"
- sh: test -f "{{.RELEASE_DIR}}/{{.RELEASE_BASE_NAME}}-{{.APP_VERSION}}-macos-arm64.tar.gz"
msg: "缺少 macOS arm64 发布资产。"
- sh: test -f "{{.RELEASE_DIR}}/{{.RELEASE_BASE_NAME}}-{{.APP_VERSION}}-macos-amd64.tar.gz"
msg: "缺少 macOS amd64 发布资产。"
- sh: test -f "{{.RELEASE_DIR}}/{{.RELEASE_BASE_NAME}}-{{.APP_VERSION}}-windows-amd64.zip"
msg: "缺少 Windows amd64 发布资产。"
- sh: test -f "{{.RELEASE_DIR}}/{{.RELEASE_BASE_NAME}}-{{.APP_VERSION}}-linux-amd64.tar.gz"
msg: "缺少 Linux amd64 发布资产。"
cmds:
- echo "release assets verified"
release:sync:readme:
summary: 同步 README 到公开发布仓库(发布日志来源固定为 release-notes.md
internal: true
env:
GH_PAGER: cat
GIT_PAGER: cat
PAGER: cat
preconditions:
- sh: gh --version >/dev/null 2>&1
msg: "未检测到 gh,请先安装 GitHub CLI。"
- sh: gh auth token >/dev/null 2>&1
msg: "gh 未登录,请先执行 gh auth login。"
cmds:
- |
python3 - <<'PY'
import base64
import json
import pathlib
import subprocess
import sys
import time
RETRYABLE_TOKENS = ("EOF", "timeout", "TLS", "temporarily unavailable", "connection reset", "connection refused")
def run_gh(cmd, *, input_text=None, allow_404=False, retries=4):
last = None
for attempt in range(1, retries + 1):
completed = subprocess.run(
cmd,
input=input_text,
text=True,
capture_output=True,
)
if completed.returncode == 0:
return completed
stderr = (completed.stderr or "").strip()
stdout = (completed.stdout or "").strip()
combined = f"{stderr}\n{stdout}".strip()
if allow_404 and "404" in combined:
return completed
last = completed
if not any(token.lower() in combined.lower() for token in RETRYABLE_TOKENS):
return completed
if attempt < retries:
time.sleep(min(2 ** (attempt - 1), 5))
return last
repo = "{{.RELEASE_REPO}}"
readme_path = pathlib.Path("README.md")
content = readme_path.read_bytes()
encoded = base64.b64encode(content).decode("ascii")
get_cmd = [
"gh", "api",
f"repos/{repo}/contents/README.md",
]
result = run_gh(get_cmd, allow_404=True)
payload = {
"message": "docs: sync README from source repo",
"content": encoded,
"branch": "main",
}
if result.returncode == 0:
existing = json.loads(result.stdout)
existing_content = base64.b64decode(existing["content"])
if existing_content == content:
sys.exit(0)
payload["sha"] = existing["sha"]
put_cmd = [
"gh", "api",
f"repos/{repo}/contents/README.md",
"--method", "PUT",
"--input", "-",
]
completed = run_gh(put_cmd, input_text=json.dumps(payload))
if completed.returncode != 0 and completed.stderr:
sys.stderr.write(completed.stderr)
sys.exit(completed.returncode)
PY
release:github:
summary: 发布当前版本到 GitHub Releases
env:
GH_PAGER: cat
GIT_PAGER: cat
PAGER: cat
preconditions:
- sh: gh --version >/dev/null 2>&1
msg: "未检测到 gh,请先安装 GitHub CLI。"
- sh: gh auth token >/dev/null 2>&1
msg: "gh 未登录,请先执行 gh auth login。"
cmds:
- task: release:verify:assets
- task: release:manifest
- task: release:sync:readme
- |
python3 - <<'PY'
import mimetypes
import json
import pathlib
import subprocess
import sys
import time
RETRYABLE_TOKENS = ("EOF", "timeout", "TLS", "temporarily unavailable", "connection reset", "connection refused")
def run_gh(cmd, *, input_text=None, allow_404=False, retries=4):
last = None
for attempt in range(1, retries + 1):
completed = subprocess.run(
cmd,
input=input_text,
text=True,
capture_output=True,
)
if completed.returncode == 0:
return completed
stderr = (completed.stderr or "").strip()
stdout = (completed.stdout or "").strip()
combined = f"{stderr}\n{stdout}".strip()
if allow_404 and "404" in combined:
return completed
last = completed
if not any(token.lower() in combined.lower() for token in RETRYABLE_TOKENS):
return completed
if attempt < retries:
time.sleep(min(2 ** (attempt - 1), 5))
return last
repo = "{{.RELEASE_REPO}}"
version = "{{.APP_VERSION}}"
tag = f"v{version}"
notes = pathlib.Path("{{.RELEASE_NOTES_PATH}}").read_text(encoding="utf-8")
assets = sorted([
pathlib.Path("{{.RELEASE_DIR}}/update.json"),
*pathlib.Path("{{.RELEASE_DIR}}").glob("*.tar.gz"),
*pathlib.Path("{{.RELEASE_DIR}}").glob("*.zip"),
])
get_cmd = [
"gh", "api",
f"repos/{repo}/releases/tags/{tag}",
]
result = run_gh(get_cmd, allow_404=True)
payload = {
"tag_name": tag,
"target_commitish": "main",
"name": tag,
"body": notes,
"draft": False,
"prerelease": False,
}
if result.returncode == 0:
release = json.loads(result.stdout)
release_id = release["id"]
edit_cmd = [
"gh", "api",
f"repos/{repo}/releases/{release_id}",
"--method", "PATCH",
"--input", "-",
]
completed = run_gh(edit_cmd, input_text=json.dumps(payload))
if completed.returncode != 0:
if completed.stderr:
sys.stderr.write(completed.stderr)
sys.exit(completed.returncode)
refreshed = run_gh(get_cmd)
if refreshed.returncode != 0:
if refreshed.stderr:
sys.stderr.write(refreshed.stderr)
sys.exit(refreshed.returncode)
release = json.loads(refreshed.stdout)
else:
create_cmd = [
"gh", "api",
f"repos/{repo}/releases",
"--method", "POST",
"--input", "-",
]
completed = run_gh(create_cmd, input_text=json.dumps(payload))
if completed.returncode != 0:
sys.stderr.write(completed.stderr)
sys.exit(completed.returncode)
release = json.loads(completed.stdout)
upload_url = release["upload_url"].split("{", 1)[0]
existing = {asset["name"]: asset["id"] for asset in release.get("assets", [])}
for asset in assets:
name = asset.name
if name in existing:
delete_cmd = [
"gh", "api",
f"repos/{repo}/releases/assets/{existing[name]}",
"--method", "DELETE",
]
deleted = run_gh(delete_cmd)
if deleted.returncode != 0:
if deleted.stderr:
sys.stderr.write(deleted.stderr)
sys.exit(deleted.returncode)
content_type = mimetypes.guess_type(name)[0] or "application/octet-stream"
upload_cmd = [
"gh", "api",
f"{upload_url}?name={name}",
"--method", "POST",
"--header", f"Content-Type: {content_type}",
"--input", str(asset),
]
uploaded = run_gh(upload_cmd)
if uploaded.returncode != 0:
if uploaded.stderr:
sys.stderr.write(uploaded.stderr)
sys.exit(uploaded.returncode)
PY