Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb980704bf | |||
| a7bd6ff9d8 |
262
release.ps1
262
release.ps1
@@ -1,3 +1,44 @@
|
||||
# Vendored from jiangchang-platform-kit `tools/release.ps1`.
|
||||
# Source: https://git.jc2009.com/client-jiangchang/jiangchang-platform-kit/-/blob/main/tools/release.ps1
|
||||
# The PyPI wheel does not ship this script; sync periodically when upstream workflow changes.
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
One-command release script for skill repos.
|
||||
|
||||
.DESCRIPTION
|
||||
- Optional auto-commit
|
||||
- Push current branch
|
||||
- Auto-increment semantic tag (vX.Y.Z)
|
||||
- Create & push tag
|
||||
- Fail fast on unsafe states
|
||||
|
||||
.EXAMPLES
|
||||
# Safe mode (recommended): requires clean working tree
|
||||
.\release.ps1
|
||||
|
||||
# Auto commit tracked/untracked changes then release
|
||||
.\release.ps1 -AutoCommit -CommitMessage "chore: update skill config"
|
||||
|
||||
# Dry run (show what would happen)
|
||||
.\release.ps1 -DryRun
|
||||
|
||||
# Custom tag prefix
|
||||
.\release.ps1 -Prefix "v" -Message "正式发布"
|
||||
|
||||
.NOTES
|
||||
Requires: git, PowerShell 5+
|
||||
|
||||
加密与 ZIP 内容由 CI 工作流 reusable-release-skill.yaml 的「Encrypt Source Code」步骤执行。CI 会:
|
||||
- 加密 scripts/(递归 PyArmor -r,输出到包内 scripts/,与源码目录树一致)
|
||||
- 复制 SKILL.md
|
||||
- 复制 references/(排除 REQUIREMENTS.md)
|
||||
- 复制 assets/
|
||||
- 复制 tests/
|
||||
- 复制 evals/(除 scripts 外均为明文;若目录不存在则跳过;复制时排除常见缓存与运行产物)
|
||||
本脚本在打 tag 前会做一次 scripts/ 结构自检,避免子目录未提交却仍发布。
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Prefix = "v",
|
||||
@@ -11,13 +52,218 @@ param(
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$sharedScript = Join-Path $scriptDir "..\jiangchang-platform-kit\tools\release.ps1"
|
||||
$sharedScript = [System.IO.Path]::GetFullPath($sharedScript)
|
||||
|
||||
if (-not (Test-Path $sharedScript)) {
|
||||
throw "Shared release script not found: $sharedScript"
|
||||
function Invoke-Git {
|
||||
param([Parameter(Mandatory = $true)][string]$Args)
|
||||
Write-Host ">> git $Args" -ForegroundColor DarkGray
|
||||
# 将 git 的 stderr 合并进 stdout,避免 PowerShell 在 $ErrorActionPreference=Stop 下
|
||||
# 把 "remote: ..." / "warning: LF -> CRLF" 等非错误输出误判为异常。
|
||||
& cmd /c "git $Args 2>&1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git command failed: git $Args"
|
||||
}
|
||||
}
|
||||
|
||||
& $sharedScript @PSBoundParameters
|
||||
exit $LASTEXITCODE
|
||||
function Get-GitOutput {
|
||||
param([Parameter(Mandatory = $true)][string]$Args)
|
||||
$output = & cmd /c "git $Args" 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git command failed: git $Args"
|
||||
}
|
||||
return @($output)
|
||||
}
|
||||
|
||||
function Test-Repo {
|
||||
& git rev-parse --is-inside-work-tree *> $null
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
}
|
||||
|
||||
function Get-CurrentBranch {
|
||||
$b = (Get-GitOutput "branch --show-current" | Select-Object -First 1).Trim()
|
||||
return $b
|
||||
}
|
||||
|
||||
function Get-StatusPorcelain {
|
||||
$lines = @(Get-GitOutput "status --porcelain")
|
||||
return $lines
|
||||
}
|
||||
|
||||
function Parse-SemVerTag {
|
||||
param(
|
||||
[string]$Tag,
|
||||
[string]$TagPrefix
|
||||
)
|
||||
$escaped = [regex]::Escape($TagPrefix)
|
||||
$m = [regex]::Match($Tag, "^${escaped}(\d+)\.(\d+)\.(\d+)$")
|
||||
if (-not $m.Success) { return $null }
|
||||
|
||||
return [pscustomobject]@{
|
||||
Raw = $Tag
|
||||
Major = [int]$m.Groups[1].Value
|
||||
Minor = [int]$m.Groups[2].Value
|
||||
Patch = [int]$m.Groups[3].Value
|
||||
}
|
||||
}
|
||||
|
||||
function Get-NextTag {
|
||||
param([string]$TagPrefix)
|
||||
|
||||
$tags = Get-GitOutput "tag --list"
|
||||
$parsed = @()
|
||||
|
||||
foreach ($t in $tags) {
|
||||
$t = $t.Trim()
|
||||
if (-not $t) { continue }
|
||||
$obj = Parse-SemVerTag -Tag $t -TagPrefix $TagPrefix
|
||||
if ($null -ne $obj) { $parsed += $obj }
|
||||
}
|
||||
|
||||
if ($parsed.Count -eq 0) {
|
||||
return "${TagPrefix}1.0.1"
|
||||
}
|
||||
|
||||
$latest = $parsed | Sort-Object Major, Minor, Patch | Select-Object -Last 1
|
||||
return "$TagPrefix$($latest.Major).$($latest.Minor).$([int]$latest.Patch + 1)"
|
||||
}
|
||||
|
||||
function Assert-SkillReleasePackagingSources {
|
||||
param([Parameter(Mandatory = $true)][string]$SkillRoot)
|
||||
|
||||
$scriptsDir = Join-Path $SkillRoot "scripts"
|
||||
$mainPy = Join-Path $scriptsDir "main.py"
|
||||
if (-not (Test-Path -LiteralPath $mainPy)) {
|
||||
return
|
||||
}
|
||||
|
||||
$text = Get-Content -LiteralPath $mainPy -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||
if ([string]::IsNullOrWhiteSpace($text)) {
|
||||
return
|
||||
}
|
||||
|
||||
$need = @()
|
||||
if ($text -match '(?m)^\s*from\s+cli\.') { $need += 'cli' }
|
||||
if ($text -match '(?m)^\s*import\s+cli\b') { $need += 'cli' }
|
||||
if ($text -match '(?m)^\s*from\s+service\.') { $need += 'service' }
|
||||
if ($text -match '(?m)^\s*import\s+service\b') { $need += 'service' }
|
||||
if ($text -match '(?m)^\s*from\s+db\.') { $need += 'db' }
|
||||
if ($text -match '(?m)^\s*import\s+db\b') { $need += 'db' }
|
||||
if ($text -match '(?m)^\s*from\s+util\.') { $need += 'util' }
|
||||
if ($text -match '(?m)^\s*import\s+util\b') { $need += 'util' }
|
||||
|
||||
foreach ($p in ($need | Select-Object -Unique)) {
|
||||
$folder = Join-Path $scriptsDir $p
|
||||
if (-not (Test-Path -LiteralPath $folder)) {
|
||||
throw "Release check failed: scripts/main.py imports from '$p' but folder is missing: $folder"
|
||||
}
|
||||
}
|
||||
|
||||
$pyFiles = @(Get-ChildItem -LiteralPath $scriptsDir -Filter *.py -Recurse -File -ErrorAction SilentlyContinue)
|
||||
Write-Host "Packaging check: $($pyFiles.Count) Python file(s) under scripts/ (CI will obfuscate all recursively)." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
function Ensure-CleanOrAutoCommit {
|
||||
param(
|
||||
[switch]$DoAutoCommit,
|
||||
[switch]$NeedClean,
|
||||
[switch]$IsDryRun,
|
||||
[string]$Msg
|
||||
)
|
||||
|
||||
$status = @(Get-StatusPorcelain)
|
||||
if ($status.Length -eq 0) { return }
|
||||
|
||||
if ($NeedClean) {
|
||||
Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow
|
||||
& git status --short
|
||||
throw "Abort: dirty working tree."
|
||||
}
|
||||
|
||||
# 默认一键发布:有改动就自动提交;也可用 -AutoCommit 显式开启
|
||||
$commitMsg = $Msg
|
||||
if ([string]::IsNullOrWhiteSpace($commitMsg)) {
|
||||
$commitMsg = "chore: auto release commit ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))"
|
||||
}
|
||||
|
||||
if (-not $DoAutoCommit) {
|
||||
Write-Host "Detected uncommitted changes, auto-committing before release..." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($IsDryRun) {
|
||||
Write-Host "[DryRun] Would run: git add -A" -ForegroundColor Yellow
|
||||
Write-Host "[DryRun] Would run: git commit -m `"$commitMsg`"" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
Invoke-Git "add -A"
|
||||
Invoke-Git "commit -m `"$commitMsg`""
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "=== Release Script Start ===" -ForegroundColor Cyan
|
||||
|
||||
if (-not (Test-Repo)) {
|
||||
throw "Current directory is not a git repository."
|
||||
}
|
||||
|
||||
$branch = Get-CurrentBranch
|
||||
if ([string]::IsNullOrWhiteSpace($branch)) {
|
||||
throw "Unable to determine current branch."
|
||||
}
|
||||
|
||||
if ($branch -notin @("main", "master")) {
|
||||
throw "Current branch is '$branch'. Release is only allowed from main/master."
|
||||
}
|
||||
|
||||
Invoke-Git "fetch --tags --prune origin"
|
||||
|
||||
Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage
|
||||
|
||||
$skillRoot = (Get-GitOutput "rev-parse --show-toplevel" | Select-Object -First 1).Trim()
|
||||
if (Test-Path -LiteralPath (Join-Path $skillRoot "SKILL.md")) {
|
||||
Assert-SkillReleasePackagingSources -SkillRoot $skillRoot
|
||||
}
|
||||
|
||||
# 用 cmd /c + 2>&1 将 stderr 合并进 stdout,避免 PowerShell 在 $ErrorActionPreference=Stop
|
||||
# 下把首次发布时 "fatal: ambiguous argument '@{u}'" 之类的 stderr 误判为异常。
|
||||
$upstream = & cmd /c 'git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>&1'
|
||||
$hasUpstream = ($LASTEXITCODE -eq 0)
|
||||
|
||||
if ($DryRun) {
|
||||
if ($hasUpstream) {
|
||||
Write-Host "[DryRun] Would run: git push" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "[DryRun] Would run: git push -u origin $branch" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
if ($hasUpstream) {
|
||||
Invoke-Git "push"
|
||||
} else {
|
||||
Invoke-Git "push -u origin $branch"
|
||||
}
|
||||
}
|
||||
|
||||
$nextTag = Get-NextTag -TagPrefix $Prefix
|
||||
Write-Host "Next tag: $nextTag" -ForegroundColor Green
|
||||
|
||||
$existing = @(Get-GitOutput "tag --list `"$nextTag`"")
|
||||
if ($existing.Length -gt 0) {
|
||||
throw "Tag already exists: $nextTag"
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[DryRun] Would run: git tag -a $nextTag -m `"$Message`"" -ForegroundColor Yellow
|
||||
Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow
|
||||
Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan
|
||||
exit 0
|
||||
}
|
||||
|
||||
Invoke-Git "tag -a $nextTag -m `"$Message`""
|
||||
Invoke-Git "push origin $nextTag"
|
||||
|
||||
Write-Host "Release success: $nextTag" -ForegroundColor Green
|
||||
Write-Host "=== Release Script Done ===" -ForegroundColor Cyan
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Host "Release failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
7
requirements-platform-kit.txt
Normal file
7
requirements-platform-kit.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Optional: install shared SDK from Gitea Package Registry (matches vendored scripts/jiangchang_skill_core at release time).
|
||||
#
|
||||
# pip install -r requirements-platform-kit.txt ^
|
||||
# --index-url https://git.jc2009.com/api/packages/client-jiangchang/pypi/simple/ ^
|
||||
# --extra-index-url https://pypi.org/simple
|
||||
#
|
||||
jiangchang-platform-kit>=0.1.0
|
||||
@@ -3,7 +3,7 @@
|
||||
CLI entry point: argv dispatch for account-manager v2.
|
||||
|
||||
Subcommand tree:
|
||||
platform list/get
|
||||
platform list/get/ensure
|
||||
account add-web|add-secret|get|get-credential|list|pick-web|mark-session|mark-error|mark-used|open
|
||||
init-master-key|export-credentials|import-credentials
|
||||
credential pick|resolve
|
||||
@@ -42,6 +42,7 @@ from service.account_service import (
|
||||
cmd_list,
|
||||
cmd_list_json,
|
||||
cmd_pick_web,
|
||||
cmd_platform_ensure,
|
||||
cmd_platform_get,
|
||||
cmd_platform_list,
|
||||
)
|
||||
@@ -60,6 +61,8 @@ _USAGE = """account-manager v2 — Credential & Browser Profile Manager
|
||||
平台管理:
|
||||
platform list [--domain logistics|llm|content]
|
||||
platform get <platform>
|
||||
platform ensure --key <key> --display-name <name> [--domain <d>]
|
||||
[--url <url>] [--auth-strategy <s>]
|
||||
|
||||
账号管理:
|
||||
account add-web --platform <p> --login-id <id> --login-id-type <type>
|
||||
@@ -161,7 +164,7 @@ def main() -> None:
|
||||
# ---- platform subcommand ----
|
||||
if cmd == "platform":
|
||||
if len(av) < 3:
|
||||
print("ERROR:CLI_BAD_ARGS platform 需要子命令:list | get")
|
||||
print("ERROR:CLI_BAD_ARGS platform 需要子命令:list | get | ensure")
|
||||
sys.exit(1)
|
||||
sub = av[2]
|
||||
if sub == "list":
|
||||
@@ -172,8 +175,30 @@ def main() -> None:
|
||||
print("ERROR:CLI_BAD_ARGS platform get 需要 <platform>。")
|
||||
sys.exit(1)
|
||||
cmd_platform_get(av[3])
|
||||
elif sub == "ensure":
|
||||
import argparse as _ap
|
||||
p = _ap.ArgumentParser(prog="platform ensure")
|
||||
p.add_argument("--key", required=True)
|
||||
p.add_argument("--display-name", default="")
|
||||
p.add_argument("--domain", default="generic")
|
||||
p.add_argument("--url", default="")
|
||||
p.add_argument("--auth-strategy", default=None)
|
||||
p.add_argument("--aliases", default=None,
|
||||
help="JSON 数组字符串,如 '[\"1688\",\"阿里巴巴1688\"]'")
|
||||
p.add_argument("--capabilities", default=None,
|
||||
help='JSON 对象,如 \'{"web":true,"api_key":false,"rpa":true}\'')
|
||||
args = p.parse_args(av[3:])
|
||||
sys.exit(cmd_platform_ensure(
|
||||
key=args.key,
|
||||
display_name=args.display_name or args.key,
|
||||
domain=args.domain,
|
||||
url=args.url,
|
||||
auth_strategy=args.auth_strategy,
|
||||
aliases=args.aliases,
|
||||
capabilities=args.capabilities,
|
||||
))
|
||||
else:
|
||||
print("ERROR:CLI_BAD_ARGS platform 子命令必须是 list 或 get。")
|
||||
print("ERROR:CLI_BAD_ARGS platform 子命令必须是 list、get 或 ensure。")
|
||||
sys.exit(1)
|
||||
|
||||
# ---- account subcommand ----
|
||||
|
||||
@@ -763,6 +763,49 @@ def get_active_lease_by_token(conn, lease_token: str) -> Optional[dict[str, Any]
|
||||
}
|
||||
|
||||
|
||||
def upsert_platform(
|
||||
conn,
|
||||
key: str,
|
||||
display_name: str,
|
||||
domain: str = "generic",
|
||||
provider_code: str | None = None,
|
||||
default_url: str = "",
|
||||
aliases_json: str = "[]",
|
||||
capabilities_json: str = '{"web": true, "api_key": false, "rpa": true}',
|
||||
default_auth_strategy: str | None = None,
|
||||
) -> None:
|
||||
"""幂等地注册或更新平台到 DB。"""
|
||||
now = int(time.time())
|
||||
if provider_code is None:
|
||||
provider_code = key
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO platforms (
|
||||
platform_key, display_name, domain, provider_code,
|
||||
default_url, aliases_json, capabilities_json,
|
||||
enabled, default_auth_strategy, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
|
||||
ON CONFLICT(platform_key) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
domain = excluded.domain,
|
||||
provider_code = excluded.provider_code,
|
||||
default_url = excluded.default_url,
|
||||
aliases_json = excluded.aliases_json,
|
||||
capabilities_json = excluded.capabilities_json,
|
||||
enabled = 1,
|
||||
default_auth_strategy = COALESCE(excluded.default_auth_strategy,
|
||||
platforms.default_auth_strategy),
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
key, display_name, domain, provider_code,
|
||||
default_url, aliases_json, capabilities_json,
|
||||
default_auth_strategy, now, now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Internal helpers
|
||||
# ============================================================================
|
||||
|
||||
@@ -42,6 +42,7 @@ from db.accounts_repo import (
|
||||
update_account_last_used,
|
||||
update_account_session_status,
|
||||
update_credential_last_used,
|
||||
upsert_platform,
|
||||
)
|
||||
from db.auth_strategy import (
|
||||
ALL_AUTH_STRATEGIES,
|
||||
@@ -190,15 +191,49 @@ def _remove_profile_dir(path: str) -> None:
|
||||
|
||||
def _resolve_or_fail(input_name: str, hint: str = "") -> Optional[str]:
|
||||
"""Resolve platform key or print ERROR:INVALID_PLATFORM and return None."""
|
||||
key = resolve_platform_key((input_name or "").strip())
|
||||
if not key:
|
||||
raw = (input_name or "").strip()
|
||||
if not raw:
|
||||
_say("ERROR:INVALID_PLATFORM 平台名不能为空。")
|
||||
return None
|
||||
|
||||
# 先尝试 platforms.py 硬编码(兼容历史)
|
||||
key = resolve_platform_key(raw)
|
||||
if key:
|
||||
return key
|
||||
|
||||
# 再查 DB(支持动态注册的平台)
|
||||
try:
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT platform_key FROM platforms WHERE platform_key = ? AND enabled = 1",
|
||||
(raw,),
|
||||
).fetchone()
|
||||
if row:
|
||||
return row[0]
|
||||
rows = conn.execute(
|
||||
"SELECT platform_key, aliases_json FROM platforms WHERE enabled = 1"
|
||||
).fetchall()
|
||||
raw_lower = raw.lower()
|
||||
for r in rows:
|
||||
try:
|
||||
aliases = json.loads(r[1] or "[]")
|
||||
if raw_lower in [str(a).lower() for a in aliases]:
|
||||
return r[0]
|
||||
except Exception:
|
||||
continue
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_say(f"ERROR:INVALID_PLATFORM 无法识别的平台「{input_name}」。")
|
||||
if hint:
|
||||
_say(hint)
|
||||
else:
|
||||
_say("支持:" + _platform_list_cn_for_help())
|
||||
return None
|
||||
return key
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -238,6 +273,55 @@ def cmd_platform_get(input_key: str) -> None:
|
||||
_say(_err_json("ERROR:INVALID_PLATFORM", f"Platform '{key}' not found in DB."))
|
||||
|
||||
|
||||
def cmd_platform_ensure(
|
||||
key: str,
|
||||
display_name: str,
|
||||
domain: str = "generic",
|
||||
url: str = "",
|
||||
auth_strategy: Optional[str] = None,
|
||||
aliases: Optional[str] = None,
|
||||
capabilities: Optional[str] = None,
|
||||
) -> int:
|
||||
"""
|
||||
幂等地注册或更新平台到 DB。
|
||||
输出单行 JSON:{"ok": true, "platform_key": "xxx", "action": "created|updated"}
|
||||
"""
|
||||
log = get_skill_logger()
|
||||
key = (key or "").strip()
|
||||
display_name = (display_name or key).strip()
|
||||
if not key:
|
||||
_say('ERROR:INVALID_ARGS platform ensure 需要 --key 参数')
|
||||
return 1
|
||||
|
||||
aliases_json = aliases if aliases else json.dumps([key, display_name], ensure_ascii=False)
|
||||
capabilities_json = capabilities if capabilities else '{"web": true, "api_key": false, "rpa": true}'
|
||||
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
existed = bool(
|
||||
conn.execute(
|
||||
"SELECT 1 FROM platforms WHERE platform_key = ?", (key,)
|
||||
).fetchone()
|
||||
)
|
||||
upsert_platform(
|
||||
conn,
|
||||
key=key,
|
||||
display_name=display_name,
|
||||
domain=domain,
|
||||
default_url=url,
|
||||
aliases_json=aliases_json,
|
||||
capabilities_json=capabilities_json,
|
||||
default_auth_strategy=auth_strategy,
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
action = "updated" if existed else "created"
|
||||
log.info("platform_ensure key=%s action=%s", key, action)
|
||||
_say(json.dumps({"ok": True, "platform_key": key, "action": action}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Account add-web
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user