更新skill

This commit is contained in:
huangshuni
2026-05-14 17:25:44 +08:00
parent 8f866944ee
commit ac15c5afc3
8 changed files with 930 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
---
name: update-sdk
description: |
更新 jpush-phonegap-plugin 插件的 JPush SDK 版本。自动拉取极光官网 Changelog,分析新增/移除/变更 API,更新 plugin.xml 中 AndroidGradle framework)和 iOSCocoaPods podspec)版本引用,同步更新 Native 层(Java/ObjC)和 JS Bridge 层代码,展示变更摘要确认后发布到 npm。
Use when: 更新 JPush SDK、升级推送 SDK 版本、jpush-phonegap-plugin 发布新版本、Cordova/PhoneGap 插件 SDK 更新。
allowed-tools:
- Bash
- Read
- Edit
- Write
- WebFetch
---
你正在更新 **jpush-phonegap-plugin** 插件。
**用户参数:** $ARGUMENTS
---
## 第一步:解析参数
`$ARGUMENTS` 中提取版本号:
- `--android X.X.X` → Android JPush SDK 目标版本
- `--ios X.X.X` → iOS JPush SDK 目标版本
如果某端版本号缺失,先询问用户再继续。
---
## 第二步:安装依赖
```bash
pip3 install requests beautifulsoup4 -q 2>&1 | tail -1
```
---
## 第三步:拉取 SDK Changelog
```bash
python3 .claude/skills/update-sdk/scripts/changelog_fetcher.py --android <ANDROID_VERSION> --ios <IOS_VERSION>
```
读取 `.claude/skills/update-sdk/scripts/.changelog_cache.json` 获取 Changelog 内容。
---
## 第四步:AI 分析变更
分析 Changelog,整理:
> **注意**Changelog 同时包含 JPush 和 JCore 的变更。只关注调用类以 `JPush` 开头的条目,忽略类名以 `JCore` 开头的内容。
1. 新增 APIAndroid + iOS 相同功能合并为统一插件 API);仅单端有的 → **先检查另一端是否已有等价实现**(见下方说明),确认缺失才标注单端
2. 移除/废弃 API
3. 行为变更
4. 新插件版本号:始终升 patch(如 3.4.9 → 3.5.03.9.9 → 4.0.0
> **跨平台等价检查**:当 Changelog 只在某一端出现新增 API 时,**不要直接标为单端 Only**。先读取另一端的 Native 文件(`src/android/JPushPlugin.java` 或 `src/ios/Plugins/JPushPlugin.m`)和 JS Bridge`www/JPushPlugin.js`),搜索功能相同或名称相近的方法。如果另一端已有对应实现,则合并为统一 API;只有确认另一端完全没有等价功能时,才标注 Android Only / iOS Only。
---
## 第五步:更新版本号引用
plugin.xml 中同时包含 SDK 版本和插件版本:
```bash
python3 .claude/skills/update-sdk/scripts/plugin_updater.py \
--android <ANDROID_VERSION> \
--ios <IOS_VERSION> \
--bump-patch \
--changelog-summary "<ONE_LINE_SUMMARY>"
```
---
## 第六步:更新 Native 层代码
**编写代码前,先通过 WebFetch 查询官网 API 文档,确认新增方法的完整签名、参数类型和返回值:**
- Android 文档:`https://docs.jiguang.cn/jpush/client/Android/android_api`
- iOS 文档:`https://docs.jiguang.cn/jpush/client/iOS/ios_api`
在文档中搜索第四步识别出的新增方法名,确认签名后再编写下方代码。
**Android**`src/android/JPushPlugin.java`
-`execute()` 方法中添加新的 action 分支处理新 API
- 内部调用 `JPushInterface.newMethod()`
**iOS**`src/ios/Plugins/JPushPlugin.m`
- 在对应的 action 处理中添加新方法
- 内部调用 JPush iOS SDK 对应方法
---
## 第七步:更新 JavaScript Bridge
**`www/JPushPlugin.js`**
- 添加新方法的 `cordova.exec()` 封装(统一 Android 和 iOS 入口)
---
## 第八步:展示变更摘要并请求确认
```
========== jpush-phonegap-plugin 更新摘要 ==========
Android JPush SDK: 旧版本 → 新版本
iOS JPush SDK: 旧版本 → 新版本
插件版本(plugin.xml):旧版本 → 新版本
新增 API...
修改的文件:plugin.xml, src/android/JPushPlugin.java, src/ios/Plugins/JPushPlugin.m, www/JPushPlugin.js, CHANGELOG.md
====================================================
确认以上变更并发布到 npm? [y/N]
```
---
## 第九步:发布(确认后执行)
```bash
python3 .claude/skills/update-sdk/scripts/publisher.py
```
@@ -0,0 +1,4 @@
{
"android": "## JPush Android SDK v6.1.0\n### 更新时间\n\n+ 2026-04-27\n\n### Change Log\n\n+ 新增接口[setKeepLongConnInBackground](https://docs.jiguang.cn/jpush/client/Android/android_api#%E9%80%80%E5%90%8E%E5%8F%B0%E6%98%AF%E5%90%A6%E4%BF%9D%E6%8C%81%E6%9E%81%E5%85%89%E9%95%BF%E8%BF%9E%E6%8E%A5-api)\n + 应用退至后台是否保持极光长连接的配置接口\n+ 新增支持厂商 VOIP 能力支持\n + [JPushMessageReceiver 新增 onVoipMessage 回调](https://docs.jiguang.cn/jpush/client/Android/android_api#jpushmessagereceiver-%E5%9B%9E%E8%B0%83%E7%B1%BB)\n+ 更新厂商推送 SDK,各厂商推送 SDK 版本如下\n + 华为推送 SDK V6.13.0.301\n + 荣耀推送 SDK V10.0.13.305\n + 小米推送 SDK V7.9.2\n + OPPO 推送 SDK V3.9.8\n + VIVO 推送 SDK V4.1.3.0\n + 特别注意,JPush Android SDK v5.2.0及其以上版本,请关注 vivo 通道集成指南 更新。\n + 魅族推送 SDK V5.0.5\n + 特别注意,JPush Android SDK v5.2.2及其以上版本,请关注 魅族 通道集成指南 更新。\n + FCM 推送 SDK V24.1.2\n + 蔚来 SDK 从此次版本开始不再支持,具体参考[蔚来通道 Push 自动切换至极光通道通知](https://community.jiguang.cn/notice/232)\n+ 修复已知问题\n\n### 升级提示\n\n+ 必须配合 JCore Android SDK v5.4.0 及以上版本使用\n+ JPush Android SDK v5.9.0 及其以上版本,各厂商 mavenCentral 集成方式优化\n + 依赖包会自动集成,无需再手动添加jar和aar,具体参考极光[厂商通道集成指南](https://docs.jiguang.cn/jpush/client/Android/android_3rd_guide)\n+ JPush Android SDK v5.2.0 及其以上版本升级到 JPush Android SDK v5.4.0 及其以上版本时,需要将消息回调方式从 JPushMessageService 改为 JPushMessageReceiver\n + 详情请查看极光文档:[Android SDK 集成指南](https://docs.jiguang.cn/jpush/client/Android/android_guide)",
"ios": "## JPush iOS SDK v6.1.0\n### 更新时间\n+ 2026-04-27\n\n### Change Log\n+ 修复已知问题\n> 注意:JPush iOS SDK 4.9.0 及以上版本需要使用 Xcode 14 及以上版本,请注意升级!\n\n### 升级提示\n+ 建议配合 JCore iOS SDK v5.4.0 及以上版本使用\n+ 升级到 JPush iOS SDK 4.9.0 及以上版本需删除 armv7 和 armv7s 架构"
}
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Fetch SDK changelog from JiGuang official docs for a specific version."""
import sys
import json
import re
import argparse
try:
import requests
except ImportError:
print("ERROR: Missing dependencies. Run: pip3 install requests")
sys.exit(1)
CONFIG_PATH = ".claude/skills/update-sdk/scripts/config.json"
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def fetch_changelog(url: str, target_version: str) -> str:
"""Fetch and extract changelog section for target_version from a Markdown URL."""
md_url = url if url.endswith(".md") else url + ".md"
try:
resp = requests.get(md_url, headers=HEADERS, timeout=30)
resp.raise_for_status()
content = resp.text
except requests.RequestException as e:
return f"ERROR: Failed to fetch {md_url}: {e}"
lines = content.split("\n")
in_section = False
section_lines = []
for line in lines:
# Match headings like: ## JPush Android SDK v6.1.0
if re.match(r'^##\s+.*v?' + re.escape(target_version) + r'\b', line):
in_section = True
section_lines.append(line)
elif in_section:
if re.match(r'^##\s+', line):
break # next version section starts
section_lines.append(line)
if not section_lines:
return f"VERSION_NOT_FOUND: Could not find changelog for version {target_version} at {md_url}"
return "\n".join(section_lines).strip()
def main():
parser = argparse.ArgumentParser(description="Fetch JiGuang SDK changelog")
parser.add_argument("--android", help="Android SDK version")
parser.add_argument("--ios", help="iOS SDK version")
parser.add_argument("--harmony", help="HarmonyOS SDK version")
args = parser.parse_args()
config = load_config()
urls = config.get("changelog_urls", {})
result = {}
platforms = [
("android", args.android),
("ios", args.ios),
("harmony", args.harmony),
]
for platform, version in platforms:
if version and platform in urls:
print(f"\n=== Fetching {platform} changelog for v{version} ===")
content = fetch_changelog(urls[platform], version)
result[platform] = content
print(content)
cache_path = ".claude/skills/update-sdk/scripts/.changelog_cache.json"
with open(cache_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"\n=== Changelog saved to {cache_path} ===")
if __name__ == "__main__":
main()
@@ -0,0 +1,60 @@
{
"plugin_name": "jpush-phonegap-plugin",
"sdk_type": "jpush",
"platform": "cordova",
"changelog_urls": {
"android": "https://docs.jiguang.cn/jpush/jpush_changelog/updates_Android",
"ios": "https://docs.jiguang.cn/jpush/jpush_changelog/updates_iOS"
},
"version_refs": {
"android_sdk": {
"integration": "cordova-framework",
"files": [
{
"path": "plugin.xml",
"pattern": "<framework src=\"cn.jiguang.sdk:jpush:{VERSION}\""
}
]
},
"ios_sdk": {
"integration": "cocoapods-podspec",
"files": [
{
"path": "plugin.xml",
"pattern": "<pod name=\"JPush\" spec=\"{VERSION}\""
}
]
},
"plugin": {
"files": [
{
"path": "package.json",
"key": "version"
},
{
"path": "plugin.xml",
"key": "version",
"pattern": "id=\"jpush-phonegap-plugin\"\n version=\"{VERSION}\""
}
]
}
},
"native_files": {
"android": [
"src/android/JPushPlugin.java"
],
"ios": [
"src/ios/Plugins/JPushPlugin.h",
"src/ios/Plugins/JPushPlugin.m"
]
},
"bridge_files": {
"js": [
"www/JPushPlugin.js"
]
},
"changelog_file": "CHANGELOG.md",
"publish": {
"type": "npm"
}
}
@@ -0,0 +1,277 @@
#!/usr/bin/env python3
"""Update SDK version references in plugin files based on config.json."""
import sys
import json
import re
import argparse
from pathlib import Path
from datetime import datetime
CONFIG_PATH = ".claude/skills/update-sdk/scripts/config.json"
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def bump_patch(version: str) -> str:
"""Bump patch version. patch and minor cap at 9; carry over to next component at 10."""
parts = version.split(".")
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
patch += 1
if patch >= 10:
patch = 0
minor += 1
if minor >= 10:
minor = 0
major += 1
return f"{major}.{minor}.{patch}"
def get_current_version_from_ref(ref: dict) -> str | None:
"""Read the current version string from a file ref."""
path = Path(ref["path"])
if not path.exists():
return None
content = path.read_text(encoding="utf-8")
if "pattern" in ref:
regex = re.escape(ref["pattern"]).replace(r"\{VERSION\}", r"([\d.]+(?:-\w+)?)")
match = re.search(regex, content)
return match.group(1) if match else None
elif "key" in ref:
if path.suffix in (".yaml", ".yml"):
match = re.search(r"^version:\s*(.+)$", content, re.MULTILINE)
return match.group(1).strip() if match else None
elif path.suffix == ".xml":
key = ref["key"]
match = re.search(rf'{re.escape(key)}="([^"]+)"', content)
return match.group(1) if match else None
else: # JSON
try:
data = json.loads(content)
keys = ref["key"].split(".")
obj = data
for k in keys:
obj = obj[k]
return str(obj)
except (KeyError, json.JSONDecodeError):
return None
return None
def update_by_pattern(file_path: str, pattern: str, new_version: str) -> bool:
"""Replace version in file using a pattern with {VERSION} placeholder."""
path = Path(file_path)
if not path.exists():
print(f" WARNING: File not found: {file_path}")
return False
content = path.read_text(encoding="utf-8")
regex = re.escape(pattern).replace(r"\{VERSION\}", r"([\d.]+(?:-\w+)?)")
replacement = pattern.replace("{VERSION}", new_version)
match = re.search(regex, content)
if not match:
print(f" WARNING: Pattern not found in {file_path}")
print(f" Pattern: {pattern}")
return False
old_version = match.group(1)
if old_version == new_version:
print(f" SKIP: {file_path} already at {new_version}")
return True
new_content = content.replace(match.group(0), replacement, 1)
path.write_text(new_content, encoding="utf-8")
print(f" UPDATED: {file_path} {old_version}{new_version}")
return True
def update_json_field(file_path: str, key: str, new_version: str) -> bool:
"""Update a field in a JSON file (supports dot-notation keys)."""
path = Path(file_path)
if not path.exists():
print(f" WARNING: File not found: {file_path}")
return False
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
keys = key.split(".")
obj = data
for k in keys[:-1]:
obj = obj.setdefault(k, {})
old = obj.get(keys[-1], "unknown")
if old == new_version:
print(f" SKIP: {file_path} [{key}] already at {new_version}")
return True
obj[keys[-1]] = new_version
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
print(f" UPDATED: {file_path} [{key}] {old}{new_version}")
return True
def update_yaml_version(file_path: str, new_version: str) -> bool:
"""Update 'version:' line in a YAML file."""
path = Path(file_path)
if not path.exists():
print(f" WARNING: File not found: {file_path}")
return False
content = path.read_text(encoding="utf-8")
match = re.search(r"^(version:\s*)(.+)$", content, re.MULTILINE)
if not match:
print(f" WARNING: No 'version:' field found in {file_path}")
return False
old_version = match.group(2).strip()
if old_version == new_version:
print(f" SKIP: {file_path} already at {new_version}")
return True
new_content = content.replace(match.group(0), f"{match.group(1)}{new_version}", 1)
path.write_text(new_content, encoding="utf-8")
print(f" UPDATED: {file_path} {old_version}{new_version}")
return True
def update_xml_attribute(file_path: str, attribute: str, new_version: str) -> bool:
"""Update a version attribute in an XML file (e.g., plugin.xml version="X.X.X")."""
path = Path(file_path)
if not path.exists():
print(f" WARNING: File not found: {file_path}")
return False
content = path.read_text(encoding="utf-8")
pattern = rf'({re.escape(attribute)}=")([^"]+)(")'
match = re.search(pattern, content)
if not match:
print(f" WARNING: Attribute '{attribute}' not found in {file_path}")
return False
old_version = match.group(2)
if old_version == new_version:
print(f" SKIP: {file_path} [{attribute}] already at {new_version}")
return True
new_content = content.replace(match.group(0), f'{match.group(1)}{new_version}{match.group(3)}', 1)
path.write_text(new_content, encoding="utf-8")
print(f" UPDATED: {file_path} [{attribute}] {old_version}{new_version}")
return True
def update_config_json_dep(file_path: str, dep_prefix: str, new_version: str) -> bool:
"""Update a maven dependency version in a UTS config.json dependencies array."""
path = Path(file_path)
if not path.exists():
print(f" WARNING: File not found: {file_path}")
return False
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
deps = data.get("dependencies", [])
updated = False
for i, dep in enumerate(deps):
if isinstance(dep, str) and dep.startswith(dep_prefix):
old = dep
parts = dep.rsplit(":", 1)
deps[i] = f"{parts[0]}:{new_version}"
print(f" UPDATED: {file_path} dep {old}{deps[i]}")
updated = True
if not updated:
print(f" WARNING: Dependency prefix '{dep_prefix}' not found in {file_path}")
return False
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent="\t", ensure_ascii=False)
f.write("\n")
return True
def update_changelog(changelog_file: str, plugin_version: str, summary: str):
path = Path(changelog_file)
date_str = datetime.now().strftime("%Y-%m-%d")
entry = f"## {plugin_version} ({date_str})\n\n{summary}\n\n"
existing = path.read_text(encoding="utf-8") if path.exists() else "# Changelog\n\n"
path.write_text(entry + existing, encoding="utf-8")
print(f" UPDATED: {changelog_file} → added v{plugin_version} entry")
def process_file_ref(ref: dict, version: str) -> bool:
path = ref["path"]
if "pattern" in ref:
return update_by_pattern(path, ref["pattern"], version)
elif "key" in ref:
if path.endswith(".yaml") or path.endswith(".yml"):
return update_yaml_version(path, version)
elif path.endswith(".xml"):
return update_xml_attribute(path, ref["key"], version)
else:
return update_json_field(path, ref["key"], version)
elif "dep_prefix" in ref:
return update_config_json_dep(path, ref["dep_prefix"], version)
return False
def main():
parser = argparse.ArgumentParser(description="Update SDK version references")
parser.add_argument("--android", help="Android SDK version")
parser.add_argument("--ios", help="iOS SDK version")
parser.add_argument("--plugin-version", help="Explicit new plugin version")
parser.add_argument("--bump-patch", action="store_true",
help="Auto-bump patch (carry at 10: x.y.9→x.(y+1).0, x.9.9→(x+1).0.0)")
parser.add_argument("--changelog-summary", default="", help="Summary for CHANGELOG.md")
args = parser.parse_args()
config = load_config()
refs = config.get("version_refs", {})
print("\n=== Updating version references ===")
if args.android and "android_sdk" in refs:
print(f"\n[Android SDK → {args.android}]")
for ref in refs["android_sdk"].get("files", []):
process_file_ref(ref, args.android)
if args.ios and "ios_sdk" in refs:
print(f"\n[iOS SDK → {args.ios}]")
for ref in refs["ios_sdk"].get("files", []):
process_file_ref(ref, args.ios)
# Resolve plugin version
plugin_version = args.plugin_version
if args.bump_patch and not plugin_version and "plugin" in refs:
for ref in refs["plugin"].get("files", []):
current = get_current_version_from_ref(ref)
if current:
plugin_version = bump_patch(current)
print(f"\n[Auto bump-patch: {current}{plugin_version}]")
break
if not plugin_version:
print(" ERROR: Could not read current plugin version for bump-patch")
sys.exit(1)
if plugin_version and "plugin" in refs:
print(f"\n[Plugin version → {plugin_version}]")
for ref in refs["plugin"].get("files", []):
process_file_ref(ref, plugin_version)
if plugin_version and args.changelog_summary:
changelog_file = config.get("changelog_file", "CHANGELOG.md")
update_changelog(changelog_file, plugin_version, args.changelog_summary)
print("\n=== Version update complete ===")
if __name__ == "__main__":
main()
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Publish plugin to package registry and create git tag."""
import sys
import json
import re
import subprocess
import argparse
from pathlib import Path
CONFIG_PATH = ".claude/skills/update-sdk/scripts/config.json"
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def run(cmd: list, cwd: str = ".") -> tuple:
try:
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=300)
return r.returncode == 0, (r.stdout + r.stderr).strip()
except subprocess.TimeoutExpired:
return False, "Command timed out after 300 seconds"
except Exception as e:
return False, str(e)
def get_plugin_version(config: dict) -> str:
refs = config.get("version_refs", {})
for ref in refs.get("plugin", {}).get("files", []):
path = Path(ref["path"])
if not path.exists():
continue
if path.suffix == ".json":
data = json.loads(path.read_text(encoding="utf-8"))
key = ref.get("key", "version")
for k in key.split("."):
data = data.get(k, {})
if isinstance(data, str):
return data
elif path.suffix in (".yaml", ".yml"):
m = re.search(r"^version:\s*(.+)$", path.read_text(encoding="utf-8"), re.MULTILINE)
if m:
return m.group(1).strip()
elif path.suffix == ".xml":
pattern = ref.get("pattern", "")
if pattern and "{VERSION}" in pattern:
regex = re.escape(pattern).replace(r"\{VERSION\}", r"([\d.]+(?:-\w+)?)")
m = re.search(regex, path.read_text(encoding="utf-8"))
else:
# fallback: match plugin id= line followed by version=
m = re.search(r'id="[^"]+"\s+version="([^"]+)"', path.read_text(encoding="utf-8"))
if m:
return m.group(1)
return "unknown"
def git_commit_and_tag(version: str, plugin_name: str) -> bool:
print(f"\n[Git] Committing {plugin_name} v{version} ...")
ok, out = run(["git", "add", "-A"])
if not ok:
print(f" ERROR: git add failed:\n{out}")
return False
ok, out = run(["git", "commit", "-m", f"chore: release {plugin_name} v{version}"])
if not ok:
print(f" ERROR: git commit failed:\n{out}")
return False
print(f" ✅ Committed")
ok, out = run(["git", "tag", f"v{version}"])
if not ok:
print(f" ERROR: git tag failed:\n{out}")
return False
print(f" ✅ Tagged v{version}")
ok, out = run(["git", "push", "origin", "HEAD"])
print(f" {'' if ok else '⚠️ '} Push commits: {'OK' if ok else out}")
ok, out = run(["git", "push", "origin", f"v{version}"])
print(f" {'' if ok else '⚠️ '} Push tag: {'OK' if ok else out}")
return True
def publish_npm() -> bool:
print("\n[npm] Publishing ...")
ok, out = run(["npm", "publish"])
if ok:
print(" ✅ Published to npm")
return True
print(f" ERROR: npm publish failed:\n{out}")
return False
def publish_dart() -> bool:
print("\n[dart] Publishing to pub.dev ...")
ok, out = run(["dart", "pub", "publish", "--force"])
if ok:
print(" ✅ Published to pub.dev")
return True
print(f" ERROR: dart pub publish failed:\n{out}")
return False
def prompt_dcloud(config: dict, plugin_name: str, version: str):
pub = config.get("publish", {})
paths = pub.get("dcloud_upload_paths", ["."])
print(f"\n⚠️ [{plugin_name} v{version}] 需手动上传至 DCloud 插件市场")
print(f" 上传地址: https://ext.dcloud.net.cn/")
print(f" 请上传以下目录:")
for p in paths:
print(f" - {Path(p).resolve()}")
def main():
parser = argparse.ArgumentParser(description="Publish plugin")
parser.add_argument("--skip-git", action="store_true", help="Skip git commit/tag/push")
parser.add_argument("--skip-publish", action="store_true", help="Skip package registry publish")
args = parser.parse_args()
config = load_config()
plugin_name = config.get("plugin_name", "unknown")
pub_type = config.get("publish", {}).get("type", "npm")
version = get_plugin_version(config)
print(f"\n=== Publishing {plugin_name} v{version} ===")
if not args.skip_git:
git_commit_and_tag(version, plugin_name)
if args.skip_publish:
print("Skipping publish (--skip-publish)")
return
if pub_type == "npm":
publish_npm()
elif pub_type == "dart":
publish_dart()
elif pub_type == "dcloud":
prompt_dcloud(config, plugin_name, version)
elif pub_type == "npm_and_dcloud":
publish_npm()
prompt_dcloud(config, plugin_name, version)
print(f"\n=== Done: {plugin_name} ===")
if __name__ == "__main__":
main()
@@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""Download and replace direct SDK files (for plugins that don't use maven/cocoapods)."""
import sys
import json
import re
import shutil
import fnmatch
import tempfile
import zipfile
import argparse
from pathlib import Path
try:
import requests
except ImportError:
print("ERROR: Missing dependencies. Run: pip3 install requests")
sys.exit(1)
CONFIG_PATH = ".claude/skills/update-sdk/scripts/config.json"
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}
# Download URL keys per sdk_type and platform
# JCore has no standalone download — it's bundled inside JPush zip
DOWNLOAD_KEYS = {
"jpush": {"android": "android", "ios": "ios", "harmony": "hmos"},
"jcore": {"android": "android", "ios": "ios"},
}
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def get_zip_url(sdk_type: str, platform: str) -> tuple[str, str]:
"""Follow redirect chain to get the actual zip URL and available version."""
key = DOWNLOAD_KEYS.get(sdk_type, {}).get(platform)
if not key:
return "", "unknown"
base_url = f"https://www.jiguang.cn/downloads/sdk/{key}"
try:
resp = requests.get(base_url, allow_redirects=True, headers=HEADERS, timeout=30)
final_url = resp.url
except requests.RequestException as e:
print(f" ERROR: Failed to resolve download URL: {e}")
return "", "unknown"
filename = Path(final_url.split("?")[0]).name
match = re.search(r"(\d+\.\d+\.\d+)", filename)
version = match.group(1) if match else "unknown"
return final_url, version
def extract_sdk_files(zip_url: str, dest_dir: Path, patterns: list) -> list[str]:
"""Download zip and extract files/dirs matching patterns into dest_dir."""
dest_dir.mkdir(parents=True, exist_ok=True)
print(f" Downloading {zip_url.split('?')[0].split('/')[-1]} ...")
try:
resp = requests.get(zip_url, stream=True, headers=HEADERS, timeout=300)
resp.raise_for_status()
except requests.RequestException as e:
print(f" ERROR: Download failed: {e}")
return []
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
zip_path = tmp_path / "sdk.zip"
total = 0
with open(zip_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=65536):
f.write(chunk)
total += len(chunk)
print(f" Downloaded {total // 1024} KB")
with zipfile.ZipFile(zip_path) as zf:
zf.extractall(tmp_path)
copied = _copy_matching(tmp_path, dest_dir, patterns)
return copied
SKIP_DIRS = {"example", "demo", "__MACOSX", ".git"}
def _copy_matching(src_root: Path, dest_dir: Path, patterns: list) -> list[str]:
"""Walk src_root and copy files/dirs whose name matches any pattern.
Skips example/demo/__MACOSX directories and deduplicates by filename.
"""
copied = []
visited_dirs = set()
copied_names = set()
for item in sorted(src_root.rglob("*")):
# Skip excluded directories and their contents
if any(part in SKIP_DIRS for part in item.parts):
continue
# Skip if already copied as part of a parent directory
if any(item.is_relative_to(d) for d in visited_dirs):
continue
name = item.name
if not any(fnmatch.fnmatch(name, p) for p in patterns):
continue
# Deduplicate by filename
if name in copied_names:
continue
dest = dest_dir / name
if item.is_dir():
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(item, dest)
visited_dirs.add(item)
print(f" Copied {name}/ → {dest_dir}")
else:
shutil.copy2(item, dest)
print(f" Copied {name}{dest_dir}")
copied.append(name)
copied_names.add(name)
return copied
def replace_from_local(user_path: str, dest_dir: Path, patterns: list) -> dict:
"""Copy SDK files from a user-provided local path."""
src = Path(user_path)
dest_dir.mkdir(parents=True, exist_ok=True)
if src.is_file() and src.suffix == ".zip":
print(f" Extracting {src.name} ...")
with tempfile.TemporaryDirectory() as tmp:
with zipfile.ZipFile(src) as zf:
zf.extractall(tmp)
copied = _copy_matching(Path(tmp), dest_dir, patterns)
else:
copied = _copy_matching(src, dest_dir, patterns)
return {"status": "updated", "source": "user_provided", "files": copied}
def handle_platform(platform: str, sdk_type: str, target_version: str,
direct_cfg: dict, user_sdk_path: str | None) -> dict:
print(f"\n=== {platform.upper()} SDK (target: v{target_version}) ===")
dest_dir = Path(direct_cfg["dest_dir"])
patterns = direct_cfg.get("file_patterns", ["*.aar", "*.jar", "*.a", "*.framework", "*.xcframework"])
if user_sdk_path:
result = replace_from_local(user_sdk_path, dest_dir, patterns)
return {"platform": platform, **result}
zip_url, available_version = get_zip_url(sdk_type, platform)
if not zip_url:
return {"platform": platform, "status": "error", "reason": "Could not resolve download URL"}
# For jcore, the SDK is bundled inside the JPush zip — skip zip-filename version check.
# For jpush/harmony, zip filename carries the SDK version directly.
if sdk_type != "jcore" and available_version != target_version.lstrip("v"):
msg = (f"Latest available version is {available_version}, "
f"target {target_version} not yet released. "
f"Re-run with --{platform}-sdk-path /path/to/sdk when available.")
print(f" ⚠️ {msg}")
return {"platform": platform, "status": "version_mismatch",
"available": available_version, "target": target_version, "reason": msg}
copied = extract_sdk_files(zip_url, dest_dir, patterns)
if copied:
return {"platform": platform, "status": "updated", "source": "auto", "files": copied}
else:
return {"platform": platform, "status": "error", "reason": "No matching files found in zip"}
def main():
parser = argparse.ArgumentParser(description="Download and replace direct SDK files")
parser.add_argument("--android", help="Android SDK target version")
parser.add_argument("--ios", help="iOS SDK target version")
parser.add_argument("--harmony", help="HarmonyOS SDK target version")
parser.add_argument("--android-sdk-path", help="Local path to Android SDK (zip or dir)")
parser.add_argument("--ios-sdk-path", help="Local path to iOS SDK (zip or dir)")
parser.add_argument("--harmony-sdk-path", help="Local path to HarmonyOS SDK (zip or dir)")
args = parser.parse_args()
config = load_config()
sdk_type = config.get("sdk_type", "jpush")
direct_sdk = config.get("direct_sdk", {})
if not direct_sdk:
print("INFO: This plugin uses maven/cocoapods. No direct SDK files to manage.")
return
results = []
platforms = [
("android", args.android, args.android_sdk_path),
("ios", args.ios, args.ios_sdk_path),
("harmony", args.harmony, args.harmony_sdk_path),
]
for platform, version, user_path in platforms:
if version and direct_sdk.get(platform):
results.append(handle_platform(platform, sdk_type, version,
direct_sdk[platform], user_path))
with open(".claude/skills/update-sdk/scripts/.sdk_download_result.json", "w") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print("\n=== SDK download summary ===")
for r in results:
icon = "" if r["status"] == "updated" else "⚠️ "
detail = f" ({len(r.get('files', []))} files)" if r.get("files") else ""
print(f" {icon} {r['platform']}: {r['status']}{detail}")
if __name__ == "__main__":
main()
+5
View File
@@ -67,3 +67,8 @@ ionic/example/src/app/app\.scss
ionic/example/src/app/main\.ts
# End of https://www.gitignore.io/api/macos,apachecordova
# Claude update-sdk skill temp files
.claude/skills/update-sdk/scripts/.sdk_download_result.json
.claude/skills/update-sdk/scripts/.changelog_cache.json
.claude/settings.local.json