From ac15c5afc399599bcac39cc2ac7b203db13102de Mon Sep 17 00:00:00 2001 From: huangshuni <15889638723@163.com> Date: Thu, 14 May 2026 17:25:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/update-sdk/SKILL.md | 123 ++++++++ .../update-sdk/scripts/.changelog_cache.json | 4 + .../update-sdk/scripts/changelog_fetcher.py | 87 ++++++ .claude/skills/update-sdk/scripts/config.json | 60 ++++ .../update-sdk/scripts/plugin_updater.py | 277 ++++++++++++++++++ .../skills/update-sdk/scripts/publisher.py | 152 ++++++++++ .../update-sdk/scripts/sdk_downloader.py | 222 ++++++++++++++ .gitignore | 5 + 8 files changed, 930 insertions(+) create mode 100644 .claude/skills/update-sdk/SKILL.md create mode 100644 .claude/skills/update-sdk/scripts/.changelog_cache.json create mode 100644 .claude/skills/update-sdk/scripts/changelog_fetcher.py create mode 100644 .claude/skills/update-sdk/scripts/config.json create mode 100644 .claude/skills/update-sdk/scripts/plugin_updater.py create mode 100644 .claude/skills/update-sdk/scripts/publisher.py create mode 100644 .claude/skills/update-sdk/scripts/sdk_downloader.py diff --git a/.claude/skills/update-sdk/SKILL.md b/.claude/skills/update-sdk/SKILL.md new file mode 100644 index 0000000..0983f83 --- /dev/null +++ b/.claude/skills/update-sdk/SKILL.md @@ -0,0 +1,123 @@ +--- +name: update-sdk +description: | + 更新 jpush-phonegap-plugin 插件的 JPush SDK 版本。自动拉取极光官网 Changelog,分析新增/移除/变更 API,更新 plugin.xml 中 Android(Gradle framework)和 iOS(CocoaPods 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 --ios +``` + +读取 `.claude/skills/update-sdk/scripts/.changelog_cache.json` 获取 Changelog 内容。 + +--- + +## 第四步:AI 分析变更 + +分析 Changelog,整理: + +> **注意**:Changelog 同时包含 JPush 和 JCore 的变更。只关注调用类以 `JPush` 开头的条目,忽略类名以 `JCore` 开头的内容。 + +1. 新增 API(Android + iOS 相同功能合并为统一插件 API);仅单端有的 → **先检查另一端是否已有等价实现**(见下方说明),确认缺失才标注单端 +2. 移除/废弃 API +3. 行为变更 +4. 新插件版本号:始终升 patch(如 3.4.9 → 3.5.0,3.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 \ + --ios \ + --bump-patch \ + --changelog-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 +``` diff --git a/.claude/skills/update-sdk/scripts/.changelog_cache.json b/.claude/skills/update-sdk/scripts/.changelog_cache.json new file mode 100644 index 0000000..d089607 --- /dev/null +++ b/.claude/skills/update-sdk/scripts/.changelog_cache.json @@ -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 架构" +} \ No newline at end of file diff --git a/.claude/skills/update-sdk/scripts/changelog_fetcher.py b/.claude/skills/update-sdk/scripts/changelog_fetcher.py new file mode 100644 index 0000000..f683192 --- /dev/null +++ b/.claude/skills/update-sdk/scripts/changelog_fetcher.py @@ -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() diff --git a/.claude/skills/update-sdk/scripts/config.json b/.claude/skills/update-sdk/scripts/config.json new file mode 100644 index 0000000..2267edc --- /dev/null +++ b/.claude/skills/update-sdk/scripts/config.json @@ -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": " 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() diff --git a/.claude/skills/update-sdk/scripts/publisher.py b/.claude/skills/update-sdk/scripts/publisher.py new file mode 100644 index 0000000..806c5e9 --- /dev/null +++ b/.claude/skills/update-sdk/scripts/publisher.py @@ -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() diff --git a/.claude/skills/update-sdk/scripts/sdk_downloader.py b/.claude/skills/update-sdk/scripts/sdk_downloader.py new file mode 100644 index 0000000..a1b7308 --- /dev/null +++ b/.claude/skills/update-sdk/scripts/sdk_downloader.py @@ -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() diff --git a/.gitignore b/.gitignore index 87c2512..6cbb515 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file