diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..bbef5ab --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,39 @@ +{ + "name": "generalupdate-skill-codegen", + "id": "generalupdate-skill-codegen", + "owner": { + "name": "JusterZhu" + }, + "metadata": { + "description": ".NET auto-update skill suite — 7 skills for GeneralUpdate: scaffolding, UI, strategy, advanced, troubleshooting (50+ issues), migration, and security audit", + "version": "0.0.1-bate.1" + }, + "plugins": [ + { + "name": "generalupdate-skill", + "source": "./", + "description": "Complete GeneralUpdate (.NET auto-update) integration skill suite. Generates dual-project scaffolding, full-state update UI (6 frameworks), 6 update strategies decision tree (Client-Server/OSS/Silent/Differential/CVP/Push), advanced extension points (Bowl crash daemon, IPC replacement, AOT), BM25-powered troubleshooting search (50+ known issues), v9.x→v10 migration guide, and 14-point security audit matrix. All templates target NuGet v10.4.6 stable API.", + "version": "0.0.1-bate.1", + "author": { + "name": "JusterZhu" + }, + "keywords": [ + "generalupdate", + "auto-update", + "dotnet", + ".net", + "wpf", + "avalonia", + "maui", + "update", + "upgrade", + "bootstrap", + "bowl", + "differential", + "nuget" + ], + "category": "code-generation", + "strict": false + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..1140f53 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,32 @@ +{ + "name": "generalupdate-skill", + "description": "Complete .NET auto-update skill suite for GeneralUpdate. 7 skills covering: Bootstrap scaffolding, update UI (6 frameworks), 6 strategies (Client-Server/OSS/Silent/Differential/CVP/Push), advanced extension points (Bowl, IPC, AOT), 50+ known issues diagnosis with BM25 search engine, version migration, and security audit. All templates target NuGet v10.4.6 stable.", + "version": "0.0.1-bate.1", + "author": { + "name": "JusterZhu" + }, + "license": "Apache-2.0", + "keywords": [ + "generalupdate", + "auto-update", + "dotnet", + ".net", + "wpf", + "avalonia", + "maui", + "winforms", + "bootstrap", + "bowl", + "differential", + "nuget" + ], + "skills": [ + ".claude/skills/generalupdate-init", + ".claude/skills/generalupdate-ui", + ".claude/skills/generalupdate-strategy", + ".claude/skills/generalupdate-advanced", + ".claude/skills/generalupdate-troubleshoot", + ".claude/skills/generalupdate-migration", + ".claude/skills/generalupdate-security-audit" + ] +} diff --git a/.claude/scripts/_sync_all.py b/.claude/scripts/_sync_all.py new file mode 100644 index 0000000..be6f53f --- /dev/null +++ b/.claude/scripts/_sync_all.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Sync all source artifacts from .claude/ to cli/assets/ before release. +This is the single entry point for ensuring CLI bundles are up-to-date. + +Usage: + python3 .claude/scripts/_sync_all.py # Dry run (verbose) + python3 .claude/scripts/_sync_all.py --apply # Actually copy files + python3 .claude/scripts/_sync_all.py --verify # Only check for differences +""" +import argparse +import filecmp +import os +import shutil +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent # .claude/scripts/ -> repo root +CLAUDE_DIR = REPO_ROOT / ".claude" +CLI_ASSETS_DIR = REPO_ROOT / "cli" / "assets" +SKILL_DIR = CLAUDE_DIR / "skills" + +# Sync mappings: (source_relative, dest_relative, description) +SYNC_MAP = [ + # All 7 skills (to cli/assets/skills/) + ("skills", "skills", "All 7 skill directories"), + # Troubleshoot scripts + data (to cli/assets/scripts/ + cli/assets/data/) + ("skills/generalupdate-troubleshoot/scripts", "scripts", "BM25 search engine"), + ("skills/generalupdate-troubleshoot/data", "data", "Issues + strategies CSV"), + # Code generator + ("scripts/generate.py", "scripts/generate.py", "Parameterized code generator"), + ("scripts/generate", "scripts/generate", "Generator templates"), +] + + +def sync_file(src: Path, dst: Path, apply: bool, dry_run: bool) -> str: + """Sync one file or directory. Returns status string.""" + if not src.exists(): + return f"⚠️ SOURCE MISSING: {src.relative_to(REPO_ROOT)}" + + if dst.exists() and filecmp.cmp(src, dst) if src.is_file() else _dirs_equal(src, dst): + return f"✓ UP TO DATE: {src.relative_to(REPO_ROOT)}" + + if dry_run or not apply: + return f"→ NEEDS SYNC: {src.relative_to(REPO_ROOT)} → {dst.relative_to(REPO_ROOT)}" + + # Apply the sync + dst.parent.mkdir(parents=True, exist_ok=True) + if src.is_file(): + shutil.copy2(src, dst) + else: + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst) + return f"✅ SYNCED: {src.relative_to(REPO_ROOT)}" + + +def _dirs_equal(a: Path, b: Path) -> bool: + """Check if all files in source exist and match in destination (b may have extras).""" + if not b.exists(): + return False + + def _files(p: Path): + return sorted( + (f for f in p.rglob("*") if f.is_file() and "__pycache__" not in str(f)), + key=lambda x: str(x).lower(), + ) + + afiles = _files(a) + + for fa in afiles: + rel = fa.relative_to(a) + fb = b / rel + if not fb.exists(): + return False + if not filecmp.cmp(fa, fb, shallow=False): + return False + + return True + + +def main(): + parser = argparse.ArgumentParser(description="Sync .claude/ source to cli/assets/") + parser.add_argument("--apply", action="store_true", help="Actually copy files (default: dry-run)") + parser.add_argument("--verify", action="store_true", help="Only check, exit 1 if out of sync") + args = parser.parse_args() + + dry_run = not args.apply + if dry_run and not args.verify: + print("═══ DRY RUN ═══ Use --apply to actually copy\n") + + statuses = [] + all_ok = True + + for src_rel, dst_rel, desc in SYNC_MAP: + src = CLAUDE_DIR / src_rel + dst = CLI_ASSETS_DIR / dst_rel + status = sync_file(src, dst, args.apply, dry_run) + statuses.append((desc, status)) + if status.startswith("⚠️"): + all_ok = False + + print(f"\n═══ Summary ({'DRY RUN' if dry_run else 'APPLIED'}) ═══\n") + for desc, status in statuses: + print(f" {status}") + + if args.verify and not all_ok: + print("\n❌ Verify FAILED: some sources are missing") + sys.exit(1) + + if args.verify: + print("\n✅ Verify PASSED: all sources are in sync") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.claude/scripts/generate.py b/.claude/scripts/generate.py new file mode 100644 index 0000000..9e222ae --- /dev/null +++ b/.claude/scripts/generate.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GeneralUpdate Code Generator — generates production-ready C# integration code. +Usage: python3 scripts/generate.py --framework wpf --strategy oss --output ./Generated + +Supports 288 combinations: 4 scenes × 6 strategies × 6 UI frameworks × 2 Bowl + +Combinations: + Scenes: None, UpgradeOnly, MainOnly, Both + Strategies: standard, oss, silent, differential, cvp, push + Frameworks: wpf-原生, wpf-layui, wpf-wpfdevelopers, winforms-antdui, avalonia-semiursa, maui, console + Bowl: yes, no +""" +import argparse +import os +import sys +from pathlib import Path +from string import Template +import json + +SCRIPT_DIR = Path(__file__).parent +TEMPLATES_DIR = SCRIPT_DIR / "generate" / "templates" + +# ============ CONFIG MATRIX ============ + +STRATEGIES = { + "standard": { + "name": "Standard Client-Server", + "slug": "standard", + "description": "Standard dual-process update with GeneralSpacestation backend", + }, + "oss": { + "name": "OSS Object Storage", + "slug": "oss", + "description": "Update via S3/MinIO/cloud object storage; no backend server needed", + "warning": "OSS模式不区分 Main/Upgrade 更新包。Upgrade.exe 必须放在 update/ 子目录。", + }, + "silent": { + "name": "Silent Update", + "slug": "silent", + "description": "Background polling update with minimal user interruption", + }, + "differential": { + "name": "Differential Update", + "slug": "differential", + "description": "Delta patch update to save bandwidth (BSDIFF/HDiffPatch)", + "warning": "差分包大小建议不超过 2GB,避免 BSDIFF 整数溢出(v10.4.6+ 已修复 #514)。", + }, + "cvp": { + "name": "Cross-Version CVP", + "slug": "cvp", + "description": "Skip intermediate versions and jump directly to target version", + "warning": "跨版本跳转需要服务端 API 兼容性验证,避免跳转后 API 不匹配。", + }, + "push": { + "name": "SignalR Push", + "slug": "push", + "description": "Server-initiated push update via SignalR real-time connection", + "warning": "HubConnection Dispose 后必须置 null。离线客户端可能错过推送。", + }, +} + +UI_FRAMEWORKS = { + "wpf-原生": {"class": "WpfNative", "uses_xaml": True, "needs_dispatcher": True}, + "wpf-layui": {"class": "WpfLayUI", "uses_xaml": True, "needs_dispatcher": True}, + "wpf-wpfdevelopers": {"class": "WpfDevelopers", "uses_xaml": True, "needs_dispatcher": True}, + "winforms-antdui": {"class": "WinFormsAntdUI", "uses_xaml": False, "needs_dispatcher": True}, + "avalonia-semiursa": {"class": "AvaloniaSemiUrsa", "uses_xaml": True, "needs_dispatcher": True}, + "maui": {"class": "MauiApp", "uses_xaml": True, "needs_dispatcher": False}, + "console": {"class": "ConsoleApp", "uses_xaml": False, "needs_dispatcher": False}, +} + + +def load_template(name): + path = TEMPLATES_DIR / name + if not path.exists(): + print(f"⚠️ Template not found: {path}") + return "" + return path.read_text(encoding="utf-8") + + +def render(template_text, variables): + """Simple {{PLACEHOLDER}} substitution with defaults for optional sections.""" + result = template_text + for key, value in variables.items(): + result = result.replace("{{" + key + "}}", str(value)) + + # Handle optional sections: {{#KEY}}content{{/KEY}} or {{^KEY}}content{{/KEY}} + # Simple positive conditional blocks + def replace_conditional(match): + key = match.group(1) + content = match.group(2) + if variables.get(key, False) and str(variables.get(key, "")).lower() in ("true", "yes", "1", key): + return content + return "" + + result = re.sub(r"\{\{#(\w+)\}\}(.*?)\{\{/\1\}\}", replace_conditional, result, flags=re.DOTALL) + + # Negative conditional blocks {{^KEY}}...{{/KEY}} + def replace_negative(match): + key = match.group(1) + content = match.group(2) + if not variables.get(key, False) or str(variables.get(key, "")).lower() in ("false", "no", "0", ""): + return content + return "" + + result = re.sub(r"\{\{(\^)(\w+)\}\}(.*?)\{\{/\2\}\}", replace_negative, result, flags=re.DOTALL) + + return result + + +def generate_bootstrap(strategy, framework, with_bowl, scenes, variables): + """Generate Bootstrap.cs integration code.""" + templ = load_template("Bootstrap.cs.template") + + listener_blocks = [] + + # Always include basic listeners + listener_names = ["MultiDownloadStatistics", "MultiDownloadCompleted", + "MultiDownloadError", "MultiAllDownloadCompleted", "Exception"] + + if framework != "console": + listener_names = ["MultiDownloadStatistics", "MultiDownloadCompleted", + "MultiDownloadError", "MultiDownloadCompleted", + "MultiAllDownloadCompleted", "Exception"] + + if framework == "console": + # Console just writes to stdout + listeners_templ = load_template("listeners_console.cs.template") + elif framework.startswith("wpf") or framework == "avalonia-semiursa": + listeners_templ = load_template("listeners_mvvm.cs.template") + elif framework == "winforms-antdui": + listeners_templ = load_template("listeners_winforms.cs.template") + elif framework == "maui": + listeners_templ = load_template("listeners_maui.cs.template") + else: + listeners_templ = load_template("listeners_console.cs.template") + + listeners_code = render(listeners_templ, variables) + + bowl_notice = "" + if with_bowl: + bowl_notice = load_template("bowl_notice.cs.template") + bowl_notice = render(bowl_notice, variables) + + strategy_notice = "" + if strategy in STRATEGIES and STRATEGIES[strategy].get("warning"): + strategy_notice = f"// ⚠️ {STRATEGIES[strategy]['warning']}\n" + + code = render(templ, { + **variables, + "LISTENERS": listeners_code, + "BOWL_NOTICE": bowl_notice, + "STRATEGY_WARNING": strategy_notice, + }) + + return code + + +def generate_manifest(variables): + """Generate generalupdate.manifest.json.""" + templ = load_template("manifest.json.template") + return render(templ, variables) + + +def generate_upgrade_program(variables): + """Generate UpgradeProgram.cs.""" + templ = load_template("UpgradeProgram.cs.template") + return render(templ, variables) + + +def generate_deployment_checklist(strategy, framework, with_bowl, variables): + """Deployment checklist Markdown.""" + templ = load_template("DeploymentChecklist.md.template") + return render(templ, variables) + + +def generate_issue_warnings(strategy, variables): + """Generate known issue warnings for the specific config combination.""" + warnings_map = { + "oss": """⚠️ OSS 特有已知问题: + - H4: OSS 不区分 Main/Upgrade 更新包,接受此行为 + - H5: Upgrade.exe 必须放在 update/ 子目录 + - L7: 示例代码中 OSS endpoint/bucket 写死,建议用环境变量 + - M13: OssClient.AppType 值 3-4 在 v10.4.6 不支持""", + "silent": """⚠️ 静默更新特有已知问题: + - H2: 无限升级循环 — 确保 manifest.json 版本号正确 + - M19: 静默通知可能不尊重系统的免打扰设置 + - M9: 升级进程超时 — 大文件操作建议增加超时时间""", + "differential": """⚠️ 差分更新特有已知问题: + - C3: BSDIFF 整数溢出 — 差分包 < 2GB + - M1: 不要额外引用 GeneralUpdate.Differential(已嵌入 Core) + - L3: 差分 clean/dirty 参数缺少验证,建议手动检查路径 + - L5: 进程内存跟踪使用 private bytes 而非 working set""", + "cvp": """⚠️ CVP 跨版本特有已知问题: + - H8: 跨版本跳转跳过 API 兼容性检查 — 服务端需要验证 + - C7: 多租户跨租户版本泄露风险 — 确认 ProductId 隔离""", + "push": """⚠️ SignalR 推送特有已知问题: + - H10: HubConnection Dispose 后重连崩溃 — 置 null + - M11: 推送更新无送达确认 — 建议实现 ACK 机制 + - M9: 慢操作超时场景下连接可能断开""", + "standard": """⚠️ 标准策略已知问题(非特有但常见): + - C1: UpgradeApp.exe 必须随首个版本发布 + - C2: Client/Upgrade NuGet 版本必须一致 + - H3: IsComplated 拼写(注意不是 IsCompleted) + - M5: InstallPath 使用相对路径导致文件解析失败 + - M6: UpdateUrl 返回空响应体时做 null 检查""", + } + warning = warnings_map.get(strategy, "该策略组合暂无特别预警。") + + t = load_template("IssuesWarning.md.template") + return render(t, {**variables, "WARNINGS_LIST": warning}) + + +def generate_strategy_checks(strategy): + """Return strategy-specific checklist items.""" + checks = { + "oss": """- [ ] Bucket 权限设置为私有 +- [ ] Upgrade.exe 放在 update/ 子目录 +- [ ] 接受 OSS 不区分 Main/Upgrade 的限制 +- [ ] 包名包含版本号 (如 MyApp_1.0.0.0.zip)""", + "silent": """- [ ] 轮询间隔 30-60 分钟 +- [ ] 下载完成后通知用户重启 +- [ ] 有 WiFi/流量限制考虑""", + "differential": """- [ ] 服务端有差分包生成机制 +- [ ] Pipeline 配置了 PatchMiddleware +- [ ] 差分包 < 2GB(避免 BSDIFF 整数溢出) +- [ ] Linux/macOS 补丁兼容性已验证""", + "cvp": """- [ ] 服务端有 CVP 构建流水线 +- [ ] 源/目标版本间 API 兼容性已验证 +- [ ] 客户端数据库迁移已测试""", + "push": """- [ ] HubConnection 生命周期管理 +- [ ] 自动重连逻辑(3次,间隔递增) +- [ ] Dispose 时将连接置 null +- [ ] 推送失败降级到轮询""", + "standard": """- [ ] GeneralSpacestation 或兼容后端已部署 +- [ ] API 返回合法的版本列表 +- [ ] 4 段式版本号返回""", + } + return checks.get(strategy, "- [ ] 基本配置已验证") + + +def generate(args): + strategy = args.strategy + framework = args.framework + with_bowl = args.bowl + scenes = args.scenes + + output_dir = Path(args.output) + project_name = args.project_name or "MyApp" + app_secret = args.app_secret_key or "CHANGE-ME-TO-A-32-CHAR-SECRET-KEY!" + update_url = args.update_url or "https://your-server.com/Upgrade/Verification" + version = args.version or "1.0.0.0" + product_id = args.product_id or project_name.lower().replace(" ", "-") + "-001" + + from datetime import date + today = date.today().isoformat() + + bowl_lower = "yes" if with_bowl else "no" + variables = { + "PROJECT_NAME": project_name, + "APP_SECRET_KEY": app_secret, + "UPDATE_URL": update_url, + "CLIENT_VERSION": version, + "PRODUCT_ID": product_id, + "STRATEGY": strategy, + "STRATEGY_NAME": STRATEGIES.get(strategy, {}).get("name", strategy), + "FRAMEWORK": framework, + "FRAMEWORK_CLASS": UI_FRAMEWORKS.get(framework, {}).get("class", "App"), + "BOWL": bowl_lower, + "BOWL_UPPER": "Yes" if with_bowl else "No", + "SCENES": scenes, + "INSTALL_PATH": "AppDomain.CurrentDomain.BaseDirectory", + "DATE": today, + "STRATEGY_CHECKS": generate_strategy_checks(strategy), + } + # ISSUE_WARNINGS depends on variables being fully constructed + variables["ISSUE_WARNINGS"] = generate_issue_warnings(strategy, variables) + + # Generate files + files = {} + + # Bootstrap + bootstrap_code = generate_bootstrap(strategy, framework, with_bowl, scenes, variables) + files["Client/Integration.cs"] = bootstrap_code + + # Manifest + manifest_json = generate_manifest(variables) + files["generalupdate.manifest.json"] = manifest_json + + # Upgrade program + upgrade_code = generate_upgrade_program(variables) + files["Upgrade/UpgradeProgram.cs"] = upgrade_code + + # Deployment checklist + checklist = generate_deployment_checklist(strategy, framework, with_bowl, variables) + files["DeploymentChecklist.md"] = checklist + + # Issue warnings + warnings = generate_issue_warnings(strategy, variables) + files["IssuesWarning.md"] = warnings + + # Write files + for relpath, content in files.items(): + full_path = output_dir / relpath + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text(content, encoding="utf-8") + print(f" ✓ {relpath}") + + print(f"\n✅ Generated {len(files)} files for {STRATEGIES[strategy]['name']} + {framework}") + print(f" Output: {output_dir.resolve()}") + + +if __name__ == "__main__": + import re # needed for template rendering + + parser = argparse.ArgumentParser(description="GeneralUpdate Code Generator") + parser.add_argument("--framework", "-f", choices=list(UI_FRAMEWORKS.keys()), default="wpf-原生", + help="Target UI framework") + parser.add_argument("--strategy", "-s", choices=list(STRATEGIES.keys()), default="standard", + help="Update strategy") + parser.add_argument("--bowl", action="store_true", default=False, + help="Include Bowl crash daemon") + parser.add_argument("--scenes", default="Both", + help="Update scenes: None/UpgradeOnly/MainOnly/Both (default: Both)") + parser.add_argument("--output", "-o", default="./Generated", + help="Output directory (default: ./Generated)") + parser.add_argument("--project-name", "-n", default="MyApp", + help="Project name (default: MyApp)") + parser.add_argument("--app-secret-key", help="AppSecretKey (min 32 chars)") + parser.add_argument("--update-url", help="Update API URL") + parser.add_argument("--version", "-v", default="1.0.0.0", + help="Client version (default: 1.0.0.0)") + parser.add_argument("--product-id", help="Product ID (default: -001)") + parser.add_argument("--list", action="store_true", help="List all available combinations") + + args = parser.parse_args() + + if args.list: + print("Available strategies:") + for k, v in STRATEGIES.items(): + print(f" {k:15s} - {v['name']}") + print("\nAvailable UI frameworks:") + for k, v in UI_FRAMEWORKS.items(): + print(f" {k:20s}") + print(f"\nTotal combinations: {len(STRATEGIES)} strategies × {len(UI_FRAMEWORKS)} frameworks × 2 Bowl × 4 scenes = {len(STRATEGIES) * len(UI_FRAMEWORKS) * 2 * 4}") + sys.exit(0) + + generate(args) diff --git a/.claude/scripts/generate/templates/Bootstrap.cs.template b/.claude/scripts/generate/templates/Bootstrap.cs.template new file mode 100644 index 0000000..ddb7214 --- /dev/null +++ b/.claude/scripts/generate/templates/Bootstrap.cs.template @@ -0,0 +1,28 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +var config = new Configinfo +{ + // === 必需 === + UpdateUrl = "{{UPDATE_URL}}", + AppSecretKey = "{{APP_SECRET_KEY}}", + AppName = "{{PROJECT_NAME}}.exe", + MainAppName = "{{PROJECT_NAME}}.exe", + ClientVersion = "{{CLIENT_VERSION}}", + ProductId = "{{PRODUCT_ID}}", + InstallPath = {{INSTALL_PATH}}, + + // === 可选 === + Encoding = System.Text.Encoding.UTF8, +{{#BOWL}} + // Bowl 配置(仅包含 GeneralUpdate.Bowl 包,不重复添加 Core) +{{/BOWL}} +}; + +{{STRATEGY_WARNING}} +{{BOWL_NOTICE}} +await new GeneralUpdateBootstrap() + .SetConfig(config) +{{LISTENERS}} + .LaunchAsync(); diff --git a/.claude/scripts/generate/templates/DeploymentChecklist.md.template b/.claude/scripts/generate/templates/DeploymentChecklist.md.template new file mode 100644 index 0000000..eaea255 --- /dev/null +++ b/.claude/scripts/generate/templates/DeploymentChecklist.md.template @@ -0,0 +1,44 @@ +# Deployment Checklist — {{PROJECT_NAME}} + +Generated for: **{{STRATEGY_NAME}}** + **{{FRAMEWORK}}** | Bowl: **{{BOWL_UPPER}}** | Updated: {{DATE}} + +--- + +## ✅ Pre-Deployment Checklist + +### Bootstrap +- [ ] `Configinfo` 6 个必填字段都已设置 +- [ ] `UpdateUrl` 已指向正确的服务端 API +- [ ] `AppSecretKey` 长度 ≥ 32 字符 + +### NuGet +- [ ] Client 和 Upgrade 项目使用相同 GeneralUpdate 版本 +{{#BOWL}} +- [ ] 只引用了 `GeneralUpdate.Bowl`(未同时引 Core) +{{/BOWL}} +{{^BOWL}} +- [ ] 只引用了 `GeneralUpdate.Core` +{{/BOWL}} + +### Deployment +- [ ] UpgradeApp.exe 存在于发布目录 +- [ ] manifest.json 的 mainAppName 与进程名匹配 +- [ ] Encoding.UTF8 已设置 +- [ ] 版本号为 4 段式 (x.y.z.w) + +### Strategy-Specific +{{STRATEGY_CHECKS}} + +--- + +## ⚠️ Known Issues for This Configuration + +{{ISSUE_WARNINGS}} + +--- + +## Rollback Plan + +- [ ] 保留上一个版本的备份目录 +- [ ] 验证回滚后版本号是否正确 +- [ ] 通知用户回滚事件 diff --git a/.claude/scripts/generate/templates/IssuesWarning.md.template b/.claude/scripts/generate/templates/IssuesWarning.md.template new file mode 100644 index 0000000..62ada68 --- /dev/null +++ b/.claude/scripts/generate/templates/IssuesWarning.md.template @@ -0,0 +1,7 @@ +# Issues Warning — {{STRATEGY_NAME}} + {{FRAMEWORK}} + +This configuration may be affected by the following known issues: + +{{WARNINGS_LIST}} + +Cross-reference with `generalupdate-troubleshoot` reference.md for full details. diff --git a/.claude/scripts/generate/templates/UpgradeProgram.cs.template b/.claude/scripts/generate/templates/UpgradeProgram.cs.template new file mode 100644 index 0000000..aa9382c --- /dev/null +++ b/.claude/scripts/generate/templates/UpgradeProgram.cs.template @@ -0,0 +1,11 @@ +using GeneralUpdate.Core; + +// Upgrade 进程入口 — 从 IPC 文件读取配置,无需 SetConfig +// 注意: Upgrade 项目的 AppType 设为 2 (UpgradeApp) + +await new GeneralUpdateBootstrap() + .AddListenerException((_, e) => + { + System.Console.Error.WriteLine($"升级错误: {e.Message}"); + }) + .LaunchAsync(); diff --git a/.claude/scripts/generate/templates/bowl_notice.cs.template b/.claude/scripts/generate/templates/bowl_notice.cs.template new file mode 100644 index 0000000..9c11ee2 --- /dev/null +++ b/.claude/scripts/generate/templates/bowl_notice.cs.template @@ -0,0 +1,10 @@ +// ⚠️ Bowl 崩溃守护 — 使用 GeneralUpdate.Bowl,不单独引用 Core +// dotnet add package GeneralUpdate.Bowl +var bowlParam = new GeneralUpdate.Bowl.MonitorParameter +{ + ProcessNameOrId = "{{PROJECT_NAME}}.exe", + TargetPath = {{INSTALL_PATH}}, + WorkModel = "Upgrade", + FailDirectory = System.IO.Path.Combine({{INSTALL_PATH}}, "fail"), + BackupDirectory = System.IO.Path.Combine({{INSTALL_PATH}}, "backup"), +}; diff --git a/.claude/scripts/generate/templates/listeners_console.cs.template b/.claude/scripts/generate/templates/listeners_console.cs.template new file mode 100644 index 0000000..3472487 --- /dev/null +++ b/.claude/scripts/generate/templates/listeners_console.cs.template @@ -0,0 +1,25 @@ + .AddListenerUpdateInfo((_, e) => + { + var count = e.Info?.Body?.Count ?? 0; + Console.WriteLine($"发现 {count} 个版本待更新"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + Console.Write($"\r下载进度: {e.ProgressPercentage}% | {e.Speed}/s | 剩余 {e.Remaining}"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + Console.WriteLine($"\n版本 {e.Version} 下载完成 (IsComplated={e.IsComplated})"); + }) + .AddListenerMultiDownloadError((_, e) => + { + Console.Error.WriteLine($"\n下载失败: 版本 {e.Version} — {e.Exception?.Message}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + Console.WriteLine($"\n全部下载完成, 正在安装..."); + }) + .AddListenerException((_, e) => + { + Console.Error.WriteLine($"错误: {e.Message}"); + }) diff --git a/.claude/scripts/generate/templates/listeners_maui.cs.template b/.claude/scripts/generate/templates/listeners_maui.cs.template new file mode 100644 index 0000000..f9b07f1 --- /dev/null +++ b/.claude/scripts/generate/templates/listeners_maui.cs.template @@ -0,0 +1,25 @@ + .AddListenerUpdateInfo((_, e) => + { + // MAUI UI 更新需要 MainThread 和 using Microsoft.Maui.ApplicationModel; + System.Console.WriteLine($"发现 {e.Info?.Body?.Count ?? 0} 个版本待更新"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + System.Console.WriteLine($"下载进度: {e.ProgressPercentage}%"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + System.Console.WriteLine($"版本 {e.Version} 下载完成"); + }) + .AddListenerMultiDownloadError((_, e) => + { + System.Console.Error.WriteLine($"下载失败: {e.Exception?.Message}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + System.Console.WriteLine("全部下载完成, 正在安装..."); + }) + .AddListenerException((_, e) => + { + System.Console.Error.WriteLine($"错误: {e.Message}"); + }) diff --git a/.claude/scripts/generate/templates/listeners_mvvm.cs.template b/.claude/scripts/generate/templates/listeners_mvvm.cs.template new file mode 100644 index 0000000..5c97771 --- /dev/null +++ b/.claude/scripts/generate/templates/listeners_mvvm.cs.template @@ -0,0 +1,30 @@ + .AddListenerUpdateInfo((_, e) => + { + // ViewModel.UpdateCheckResult(e.Info?.Body) + System.Console.WriteLine($"发现 {e.Info?.Body?.Count ?? 0} 个版本待更新"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + // ViewModel: ProgressPercentage, Speed, Remaining, TotalBytesToReceive, BytesReceived + // 注意: 使用正确的 Dispatcher 更新 UI + // WPF: Application.Current.Dispatcher.Invoke(() => { ... }); + // Avalonia: Dispatcher.UIThread.Post(() => { ... }); + // 不要直接使用 Dispatcher.Invoke() — 它在不同框架中语义不同 + System.Console.WriteLine($"下载进度: {e.ProgressPercentage}%"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + System.Console.WriteLine($"版本 {e.Version} 下载完成"); + }) + .AddListenerMultiDownloadError((_, e) => + { + System.Console.Error.WriteLine($"下载失败: 版本 {e.Version} — {e.Exception?.Message}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + System.Console.WriteLine("全部下载完成, 正在安装..."); + }) + .AddListenerException((_, e) => + { + System.Console.Error.WriteLine($"错误: {e.Message}"); + }) diff --git a/.claude/scripts/generate/templates/listeners_winforms.cs.template b/.claude/scripts/generate/templates/listeners_winforms.cs.template new file mode 100644 index 0000000..cc4a45f --- /dev/null +++ b/.claude/scripts/generate/templates/listeners_winforms.cs.template @@ -0,0 +1,26 @@ + .AddListenerUpdateInfo((_, e) => + { + System.Console.WriteLine($"发现 {e.Info?.Body?.Count ?? 0} 个版本待更新"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + // WinForms: use Control.Invoke in your Form class + // this.Invoke((MethodInvoker)(() => { progressBar.Value = e.ProgressPercentage; })); + System.Console.WriteLine($"下载进度: {e.ProgressPercentage}%"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + System.Console.WriteLine($"版本 {e.Version} 下载完成"); + }) + .AddListenerMultiDownloadError((_, e) => + { + System.Console.Error.WriteLine($"下载失败: {e.Exception?.Message}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + System.Console.WriteLine("全部下载完成, 正在安装..."); + }) + .AddListenerException((_, e) => + { + System.Console.Error.WriteLine($"错误: {e.Message}"); + }) diff --git a/.claude/scripts/generate/templates/manifest.json.template b/.claude/scripts/generate/templates/manifest.json.template new file mode 100644 index 0000000..7319d50 --- /dev/null +++ b/.claude/scripts/generate/templates/manifest.json.template @@ -0,0 +1,8 @@ +{ + "mainAppName": "{{PROJECT_NAME}}.exe", + "updateAppName": "Upgrade{{PROJECT_NAME}}.exe", + "updatePath": "./update/", + "appType": 1, + "version": "{{CLIENT_VERSION}}", + "productId": "{{PRODUCT_ID}}" +} diff --git a/.claude/skills/generalupdate-advanced/SKILL.md b/.claude/skills/generalupdate-advanced/SKILL.md index ccdfad7..420fa96 100644 --- a/.claude/skills/generalupdate-advanced/SKILL.md +++ b/.claude/skills/generalupdate-advanced/SKILL.md @@ -36,6 +36,33 @@ allowed-tools: "Read, Write, Edit, Glob" --- +## 📋 用户需求提取(高级定制前必须确认) + +``` +### 定制目标(必需) +- 需要什么定制: ______(Bowl 崩溃守护 / IPC 替换 / Pipeline 定制 / 自定义策略 / AOT / Drivelution / 黑名单 / 认证提供者 / 差分引擎) +- 使用的 GeneralUpdate 版本: ______(v10.4.6 稳定版 / v10.5.0+ 开发分支) +- .NET 版本: ______(.NET 6/8/9/10) + +### Bowl(如果选择) +- 被监控进程名: ______ +- 工作模式: ______(Normal / Upgrade) +- 是否需要崩溃 Dump: ______(是/否) +- 备份目录路径: ______ + +### IPC 替换(如果选择) +- 替换方式: ______(NamedPipe / SharedMemory / 自定义) +- 目标平台: ______(Windows / Linux / macOS / 跨平台) +- 安全要求: ______(加密 / 签名 / 无额外安全) + +### AOT(如果选择) +- 当前剪裁警告: ______(有/无) +- 是否使用反射: ______(是/否) +- JSON 序列化需求: ______(有/无) +``` + +--- + ## 1. Pipeline 管道系统(v10.4.6 可用) GeneralUpdate 使用 Pipeline 管道模式处理更新包的校验、解压、补丁应用。 @@ -262,6 +289,49 @@ var result = GeneralDrivelution.InstallDriver(driverPath); --- +## ✅ 高级定制验证清单 + +### Bowl 崩溃守护 +- [ ] 只引用了 `GeneralUpdate.Bowl`(不单独引用 Core) +- [ ] `MonitorParameter` 的 `ProcessNameOrId` 与实际进程名匹配 +- [ ] `TargetPath` 设置为应用安装根目录,非子目录 +- [ ] `WorkModel` 根据场景选择 Correct(Normal/Upgrade) +- [ ] `FailDirectory` 有写入权限 +- [ ] Linux/macOS 无此功能(Bowl 仅 Windows) + +### Pipeline 定制 +- [ ] `PipelineContext` 中的 Key 名称使用字符串常量拼写正确("ZipFilePath", "Hash", "Format", "Encoding", "SourcePath", "PatchEnabled") +- [ ] 中间件注册顺序正确:Hash → Compress → Patch → Drivelution +- [ ] `Encoding` 设置为 `Encoding.UTF8` + +### AOT/NativeAOT +- [ ] 启用了 `true` +- [ ] 对反射路径添加了 `[DynamicDependency]` 或 `[RequiresUnreferencedCode]` +- [ ] 使用了内置的 `JsonSerializerContext` 子类(减少裁剪) +- [ ] 通过 `dotnet build` 无 AOT 裁剪警告 + +### IPC 替换 +- [ ] 替换方案在目标平台上可用(Linux 无 NamedPipe 服务端,但有客户端) +- [ ] 加密方案与 Client/Upgrade 两端一致 +- [ ] IPC 数据长度有上限保护(防止内存溢出) + +--- + +## ⚠️ 反模式清单(高级定制特有) + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **在 v10.4.6 稳定版上使用开发分支 API(IUpdateHooks 等)** | 编译失败 / 运行时 MissingMethodException | 检查 API 可用性表 | +| 2 | **PipeLineContext Key 拼写错误(如 ZipFilePath 写成 ZipFilePatch)** | Pipeline 运行异常,值未传递 | 使用类库公开的常量或文档中的 Key 名 | +| 3 | **Bowl 的 WorkModel 设为 Upgrade 但进程是主程序** | 监控逻辑错误 | Normal=主线进程,Upgrade=升级进程 | +| 4 | **Windows 上 IPC 使用默认加密密钥** | 加密可被破解(代码审计 #1) | 使用强密钥(≥ 32 字符) | +| 5 | **差分包生成时使用不同版本的源文件结构** | 补丁应用失败,文件找不到 | 源和目标版本的文件结构必须一致 | +| 6 | **AOT 项目中使用了大量反射且未标记 DynamicDependency** | 运行时 TypeLoadException / 被剪裁 | 使用源代码生成器或显式标记保留 | +| 7 | **Pipeline 中 PatchMiddleware 排在 CompressMiddleware 前面** | 未解压就试图打补丁 | 顺序必须是 Compress→Patch | +| 8 | **自定义 Strategy 直接操作 private 方法** | 下游版本更新后 API 兼容性破裂 | 通过受保护的抽象方法扩展 | + +--- + ## 相关技能 - `/generalupdate-init` — Bootstrap 配置 diff --git a/.claude/skills/generalupdate-init/SKILL.md b/.claude/skills/generalupdate-init/SKILL.md index b2034a8..510ec25 100644 --- a/.claude/skills/generalupdate-init/SKILL.md +++ b/.claude/skills/generalupdate-init/SKILL.md @@ -28,28 +28,69 @@ allowed-tools: "Bash, Read, Write, Edit, Glob, Grep, WebSearch" --- -## 工作流程 +## 📋 用户需求提取 + +在生成代码前,必须先提取以下信息。**不确定的必须追问:** ``` -1. 探测项目状态 - ├── 检查 .csproj → 目标框架、UI 类型 - └── 检查现有配置 → 已安装 NuGet?已有 manifest? - -2. 选择集成模式 - ├── [Minimal] (new Configinfo + SetConfig + LaunchAsync)— 推荐新用户 - ├── [Standard](Configinfo + 事件监听)— 需要精细控制 - └── [Scaffold](完整双项目结构)— 从零开始的团队项目 - -3. 生成输出 - ├── NuGet 安装命令 - ├── Bootstrap 配置代码 - ├── manifest.json 模板 - └── 部署检查清单 - -4. 后续引导 - ├── 需要 UI → generalupdate-ui - ├── 选择策略 → generalupdate-strategy - └── 遇到问题 → generalupdate-troubleshoot +### 项目状态 +- 现有项目类型: ______(新项目 / 已有项目 / 从旧版迁移) +- .NET 版本: ______ +- UI 框架: ______(WPF/WinForms/Avalonia/MAUI/控制台/无) +- 目标平台: ______(Windows/Linux/macOS/多平台) + +### 更新需求 +- 是否需要显示进度 UI: ______(是/否) +- 是否有后端服务: ______(是/否) +- 更新策略倾向: ______(标准/OSS/静默/差分/跨版本/推送) +- 是否需要崩溃守护 Bowl: ______(是/否) + +### 已有配置(如果存在) +- 是否已安装 NuGet: ______(是/否,版本号) +- 是否已有 Configinfo 配置: ______(是/否) +- 是否已有 manifest.json: ______(是/否) +``` + +--- + +## 工作流程(按顺序执行) + +### Step 1:探测项目状态 + +``` +├── 检查 .csproj → 目标框架、UI 类型、是否有 NuGet 引用 +├── 检查是否存在 generalupdate.manifest.json +├── 检查是否存在 Configinfo/Bootstrap 配置代码 +└── 检查项目结构 → 是否已有独立的 Upgrade 项目 +``` + +### Step 2:选择集成模式 + +基于需求提取结果,选择以下模式之一: + +| 模式 | 适用场景 | 产出 | +|------|---------|------| +| **[Minimal]** | 新用户快速上手,控制台/服务应用 | 3 行 Bootstrap 代码 | +| **[Standard]** | 需要精确控制更新过程 | Configinfo + 完整事件监听 | +| **[Scaffold]** | 团队项目,从零开始 | 完整 Client + Upgrade 双项目结构 | + +### Step 3:生成输出 + +``` +├── NuGet 安装命令(按平台选 Core/Bowl) +├── Bootstrap 配置代码(按模式) +├── manifest.json 模板 +├── 部署检查清单 +└── 已知问题预警(针对你的配置组合) +``` + +### Step 4:引导下一步 + +``` +├── 需要 UI → /generalupdate-ui +├── 选择策略 → /generalupdate-strategy +├── 需要 Bowl 守护 → /generalupdate-advanced +└── 遇到问题 → /generalupdate-troubleshoot ``` --- @@ -274,6 +315,50 @@ v10.4.6 无 `IUpdateHooks`、无可编程 `Option`、无静默轮询器。 --- +## ✅ 集成验证清单(交付前逐项检查) + +### Bootstrap 配置 +- [ ] `Configinfo` 的 6 个必填字段都已设置(UpdateUrl, AppSecretKey, AppName, MainAppName, ClientVersion, ProductId, InstallPath) +- [ ] `UpdateUrl` 指向的服务端 API 可正常返回版本信息 +- [ ] `AppSecretKey` 长度 ≥ 16 字符,与服务端一致 +- [ ] `AppType` 设置正确(Client = 1, Upgrade = 2) +- [ ] 生产环境使用 `AppDomain.CurrentDomain.BaseDirectory` 作为 InstallPath + +### NuGet & 编译 +- [ ] Client 和 Upgrade 项目使用**完全相同**的 GeneralUpdate NuGet 版本 +- [ ] 如果用 Bowl:项目中只能有 `GeneralUpdate.Bowl`,不能同时有 `GeneralUpdate.Core` +- [ ] 项目能正常 `dotnet build`(0 errors) +- [ ] 无需额外引用 `GeneralUpdate.Differential`(已嵌入 Core) + +### 部署结构 +- [ ] UpgradeApp.exe 存在于发布目录(首个版本就必须有) +- [ ] `generalupdate.manifest.json` 的 `UpdateAppName` 包含 `.exe` +- [ ] IPC 文件(`UpdateInfo.msg`)路径在 Client/Upgrade 间一致 +- [ ] `Encoding` 设置为 `Encoding.UTF8`(防止 Linux/macOS 中文乱码) + +### 迁移场景(从 v9.x 升级) +- [ ] 检查旧代码中是否有 `SetSource()` / `SetOption()` / `Hooks()` 等不存在的方法 +- [ ] `AppType` 原来是 enum 吗?v10.4.6 中是 class,`ClientApp = 1`, `UpgradeApp = 2` +- [ ] `LaunchAsync()` 在 v10.4.6 中返回 `Task`(不是 `Task`) +- [ ] 删除 `OssClient` 相关引用(v10.4.6 不支持) + +--- + +## ⚠️ 反模式清单 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **Core 和 Bowl 引用到同一个项目** | CS0433 类型冲突,编译失败 | 用 Bowl 时只引 Bowl(传递依赖 Core) | +| 2 | **Client/Upgrade NuGet 版本号不一致** | 运行时 MethodNotFoundException | 锁定完全相同版本 | +| 3 | **UpgradeApp.exe 不随首个版本发布** | 第一次更新时 FileNotFoundException | 首个版本就包含 UpgradeApp | +| 4 | **事件监听中做耗时操作(网络 IO / 磁盘 IO)** | Update 进程 UI 卡死,超时被 Kill | 仅更新 UI 状态,耗时操作异步 | +| 5 | **IPC 文件编码未设置 UTF-8** | Linux/macOS 中文乱码 | `Encoding.UTF8` | +| 6 | **版本号不是 4 段式(如 1.0.0.0)** | 版本比较逻辑异常 | 始终用 `x.y.z.w` 格式 | +| 7 | **manifest.json 的 mainAppName 不匹配真实进程名** | 更新后主程序找不到 | 和实际 exe 名称一致 | +| 8 | **为 v9.x 编写的代码直接用在 v10** | API 不兼容,编译失败 | 对照 v10.4.6 稳定版 API 重写 | + +--- + ## 相关技能 - `/generalupdate-ui` — UI 框架自动检测 + 更新窗口代码生成 diff --git a/.claude/skills/generalupdate-init/project-scaffold/ClientProgram.cs b/.claude/skills/generalupdate-init/project-scaffold/ClientProgram.cs index cd32615..7eff708 100644 --- a/.claude/skills/generalupdate-init/project-scaffold/ClientProgram.cs +++ b/.claude/skills/generalupdate-init/project-scaffold/ClientProgram.cs @@ -2,8 +2,16 @@ using GeneralUpdate.Common.Shared.Object; using GeneralUpdate.Common.Download; -string updateUrl = args.Length > 0 ? args[0] : "https://your-server.com/api"; -string secretKey = args.Length > 1 ? args[1] : "your-secret-key"; +// ===================================================== +// 使用说明 +// 1. 将此文件放入 Client 项目 +// 2. install GeneralUpdate.Core NuGet 包 +// 3. 按需修改 UpdateUrl 和 AppSecretKey +// 4. 确保 Upgrade 项目已存在并一同发布 +// ===================================================== + +string updateUrl = args.Length > 0 ? args[0] : "https://your-server.com/Upgrade/Verification"; +string secretKey = args.Length > 1 ? args[1] : "your-32-char-secret-key-here!"; Console.WriteLine($"[Client] 启动版本检查: {updateUrl}"); diff --git a/.claude/skills/generalupdate-init/templates/FullIntegration.cs b/.claude/skills/generalupdate-init/templates/FullIntegration.cs index 2422c9b..3818882 100644 --- a/.claude/skills/generalupdate-init/templates/FullIntegration.cs +++ b/.claude/skills/generalupdate-init/templates/FullIntegration.cs @@ -24,14 +24,14 @@ public static async Task RunAsync() var config = new Configinfo { // --- 必填 --- - UpdateUrl = "https://your-server.com/Upgrade/Verification", - AppSecretKey = "your-32-char-secret-key-here!", + UpdateUrl = "{{UPDATE_URL}}", + AppSecretKey = "{{APP_SECRET_KEY}}", // --- 应用信息 --- - AppName = "MyApp.exe", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0.0", // ⚠️ 4 段式 - ProductId = "my-product-001", + AppName = "{{PROJECT_NAME}}.exe", + MainAppName = "{{PROJECT_NAME}}.exe", + ClientVersion = "{{CLIENT_VERSION}}", // ⚠️ 4 段式 + ProductId = "{{PRODUCT_ID}}", InstallPath = AppDomain.CurrentDomain.BaseDirectory, // --- 可选 --- diff --git a/.claude/skills/generalupdate-init/templates/MinimalIntegration.cs b/.claude/skills/generalupdate-init/templates/MinimalIntegration.cs index c5c8a37..ac28a79 100644 --- a/.claude/skills/generalupdate-init/templates/MinimalIntegration.cs +++ b/.claude/skills/generalupdate-init/templates/MinimalIntegration.cs @@ -25,13 +25,13 @@ public static async Task RunAsync() // 1. 创建配置对象 var config = new Configinfo { - UpdateUrl = "https://your-server.com/api", - AppSecretKey = "your-32-char-secret-key-here!", - AppName = "MyApp.exe", - MainAppName = "MyApp.exe", - ClientVersion = "1.0.0.0", - ProductId = "my-product-001", - InstallPath = "." + UpdateUrl = "{{UPDATE_URL}}", + AppSecretKey = "{{APP_SECRET_KEY}}", + AppName = "{{PROJECT_NAME}}.exe", + MainAppName = "{{PROJECT_NAME}}.exe", + ClientVersion = "{{CLIENT_VERSION}}", + ProductId = "{{PRODUCT_ID}}", + InstallPath = "{{INSTALL_PATH}}" }; // 2. 启动更新 diff --git a/.claude/skills/generalupdate-init/templates/generalupdate.manifest.json b/.claude/skills/generalupdate-init/templates/generalupdate.manifest.json index 57b3b91..741a152 100644 --- a/.claude/skills/generalupdate-init/templates/generalupdate.manifest.json +++ b/.claude/skills/generalupdate-init/templates/generalupdate.manifest.json @@ -1,9 +1,9 @@ { - "MainAppName": "MyApp.exe", - "UpdateAppName": "UpgradeApp.exe", - "ProductId": "my-product-001", + "MainAppName": "{{PROJECT_NAME}}.exe", + "UpdateAppName": "Upgrade{{PROJECT_NAME}}.exe", + "ProductId": "{{PRODUCT_ID}}", "InstallPath": ".", "UpdatePath": "update", - "ClientVersion": "1.0.0.0", - "UpgradeClientVersion": "1.0.0.0" + "ClientVersion": "{{CLIENT_VERSION}}", + "UpgradeClientVersion": "{{CLIENT_VERSION}}" } diff --git a/.claude/skills/generalupdate-migration/SKILL.md b/.claude/skills/generalupdate-migration/SKILL.md new file mode 100644 index 0000000..8ce5c64 --- /dev/null +++ b/.claude/skills/generalupdate-migration/SKILL.md @@ -0,0 +1,145 @@ +--- +name: generalupdate-migration +description: | + Guide developers through migrating GeneralUpdate from older versions to the + latest stable API (v10.4.6). Covers v9.x → v10 and dev-branch (v10.5.0-beta.2) + → stable (v10.4.6) migration paths. Detects breaking API changes, deprecated + types, and provides automated migration scripts. + Triggers on: "migrate", "migration", "upgrade from v9", "upgrade from v10.5", + "迁移", "旧版本升级", "API 变更", "breaking changes", "不再兼容", + "v10.4.6", "v10.5.0", "开发分支", "稳定版迁移", + "IUpdateHooks not found", "SetSource not found", "OssClient missing", + "ProcessContract missing", "Option system". +when_to_use: | + - User has an existing GeneralUpdate integration and wants to upgrade to latest + - User reports compilation errors after updating NuGet package + - User's code uses v10.5.0+ dev-branch APIs (IUpdateHooks, SetSource, etc.) + - User is on v9.x and needs to migrate to the dual-process architecture + - User sees "missing method" or "type not found" after package update + - User asks about API compatibility between versions + - Run AFTER generalupdate-init if migration is the primary goal +allowed-tools: "Read, Write, Edit, Glob, Grep, Bash" +--- + +# 🔄 GeneralUpdate 迁移指南 + +帮助开发者从旧版本 GeneralUpdate 迁移到最新稳定版 API(v10.4.6)。 + +> ⚠️ **目标版本:NuGet v10.4.6 稳定版** +> 开发分支(v10.5.0-beta.2)API 与稳定版有根本性差异。 + +--- + +## 📋 迁移前需求提取 + +``` +### 当前状态 +- 当前 GeneralUpdate 版本: ______(v9.x / v10.0-10.3 / v10.5.0-beta.x / 不确定) +- 当前 .NET 版本: ______ +- UI 框架: ______ +- 是否使用了 Bowl: ______(是/否) +- 是否使用了 Differential: ______(是/否) + +### 迁移后目标 +- 目标版本: ______(v10.4.6 稳定版 / 继续用开发分支) +- 是否需要新的功能(Bowl/IPC 替换/AOT): ______ +``` + +--- + +## 迁移路径 + +### 路径 A:v9.x → v10.4.6 稳定版 + +这是最大的跳跃。v9.x 和 v10 的架构完全不同。 + +``` +v9.x (单进程, HttpClient 直连) + ↓ + Breaking Changes: + ├── 单进程 → 双进程架构(Client + Upgrade) + ├── HttpClient 直连 → GeneralSpacestation 服务端 + ├── 无 IPC → AES 加密 IPC 文件 + ├── 无 manifest.json → 必须携带 manifest + └── API 命名空间全部重命名 + ↓ +v10.4.6 (双进程, Configinfo + Bootstrap) +``` + +**迁移步骤:** + +```csharp +// ❌ v9.x 写法(不复存在) +// var updater = new GeneralUpdater("https://api/method"); +// updater.Start(); + +// ✅ v10.4.6 写法 +await new GeneralUpdateBootstrap() + .SetConfig(new Configinfo + { + UpdateUrl = "https://your-server.com/Upgrade/Verification", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = "." + }) + .LaunchAsync(); +``` + +| v9.x API | v10.4.6 对应 | 说明 | +|----------|-------------|------| +| `GeneralUpdater` | `GeneralUpdateBootstrap` | 完全重命名 | +| `SetApiUrl()` / `SetMethod()` | `Configinfo.UpdateUrl` | 统一到 Configinfo | +| `CheckUpdateAsync()` | `.LaunchAsync()` | 异步改为返回 Bootstrap 实例 | +| 单进程直接更新 | Client + Upgrade 双进程 | 必须创建独立 Upgrade 项目 | +| N/A | `generalupdate.manifest.json` | 必须随首发版本发布 | + +### 路径 B:v10.5.0-beta.x (开发分支) → v10.4.6 稳定版 + +如果你已经在用开发分支的 API(如 `IUpdateHooks`、`Option` 系统),回退到稳定版需要重写: + +| 开发分支 API (v10.5.0-beta.x) | 稳定版替代 (v10.4.6) | 处理方式 | +|-------------------------------|---------------------|---------| +| `new Option()` / `SetOption()` | 不存在 | 改用 `Configinfo` 属性直接设置 | +| `.Hooks()` / `IUpdateHooks` | 不存在 | 去除 Hooks 引用;在事件监听中做等价逻辑 | +| `.Strategy()` / `IStrategy` | 不存在 | 直接用内置策略;或手动调用 `AbstractStrategy` | +| `SilentPollOrchestrator` | 不存在 | 手动实现定时器 + 调用 Bootstrap | +| `ISslValidationPolicy` | 不存在 | 在 `HttpClientHandler` 层级配置 | +| `IProcessInfoProvider` / `ProcessContract` | 不存在 | 接受默认加密文件 IPC;无法替换 | +| `OssClient (AppType=3,4)` | 不存在 | 只使用 AppType=1(Client) 和 2(Upgrade) | +| 硬编码版本号 | `Configinfo.ClientVersion` | 建议使用 `Assembly.GetEntryAssembly()?.GetName()?.Version` | + +--- + +## 迁移验证清单 + +### 编译验证 +- [ ] `dotnet build` 无错误 +- [ ] 无 `MissingMethodException` 的风险(检查所有方法名是否存在于 v10.4.6) +- [ ] 无 `CS0433` 类型冲突(Core + Bowl 不同时引用) + +### 架构验证 +- [ ] 项目已拆分为 Client + Upgrade 两个独立项目 +- [ ] Upgrade 项目 `AppType = 2` +- [ ] Client 项目 `AppType = 1` +- [ ] `generalupdate.manifest.json` 存在且配置正确 + +### 运行验证 +- [ ] 版本检查 API 可正常返回 +- [ ] 下载后 Upgrade 进程可正常启动 +- [ ] 更新完成后主程序可正常重启 +- [ ] IPC 文件编码设为 `Encoding.UTF8` + +--- + +## ⚠️ 迁移反模式 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **直接在项目中替换 NuGet 版本不修改代码** | 大量编译错误 | 先清理旧 API 引用再升级 NuGet | +| 2 | **认为 v9.x 的配置对象就是 Configinfo** | Configinfo 属性名完全不同 | 对照文档重新写 Configinfo | +| 3 | **试图在 v10.4.6 中使用 dev-branch 的 API** | MissingMethodException | 检查 API 可用性表 | +| 4 | **迁移后不测试 Upgrade 进程** | 主程序能更新但 Upgrade 崩溃 | 两端都要测试 | +| 5 | **保留旧的 v9.x 引用不删除** | 类型冲突 | 清空 csproj 重新添加引用 | diff --git a/.claude/skills/generalupdate-security-audit/SKILL.md b/.claude/skills/generalupdate-security-audit/SKILL.md new file mode 100644 index 0000000..6326810 --- /dev/null +++ b/.claude/skills/generalupdate-security-audit/SKILL.md @@ -0,0 +1,131 @@ +--- +name: generalupdate-security-audit +description: | + Security audit guide for GeneralUpdate deployments. Covers IPC encryption, + AppSecretKey strength, HTTPS enforcement, cross-tenant isolation, ZipSlip + protection, and dependency vulnerability scanning. Generates audit report + with severity ratings and remediation steps. + Triggers on: "security", "audit", "安全审计", "安全审查", "漏洞", + "vulnerability", "penetration", "渗透测试", "encryption", "IPC encryption", + "AppSecretKey", "HTTPS", "cross-tenant", "ZipSlip", "路径穿越", + "hardcoded key", "硬编码", "密钥泄露", "信息泄露", "privilege", + "权限提升", "security review", "安全检查". +when_to_use: | + - User asks about security of their GeneralUpdate deployment + - User wants to audit their update pipeline for vulnerabilities + - User is deploying in a multi-tenant environment + - User's security team requires a compliance review + - User mentions penetration testing or security assessment + - User is using GeneralUpdate in a regulated industry (finance, healthcare, gov) + - Run AFTER generalupdate-init as a post-integration check +allowed-tools: "Read, Write, Edit, Glob, Grep" +--- + +# 🔒 GeneralUpdate 安全审计指南 + +全面覆盖 GeneralUpdate 部署的安全风险面。基于代码审计发现(17 CRITICAL/HIGH 项)和最佳实践。 + +--- + +## 📋 审计前信息收集 + +``` +### 部署环境 +- 部署模式: ______(内网 / 公网 / 混合) +- 租户模式: ______(单租户 / 多租户) +- 客户端数量: ______ +- 客户端操作系统: ______(Windows / Linux / macOS / 混合) + +### 服务端 +- 后端类型: ______(GeneralSpacestation / 自定义 / OSS) +- 传输协议: ______(HTTP / HTTPS) +- 认证方式: ______(Bearer / Basic / HMAC / 无) +- API 是否公开访问: ______(是 / 否,有网络隔离) + +### 客户端 +- GeneralUpdate 版本: ______ +- 是否使用 IPC: ______(是 / 否) +- 是否使用 Bowl: ______(是 / 否) +- 是否使用 Differential: ______(是 / 否) +``` + +--- + +## 安全审计矩阵 + +| # | 检查项 | 严重度 | 描述 | 修复措施 | +|---|--------|--------|------|---------| +| S01 | **AppSecretKey 强度** | 🔴 CRITICAL | 密钥长度不足、纯字母、与示例代码相同 | 使用 ≥ 32 字符,大小写+数字+符号的随机密钥 | +| S02 | **IPC 加密** | 🔴 CRITICAL | 默认 IPC 加密密钥硬编码在二进制中 | 确保 AppSecretKey 唯一且服务端/客户端一致 | +| S03 | **HTTPS 传输** | 🟠 HIGH | UpdateUrl 使用 HTTP 而非 HTTPS | 生产环境强制 HTTPS;配置 HSTS | +| S04 | **ZipSlip 路径穿越** | 🔴 CRITICAL | 解压 ZIP 时未验证 ../ 路径 | 验证压缩包条目路径是否在目标目录内 | +| S05 | **多租户隔离** | 🔴 CRITICAL | 服务端未按 ProductId 隔离租户 | 服务端添加租户身份验证中间件 | +| S06 | **事件日志泄露** | 🟡 MEDIUM | ExceptionEventArgs 日志可能包含敏感路径 | 脱敏后记录,过滤路径和密钥 | +| S07 | **差分包签名** | 🟠 HIGH | 差分补丁无数字签名验证 | 对更新包进行 Authenticode 签名 | +| S08 | **临时目录权限** | 🟡 MEDIUM | 临时解压目录权限可能过大 | 设置仅为当前用户可读写 | +| S09 | **OSS Bucket 权限** | 🟠 HIGH | 更新包存储 Bucket 设为公共读 | 设置为私有,使用预签名 URL | +| S10 | **依赖版本漏洞** | 🟡 MEDIUM | GeneralUpdate 及其依赖可能存在已知 CVE | 定期检查 NuGet 依赖安全公告 | +| S11 | **回滚攻击** | 🟠 HIGH | 攻击者可提交降级版本号强制安装旧版本 | 服务端校验版本号单调递增 | +| S12 | **下载完整性** | 🟠 HIGH | 下载的更新包无完整性校验 | 确保 Pipeline 包含 HashMiddleware | +| S13 | **Bowl 提权** | 🟡 MEDIUM | Bowl 崩溃守护以高权限运行可能被滥用 | 以最小必要权限运行 Bowl | +| S14 | **信息泄露通过 manifest** | 🔵 LOW | manifest.json 中的 ProductId、版本号可被枚举 | 非公开环境下不暴露 manifest 文件 | + +--- + +## 审计报告输出格式 + +完成审计后按以下格式输出: + +``` +## 🔒 GeneralUpdate 安全审计报告 + +### 概要 +- 项目: ______ +- 审计日期: ______ +- 总体评分: A/B/C/D/F +- 严重问题: ______ 个 +- 高风险: ______ 个 +- 中风险: ______ 个 +- 低风险: ______ 个 + +### 严重问题(必须立即修复) +- S01 AppSecretKey 强度: ⚠️ 当前密钥长度为 X,需要 ≥ 32 + 修复: ______ + +### 高风险(建议尽快修复) +... + +### 中风险(评估后修复) +... + +### 低风险(记录在案) +... + +### 修复建议优先级 +1. 立即:S01, S03, S04 +2. 本周:S05, S07, S09 +3. 本月:S08, S10, S11 +``` + +--- + +## 安全配置检查清单 + +- [ ] AppSecretKey 长度 ≥ 32 字符,混合大小写+数字+符号 +- [ ] 生产环境使用 HTTPS +- [ ] IPC 文件编码设为 Encoding.UTF8 +- [ ] Pipeline 包含 HashMiddleware 做完整性校验 +- [ ] OSS Bucket 权限设为私有 +- [ ] 服务端按 ProductId 隔离租户 +- [ ] 版本号严格单调递增 +- [ ] 更新包进行 Authenticode 签名 +- [ ] Zip 解压有路径穿越防护 +- [ ] 日志中不记录敏感信息 + +--- + +## 相关技能 + +- `/generalupdate-init` — 修复审计发现的问题 +- `/generalupdate-advanced` — IPC 替换、自定义认证 +- `/generalupdate-troubleshoot` — 已知安全问题参考 diff --git a/.claude/skills/generalupdate-strategy/SKILL.md b/.claude/skills/generalupdate-strategy/SKILL.md index a122594..1623652 100644 --- a/.claude/skills/generalupdate-strategy/SKILL.md +++ b/.claude/skills/generalupdate-strategy/SKILL.md @@ -29,16 +29,67 @@ allowed-tools: "Read, Write, Edit, Glob" --- -## 策略决策树 +## 📋 用户需求提取(推荐策略前必须确认) + +``` +### 部署环境 +- 是否有后端服务: ______(是/否/计划中) +- 服务端类型: ______(GeneralSpacestation / 自定义 API / S3/MinIO / 无) +- 客户端数量: ______(几十/几百/几千/万+) +- 客户端是否 7×24 运行: ______(是/否) + +### 更新需求 +- 是否需要节省带宽: ______(是/否 → 推荐差分) +- 是否需要跳过中间版本: ______(是/否 → 推荐 CVP) +- 是否需要服务端主动触发: ______(是/否 → 推荐 SignalR) +- 是否需要用户无感知: ______(是/否 → 推荐静默) +- 是否需要显示更新进度: ______(是/否 → 推荐标准 + UI) + +### 约束条件 +- 目标平台: ______(Windows/Linux/macOS/多平台) +- 网络环境: ______(内网/公网/离线) +- 是否需要崩溃恢复: ______(是/否 → 配合 Bowl) +``` + +--- + +## 策略决策树(详细版) ``` 你的应用有后端服务吗? ├── 有 -│ ├── 需要服务端主动推送更新? → SignalR 推送 -│ └── 否 → 标准客户端-服务端 +│ ├── 需要服务端主动推送更新? +│ │ └── YES → ⑥ SignalR 推送(需额外部署 SignalR Hub) +│ └── NO +│ ├── 需要节省下载带宽? +│ │ ├── YES → ④ 差分更新(生成补丁包,减少 60-90% 体积) +│ │ └── NO +│ │ ├── 需要跳过中间版本直达最新? +│ │ │ ├── YES → ⑤ 跨版本 CVP(需服务端额外构建) +│ │ │ └── NO +│ │ │ └── ① 标准客户端-服务端(推荐新手入门) +│ └── 需要后台无声升级? +│ └── YES → ③ 静默更新(基于标准或 OSS + 定时轮询) │ -└── 没有(只有对象存储 S3/MinIO/OSS) - └── OSS 标准 +└── 没有(只有对象存储 S3/MinIO) + ├── 需要节省带宽? + │ ├── YES → ④ 差分更新(OSS + 差分补丁,v10.4.6 支持有限) + │ └── NO + │ └── ② OSS 标准(最低成本,零服务端) + │ + └── 需要后台无声升级? + └── YES → ③ 静默更新(OSS + 定时检查) + +### 混合策略组合 + +常见组合方案: +| 场景 | 策略组合 | 说明 | +|------|---------|------| +| 标准 Web 应用 | ① 标准 + 🎨 UI | 有后端,显示进度 | +| 无服务端节省带宽 | ② OSS + ④ 差分 | 零服务端 + 增量更新 | +| 长期运行后台服务 | ③ 静默(基于 ① 或 ②) | 用户无感知 | +| 强制升级 | ⑤ CVP + ⑥ SignalR | 跳过旧版本,主动推送 | +| 企业级高可靠 | ① 标准 + Bowl + ③ 静默 | 完整链路 | ``` --- @@ -107,6 +158,55 @@ await new GeneralUpdateBootstrap() --- +## ✅ 策略选择验证清单 + +### 策略匹配度 +- [ ] 选定的策略与部署环境匹配(有后端→标准/无后端→OSS) +- [ ] 带宽需求与策略匹配(大文件→差分,版本多→CVP) +- [ ] 用户体验目标与策略匹配(需要交互→标准+UI,后台→静默) +- [ ] 平台兼容性确认(Linux/macOS 不支持 Bowl) + +### OSS 策略 +- [ ] Bucket 权限设置为私有 +- [ ] 更新包的 URL 可公开访问或使用预签名 URL +- [ ] Upgrade.exe 放在 `update/` 子目录(OSS 特有要求) +- [ ] 没有区分 Main/Upgrade 独立更新包(OSS 限制,接受) + +### 静默策略 +- [ ] 轮询间隔合理(建议 30-60 分钟,太短耗电/流量) +- [ ] 有"新版本可用"的系统通知或托盘图标提示 +- [ ] 下载完成后再通知用户重启,而非下载前 +- [ ] 后台下载有流量/电量优化(WiFi 下才下载大包) + +### SignalR 推送 +- [ ] HubConnection 的生命周期管理完善 +- [ ] 重连逻辑(自动重试 3 次,间隔递增) +- [ ] Dispose 时将 HubConnection 置 null(否则重连崩溃) +- [ ] 推送消息有超时保护和降级策略(推送失败→回退到轮询) + +### 差分策略 +- [ ] 服务端有差分包生成机制(`DifferentialCore.CleanAsync`) +- [ ] 客户端 Pipeline 配置了 PatchMiddleware +- [ ] 注意大文件差分可能触发的整数溢出(v10.4.6 已修复 #514) +- [ ] Linux/macOS 上 BSDIFF 补丁兼容性已验证 + +--- + +## ⚠️ 反模式清单 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **有后端却选 OSS** | 浪费后端服务能力,失去版本管理 | 有后端 → 标准策略 | +| 2 | **低频轮询(每天 1 次)** | 用户等很久才收到更新 | 静默模式 30-60 分钟轮询 | +| 3 | **高频轮询(每分钟 1 次)** | 浪费带宽和电池 | 静默模式建议 ≥ 30 分钟 | +| 4 | **SignalR 连接永不释放** | 内存泄漏 | 页面/应用关闭时 Dispose HubConnection | +| 5 | **差分包太大(> 2GB)** | 整数溢出导致进程崩溃(BSD-514) | 分多个版本发布,或用全量包 | +| 6 | **CVP 跳版本不测试中间版本 API 变更** | 客户端数据迁移失败 | 在服务端做好版本兼容测试 | +| 7 | **OSS 包名不包含版本号** | 客户端版本比较逻辑异常 | `MyApp_1.0.0.0.zip` 格式命名 | +| 8 | **静默更新后不通知用户重启** | 用户不知道新版本已下载 | 下载完成后通知 + 延迟重启选项 | + +--- + ## 相关技能 - `/generalupdate-init` — 如果还未配置 Bootstrap diff --git a/.claude/skills/generalupdate-troubleshoot/SKILL.md b/.claude/skills/generalupdate-troubleshoot/SKILL.md index 49feb0d..d7daa15 100644 --- a/.claude/skills/generalupdate-troubleshoot/SKILL.md +++ b/.claude/skills/generalupdate-troubleshoot/SKILL.md @@ -35,6 +35,29 @@ allowed-tools: "Read, Write, Edit, Glob, Grep, Bash" 综合性诊断系统 — 覆盖 50+ 已知问题,均可追溯到 GitHub/Gitee Issue 或代码审计发现。 +--- + +## 📋 用户症状提取(诊断前必须收集) + +``` +### 必填信息 +- 症状描述: ______ +- 错误信息/堆栈: ______ +- GeneralUpdate 版本: ______ +- 平台: ______(Windows / Linux / macOS) +- .NET 版本: ______ +- 更新策略: ______(标准 / OSS / 静默 / 差分 / 跨版本 / 推送) +- 最近是否改过配置: ______(是/否,改了啥) + +### 可选信息 +- 事件监听中是否有异常(ExceptionEventArgs): ______ +- 是否有日志(Logs/generalupdate-trace *.log): ______ +- 问题是否可复现: ______(是/否,频率) +- 首次出现时间点: ______ +``` + +--- + ## 工作流程 ``` @@ -45,9 +68,10 @@ allowed-tools: "Read, Write, Edit, Glob, Grep, Bash" ├── 平台(Windows/Linux/macOS)? └── 更新策略(标准/OSS/静默)? -2. 症状匹配 → 查找 `reference.md` - ├── 找到匹配的 Q/C/H/M/L → 给出根因 + 修复 + 代码 - └── 未找到匹配 → 执行通用诊断流程(6步骤) +2. 症状匹配 + ├── 优先:python3 scripts/search.py "<症状>" --domain issue + │ └── 匹配到 → 给出根因 + 修复 + 代码 + └── 未匹配 → 降级到 reference.md 全文搜索 3. 提供修复 ├── 具体的代码修改、配置调整、版本升级建议 @@ -57,6 +81,20 @@ allowed-tools: "Read, Write, Edit, Glob, Grep, Bash" └── 确认修复后问题解决 ``` +## 症状搜索(推荐) + +优先使用 BM25 搜索引擎精确匹配已知问题,而不是在 reference.md 中手动查找: + +```bash +# 自然语言搜索已知问题 +python3 skills/generalupdate-troubleshoot/scripts/search.py "升级后应用启动不了" --domain issue +python3 skills/generalupdate-troubleshoot/scripts/search.py "方法找不到 MethodNotFound" --domain issue +python3 skills/generalupdate-troubleshoot/scripts/search.py "中文乱码 garbled" --domain issue + +# 搜索策略相关问题 +python3 skills/generalupdate-troubleshoot/scripts/search.py "OSS 权限问题" --domain strategy +``` + ## 症状分级 reference.md 中的问题按严重度分级: @@ -69,3 +107,46 @@ reference.md 中的问题按严重度分级: | L | 🔵 **低** | 代码气味、边缘情况、已知行为 | 12 | **完整清单请查阅 `reference.md`** + +--- + +## ✅ 通用诊断前检查清单 + +在深入诊断前,先快速排查最常见的原因: + +### 运行环境检查 +- [ ] 目标机器安装了正确的 .NET 运行时(版本与发布框架匹配) +- [ ] 目标机器上有写入权限(InstallPath 目录可写) +- [ ] 防火墙未阻断 UpdateUrl 的通信端口 +- [ ] 磁盘空间充足(至少 2× 更新包大小) +- [ ] Linux/macOS:UpgradeApp 有 `chmod +x` 执行权限 + +### 版本检查 +- [ ] Client 和 Upgrade 项目 NuGet 版本**完全一致** +- [ ] 服务端返回的版本号是 4 段式(如 1.0.0.0) +- [ ] manifest.json 中 `mainAppName` 与实际进程名匹配 +- [ ] `AppType` 设置正确(Client = 1, Upgrade = 2) + +### 配置检查 +- [ ] `Configinfo` 的 6 个必填字段都已设置 +- [ ] `UpdateUrl` 可通过 HTTP GET 访问并返回合法 JSON +- [ ] `AppSecretKey` 与服务端配置一致(长度 ≥ 16 字符) +- [ ] UpgradeApp.exe 存在于发布目录的 `update/` 子目录中 + +### 日志检查 +- [ ] 查看 `Logs/generalupdate-trace-*.log`(如有) +- [ ] 检查事件监听中的 `ExceptionEventArgs` +- [ ] 检查 `MultiDownloadErrorEventArgs` 中的异常 + +--- + +## ⚠️ 诊断阶段的反模式 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **只看错误信息不看事件** | 错过 ExceptionEventArgs 中的详细信息 | 订阅所有 6 个事件 | +| 2 | **日志文件路径不对就认为无日志** | 漏掉关键诊断信息 | 在 InstallPath/Logs 下查找 | +| 3 | **只检查 Client 不检查 Upgrade 进程** | 问题在 Upgrade 端但诊断方向全错 | 两端都要检查 | +| 4 | **升级问题直接改代码** | 可能是服务端配置问题而非客户端 Bug | 优先检查服务端返回的版本信息 | +| 5 | **忽略 NuGet 版本一致性** | 方向错,"Method not found" 根因是版本不一致 | 第一个就要检查版本 | +| 6 | **只在 Debug 环境测试** | Release 环境可能缺少运行时文件 | 在发布/生产环境复现 diff --git a/.claude/skills/generalupdate-troubleshoot/data/issues.csv b/.claude/skills/generalupdate-troubleshoot/data/issues.csv new file mode 100644 index 0000000..c814db2 --- /dev/null +++ b/.claude/skills/generalupdate-troubleshoot/data/issues.csv @@ -0,0 +1,52 @@ +id,severity,symptom_en,symptom_zh,cause,solution,code_ref,workaround,keywords +C1,C,"Upgrade process not started / FileNotFoundException: upgrade application not found","升级进程没启动 / FileNotFoundException","UpgradeApp.exe not shipped with main application in first release","Ensure UpgradeApp.exe is included in the publish directory from the first release","Configinfo.UpdatePath, Configinfo.UpdateAppName","OSS mode: place Upgrade.exe in update/ subdirectory","upgrade not start, file not found, 升级没启动, FileNotFoundException, missing upgrade" +C2,C,"Method not found exception at runtime","运行时 Method not found 异常","Client and Upgrade projects use different GeneralUpdate NuGet versions","Lock Client.csproj and Upgrade.csproj to the exact same NuGet version","GeneralUpdate.Core, .csproj","Pin version in Directory.Build.props to enforce consistency","method not found, MissingMethodException, 方法找不到, NuGet conflict, version mismatch" +C3,C,"BSOD / OutOfMemory / Process crash on differential patch","蓝屏/内存溢出/进程崩溃差分补丁","BsdiffDiffer.WriteInt64 overflows on long.MinValue negation; control value > int.MaxValue truncates to negative","Update to v10.4.6+ (#514 fixed). If cannot update: add MaxInputFileSize limit in differential engine","BsdiffDiffer, PatchMiddleware","Limit patch file size at application level","BSOD, crash, OOM, differential, patch, 蓝屏, 崩溃, 内存溢出, BSDIFF" +C4,C,"PathTooLongException during backup (recursive nesting)","备份递归嵌套 PathTooLongException","StorageManager.Backup() creates backup dir INSIDE install path; empty SkipDirectorys list doesn't trigger default skip logic","Set SkipDirectorys on Configinfo to exclude backup directories","Configinfo.SkipDirectorys, StorageManager.Backup","Set SkipDirectorys to exclude '.backups','backup-'","path too long, backup recursion, PathTooLongException, 路径过长, 备份嵌套" +C5,C,"IPC encryption key hardcoded / weak","IPC 加密密钥硬编码/弱密钥","Default IPC encryption key is a hardcoded string; AppSecretKey is reused as encryption key","Use strong AppSecretKey (>= 32 chars, mixed case + numbers + symbols)","Configinfo.AppSecretKey, ProcessInfoJsonContext","Rotate key periodically, audit encryption in IPC path","IPC encryption, hardcoded key, 加密, 硬编码, security, AppSecretKey" +C6,C,"ZipSlip / path traversal in decompression","ZIP 解压路径穿越漏洞","CompressMiddleware.Extract doesn't validate ../ in zip entry paths against a whitelist before extraction","Validate zip entry paths against target directory; reject entries with ../","CompressMiddleware, ZipMiddleware","Scan entry names before extraction for path traversal patterns","zip slip, path traversal, security, 解压漏洞, 目录穿越" +C7,C,"Cross-tenant version leakage in multi-tenant deployment","多租户部署中跨租户版本泄露","Server API returns version info without tenant isolation; ProductId not validated against tenant context","Update server to validate ProductId against tenant context; add tenant-scoped API keys","GeneralSpacestation, ProductId","Add tenant header validation middleware on server side","multi-tenant, cross-tenant, security, 多租户, 跨租户, ProductId" +C8,C,"EventManager.Instance returned after Dispose","EventManager.Dispose 后 Instance 仍可访问","EventManager.Instance property returns the instance even after Dispose() is called","Call Clear() first, then Dispose(); do not call Instance after Dispose","EventManager.Instance, EventManager.Dispose","Wrap in try-finally; set to null after dispose","EventManager, dispose, singleton, memory leak, after dispose" +H1,H,"Chinese text garbled on Linux/macOS","Linux/macOS 中文乱码","IPC file encoding defaults to system default, not UTF-8","Set Encoding.UTF8 in PipelineContext and Configinfo","PipelineContext Encoding, Configinfo","Always explicitly set Encoding.UTF8","Chinese garbled, encoding, UTF8, 乱码, 编码, Linux, macOS" +H2,H,"Infinite update loop","无限升级循环","manifest.json version number doesn't match actual installed version or write-back fails","Ensure manifest.json version number is correct; implement write-back after update","generalupdate.manifest.json, ClientVersion","Add version write-back logic after successful update","infinite loop, 死循环, version mismatch, manifest" +H3,H,"MultiDownloadCompletedEventArgs.IsComplated typo causes binding failure","MultiDownloadCompletedEventArgs.IsComplated 拼写错误导致绑定失败","v10.4.6 API has typo IsComplated (not IsCompleted). UI bindings using correct spelling get null","Use IsComplated in code; or write a wrapper property that redirects to IsComplated","MultiDownloadCompletedEventArgs","Wrap in adapter class with IsCompleted alias","IsComplated, IsCompleted, typo, spelling, binding, 拼写错误" +H4,H,"OSS mode doesn't distinguish Main vs Upgrade update packages","OSS 模式不区分 Main/Upgrade 更新包","OSS strategy treats all packages the same; no MainOnly/UpgradeOnly differentiation","Accept this behavior for OSS; upgrade to standard server strategy if you need fine-grained control","OSSStrategy","Use standard server strategy if Main/Upgrade differentiation is needed","OSS, MainOnly, UpgradeOnly, 不区分, OSS 策略" +H5,H,"Upgrade.exe must be in update/ subdirectory for OSS","OSS 模式下 Upgrade.exe 必须在 update/ 子目录","OSS strategy scans update/ directory; placing Upgrade.exe elsewhere won't be found","Place Upgrade.exe in update/ subdirectory from the first release","OSSStrategy, UpdatePath","Verify directory structure before first OSS deployment","OSS, update directory, subdirectory, 子目录, OSS 部署" +H6,H,"EventManager concurrency race on add/remove/dispatch","EventManager 并发竞争问题","EventManager uses List<> not concurrent collection; add/remove during dispatch causes race","Use lock or ConcurrentBag for listener storage","EventManager.cs","Avoid modifying listeners while dispatch is in progress","concurrency, EventManager, thread safe, 并发, 线程安全" +H7,H,"Container (IoC) disposed entry can cause ObjectDisposedException","容器释放后访问导致 ObjectDisposedException","AutoFac container holds singleton references; after disposal access to Instance throws","Check container.IsDisposed before accessing Instance; add null check wrapper","GeneralUpdateBootstrap, DI container","Wrap container access in try-catch (ObjectDisposedException)","container, disposed, ObjectDisposedException, IoC, AutoFac" +H8,H,"Cross-version (CVP) jump skips API compatibility checks","跨版本跳转跳过 API 兼容性检查","CVP strategy allows jumping arbitrary number of versions; no intermediate API compatibility validation","Ensure server-side compatibility validation between source and target versions","CrossVersionStrategy, CVP","Test API compatibility for each version pair before deployment","CVP, cross version, API compatibility, 跨版本, API 兼容" +H9,H,"Linux: Pipeline hash algorithm platform-specific differences","Linux 上哈希算法平台差异","HashMiddleware may use platform-specific crypto implementations; hash mismatch between build and deploy","Use cross-platform hash algorithm (SHA256 consistently)","HashMiddleware, HashAlgorithm","Set HashAlgorithmName explicitly to SHA256","Linux, hash, SHA256, 哈希, 平台差异" +H10,H,"SignalR HubConnection dispose-then-reconnect crash","SignalR HubConnection 释放后重连崩溃","HubConnection.Dispose() sets internal state; reconnecting without new instance crashes","Set HubConnection to null after Dispose; create new instance for reconnect","SignalR, HubConnection, Dispose","Always null-check before reconnecting","SignalR, HubConnection, reconnect, 重连, 推送" +H11,H,"Bowl v10.4.6 has no public LaunchAsync method","Bowl v10.4.6 无公开 LaunchAsync 方法","v10.4.6 Bowl class only provides base type definitions; LaunchAsync is not publicly exposed","Use Bowl with v10.5.0+ dev branch; in v10.4.6 only basic type definitions are available","GeneralUpdate.Bowl.Bowl, MonitorParameter","Bowl parameters can be configured but execution requires dev branch","Bowl, LaunchAsync, 崩溃守护, 守护进程" +M1,M,"Differential package referenced unnecessarily","不必要地引用了 Differential 包","Developers add GeneralUpdate.Differential package not knowing types are already in Core","Don't add GeneralUpdate.Differential separately; the types are embedded in GeneralUpdate.Core","GeneralUpdate.Core, GeneralUpdate.Differential","Remove any existing GeneralUpdate.Differential reference from csproj","Differential, NuGet, 差分, 额外引用" +M2,M,"Wrong EventArgs type used in event listeners","事件监听到用了错误的 EventArgs 类型","Using ProgressEventArgs instead of MultiDownloadStatisticsEventArgs or vice versa","Use exact EventArgs type from GeneralUpdate.Common.Download namespace","MultiDownloadStatisticsEventArgs, ProgressEventArgs","Always check the namespace before importing event args types","event args, EventArgs, 事件参数, 错误类型" +M3,M,"Missing AddListenerException leads to silent failures","未订阅 ExceptionEventArgs 导致静默失败","Without AddListenerException, non-critical exceptions are silently swallowed","Always add AddListenerException handler; at minimum log the exception message","AddListenerException, ExceptionEventArgs","Add a default handler that logs to file even in production","silent failure, exception, 静默失败, 未订阅" +M4,M,"Version number not 4-segment (x.y.z.w)","版本号不是 4 段式","Server or client uses 3-segment version (1.0.0); comparison logic expects 4 segments","Always use 4-segment version format: x.y.z.w","ClientVersion, VersionInfo","Pad to 4 segments at server response level","version, 4 segment, 版本号, 三段式" +M5,M,"InstallPath set to relative path causes file resolution failure","InstallPath 使用相对路径导致文件解析失败","Relative InstallPath resolved differently in Client vs Upgrade context","Use absolute path: AppDomain.CurrentDomain.BaseDirectory for production","Configinfo.InstallPath","Use Path.GetFullPath() to resolve at runtime","InstallPath, relative path, 相对路径, 安装路径" +M6,M,"UpdateUrl returns success but empty body causes null reference","UpdateUrl 返回成功但空响应体导致空引用","Server API returns 200 OK with null/empty body; bootstrap doesn't handle null Info.Body","Add null check on e.Info?.Body before iterating VersionInfo list","UpdateInfoEventArgs, VersionInfo","Wrap version list access in null-conditional operator","null reference, empty response, 空响应, 空引用, body null" +M7,M,"Version comparison uses string instead of semantic version","版本比较使用字符串而非语义版本","Version strings compared lexicographically (1.9.0.0 > 1.10.0.0)","Use Version class (System.Version) or a semantic version parser for comparison","ClientVersion, Version.Parse","Cast to System.Version before comparison","version compare, semantic version, 版本比较, 语义版本" +M8,M,"BlackFiles/BlackFormats patterns not applied to upgrade directory","黑名单模式未应用到升级目录","BlackFiles patterns only apply to main app directory; upgrade path files not filtered","Apply same blacklist logic to upgrade directory; or use separate upgrade-specific blacklist","Configinfo.BlackFiles, Configinfo.BlackFormats","Duplicate blacklist entries for both main and upgrade paths","blacklist, BlackFiles, 黑名单, 升级目录" +M9,M,"Update process killed due to watchdog timeout on slow operations","慢操作导致升级进程被监控超时杀死","Upgrade process has a hard timeout; large file operations exceed the limit","Increase timeout for large updates; show progress to watchdog; split large operations","GeneralUpdateBootstrap, Watchdog","Show periodic progress to prevent watchdog timeout","timeout, watchdog, killed, 超时, 被杀" +M10,M,"Single-process mode not supported in v10.4.6","v10.4.6 不支持单进程模式","Developers expect to run update in same process; v10.4.6 requires dual-process architecture","Accept dual-process requirement; document that single-process is not supported","GeneralUpdateBootstrap, AppType","Not supported in v10.4.6; consider v10.5.0+ for single-process","single process, 单进程, dual process, 双进程" +M11,M,"SignalR push update delivery not acknowledged","SignalR 推送更新无送达确认","Push notification sent but no acknowledgment; client may miss the update while offline","Implement client-side acknowledgment; use retry queue on server for unacknowledged pushes","SignalRStrategy, HubConnection","Add ACK from client; server retries unacknowledged pushes","SignalR, acknowledgment, 推送确认, 送达" +M12,M,"Pre-release version included in production update list","预发布版本被包含在生产更新列表中","Server returns pre-release/beta versions in the production version list; client updates to wrong version","Filter versions by release channel; separate pre-release and production version lists","GeneralSpacestation, VersionInfo","Add release channel field to version metadata","pre-release, beta, 预发布, 测试版本" +M13,M,"OssClient.AppType value 3-4 not supported in v10.4.6","v10.4.6 不支持 OssClient.AppType(值 3-4)","v10.4.6 only supports AppType values 1 (Client) and 2 (Upgrade); 3-4 are dev-branch only","Use AppType.ClientApp (1) or AppType.UpgradeApp (2) only","AppType, ClientApp, UpgradeApp","Avoid values 3-4 in v10.4.6","OssClient, AppType, 3-4, not supported, 不支持" +M14,M,"ProgressEventArgs used in v10.4.6 API where MultiDownload* expected","ProgressEventArgs 与 MultiDownload* 混淆","v10.4.6 uses MultiDownload* events for batch downloads; ProgressEventArgs is for single download only","Use MultiDownloadStatisticsEventArgs for batch download; ProgressEventArgs for single file","MultiDownloadStatisticsEventArgs, ProgressEventArgs","Check event type before using in v10.4.6","progress, event, 进度事件, 混淆" +M15,M,"Custom pipeline middleware order wrong causes silent failure","自定义中间件顺序错误导致静默失败","Middleware registered in wrong order (Patch before Compress); no error but update silently broken","Follow correct order: Hash -> Compress -> Patch -> Drivelution","PipelineBuilder, UseMiddleware","Document and verify middleware order","pipeline, middleware order, 管道, 中间件顺序" +M16,M,"ConfigurationProviderFactory.Providers dictionary not thread-safe","ConfigurationProviderFactory.Providers 字典非线程安全","Dictionary used without synchronization; concurrent access in multi-threaded context may corrupt","Use ConcurrentDictionary or external lock for Providers access","ConfigurationProviderFactory","Replace Dictionary with ConcurrentDictionary","thread safety, ConfigurationProviderFactory, 线程安全" +M17,M,"Socket/HttpClient not disposed after download completes","下载完成后 Socket/HttpClient 未释放","HttpClient instances created per request without reuse or disposal","Use singleton HttpClient; or wrap in using block","MultiDownloadExecutor, HttpClient","Configure HttpClient as singleton in DI container","HttpClient, socket leak, 连接泄漏, 未释放" +M18,M,"LaunchAsync() returns Task not Task","LaunchAsync() 返回 Task 而非 Task","Developers expect bool return type for success/failure check; actual return is Bootstrap instance","The returned Bootstrap instance can be inspected; do not expect bool","GeneralUpdateBootstrap.LaunchAsync","Ignore return value; use event listeners for status","LaunchAsync, return type, Task, 返回值" +M19,M,"Silent mode notifications don't respect Do Not Disturb settings","静默模式通知不遵守免打扰设置","Silent update notifications pop up even when system is in DND or presentation mode","Check system DND status before showing notifications; queue notifications for later","SilentStrategy, Notification","Use OS-level DND API before showing notification","silent, notification, DND, 通知, 免打扰" +M20,M,"Service mode (Windows Service) IPC path resolution difference","Windows 服务模式下 IPC 路径解析差异","Windows Services run with different working directory than user apps; IPC file path resolved incorrectly","Use absolute paths for IPC files; ensure service account has write access to IPC directory","IPC, ProcessInfoJsonContext","Always use full absolute path for IPC file","Windows Service, IPC, path, 服务, 路径" +L1,L,"Thread.Sleep in event listener blocks update pipeline","事件监听中使用 Thread.Sleep 阻塞更新管道","Synchronous waits in event handlers block the pipeline execution","Use async event handlers; avoid Thread.Sleep/Task.Wait in any event listener","AddListenerException, AddListenerMultiDownloadStatistics","Replace Sleep with Task.Delay and async pattern","block, sleep, 阻塞, 事件监听" +L2,L,"Hardcoded temporary path in unzip operations","解压操作中硬编码临时路径","CompressMiddleware uses hardcoded temp path; conflicts with system temp policy","Use Path.GetTempPath() or configurable temp directory","CompressMiddleware, ZipMiddleware","Override via environment variable if needed","temp path, hardcoded, 临时路径, 硬编码" +L3,L,"Differential clean/dirty parameter validation missing","差分 clean/dirty 参数缺失验证","DifferentialCore.CleanAsync/Core.DirtyAsync doesn't validate input paths","Validate source/target/patch directories exist before calling","DifferentialCore.CleanAsync, DifferentialCore.DirtyAsync","Add manual directory existence checks","differential, parameter validation, 差分, 参数校验" +L4,L,"ServiceCollection registration not validated before Build","Build 前 ServiceCollection 注册未验证","Multiple registrations of same type; last wins silently","Use TryAdd instead of Add for service registration to detect duplicates","GeneralUpdateBootstrap, ServiceCollection","Replace Add with TryAdd patterns","DI, registration, duplicate, ServiceCollection" +L5,L,"Process memory tracking via private bytes not working-memory","进程内存跟踪使用 private bytes 而非 working set","Private bytes doesn't reflect actual memory pressure; working set is more meaningful for GC pressure","Use WorkingSet64 instead of PrivateMemorySize64 for memory monitoring","ProcessMonitor, MemoryMetrics","Switch to WorkingSet64 for memory threshold monitoring","memory, private bytes, working set, 内存跟踪" +L6,L,"Drivelution middleware exception loses context","Drivelution 中间件异常丢失上下文","Driver installation failure exception is caught without original stack trace or driver name","Wrap exception with driver name and operation context before rethrowing","DrivelutionMiddleware, GeneralDrivelution","Log driver name and operation at entry point","Drivelution, exception, 驱动, 异常" +L7,L,"OSS container/region info hardcoded in example code","OSS 示例代码中硬编码了容器/区域信息","Sample code uses hardcoded OSS endpoint/bucket values; needs env-based configuration","Use environment variables or config file for OSS endpoint and bucket","OSSStrategy, OssSample","Extract OSS config to environment variables","OSS, hardcoded, 硬编码, 示例, endpoint" +L8,L,"SkipDirectorys empty list doesn't use defaults","SkipDirectorys 空列表不使用默认值","Empty SkipDirectorys list skips nothing; default skip patterns not applied","Set SkipDirectorys explicitly even if using default patterns","Configinfo.SkipDirectorys","Always set SkipDirectorys with at least basic patterns","SkipDirectorys, empty list, 跳过目录, 空列表" +L9,L,"Bowl process name may match multiple processes","Bowl 进程名可能匹配多个进程","ProcessNameOrId uses string match; multiple processes with same name monitored instead of one","Use PID instead of process name for Bowl monitoring, or use full path","Bowl, MonitorParameter, ProcessNameOrId","Use exact process name with extension (MyApp.exe) to narrow match","Bowl, process name, multiple, 多个进程" +L10,L,"Linux: missing chmod +x on downloaded UpgradeApp","Linux 上下载的 UpgradeApp 缺少执行权限","File permissions not set after download; no IUpdateHooks in v10.4.6 to fix it","Manually chmod +x after download, or use post-update script","GeneralUpdateBootstrap, LinuxStrategy","Add chmod command to deployment script","Linux, chmod, permission, 权限, 执行" +L11,L,"WinForms: ShowDialog in update event blocks IPC","WinForms ShowDialog 阻塞 IPC 通信","Modal dialog shown during update event blocks the IPC writing thread","Use non-modal forms or async dialog patterns","WinForms, IPC","Replace ShowDialog with Show + event-based close","WinForms, ShowDialog, IPC, 模态框" +L12,L,"VersionRespDTO nullable fields cause null warnings","VersionRespDTO 可空字段导致空警告","Some fields in VersionRespDTO are nullable; no null handling in consuming code","Add null coalescing operators (??) when accessing VersionRespDTO properties","VersionRespDTO, VersionInfo","Use pattern matching before accessing nullable fields","nullable, 可空, null warning, VersionInfo" diff --git a/.claude/skills/generalupdate-troubleshoot/data/strategies.csv b/.claude/skills/generalupdate-troubleshoot/data/strategies.csv new file mode 100644 index 0000000..8529aaf --- /dev/null +++ b/.claude/skills/generalupdate-troubleshoot/data/strategies.csv @@ -0,0 +1,7 @@ +id,name,description,server_required,best_for,pros,cons,keywords +S01,Standard Client-Server,"Standard dual-process update with GeneralSpacestation backend",yes,"First-time users, enterprise apps with backend","Full control over version management; fine-grained Main/Upgrade differentiation; supports all event types","Requires GeneralSpacestation or compatible backend; higher deployment complexity","standard, client-server, GeneralSpacestation, 标准, 客户端-服务端" +S02,OSS Object Storage,"Update via S3/MinIO/cloud object storage; no backend server needed",no,"Small projects, startups, no-backend scenarios, cost-sensitive","Zero server cost; simple deployment; global CDN support; scales automatically","No Main/Upgrade differentiation; Upgrade.exe must be in update/ subdirectory; no real-time version control","OSS, S3, MinIO, 对象存储, 无服务器" +S03,Silent Update,"Background polling update with minimal user interruption",yes,"Long-running apps, kiosk systems, background services","User-unaware updates; configurable poll interval; can pair with any strategy","Requires notification strategy; poll interval tuning; no user feedback loop","silent, background, polling, 静默, 后台, 轮询" +S04,Differential Update,"Delta patch update to save bandwidth (BSDIFF/HDiffPatch)",yes,"Large applications, bandwidth-constrained networks, frequent updates","60-90% bandwidth reduction; quick patch application; works over any transport","Requires differential build pipeline on server; patch size limited (avoid >2GB); BSDIFF integer overflow risk in older versions","differential, delta, patch, BSDIFF, HDiffPatch, 差分, 增量" +S05,Cross-Version CVP,"Skip intermediate versions and jump directly to target version",yes,"Skip multiple versions, forced updates, long-unupdated clients","Bypass intermediate versions; reduce update steps; force minimum version compliance","Requires CVP build pipeline; API compatibility risk for large jumps; full compatibility testing needed","CVP, cross version, skip, 跨版本, 跳版本" +S06,SignalR Push,"Server-initiated push update via SignalR real-time connection",yes,"Real-time apps, urgent updates, managed fleets","Instant update notification; server-controlled rollout; targeted version deployment","Requires persistent connection; offline clients miss pushes; HubConnection lifecycle management; disposal/reconnect complexity","SignalR, push, real-time, 推送, 实时" diff --git a/.claude/skills/generalupdate-troubleshoot/scripts/core.py b/.claude/skills/generalupdate-troubleshoot/scripts/core.py new file mode 100644 index 0000000..8b7140d --- /dev/null +++ b/.claude/skills/generalupdate-troubleshoot/scripts/core.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GeneralUpdate Search Core - BM25 search engine for troubleshooting issues and strategies. +""" +import csv +import re +import os +from pathlib import Path +from math import log +from collections import defaultdict + +# Allow DATA_DIR override via environment variable (for testing) +_default_data_dir = Path(__file__).resolve().parent.parent / "data" +DATA_DIR = Path(os.environ.get("GENERALUPDATE_DATA_DIR", str(_default_data_dir))) +MAX_RESULTS = 3 + +CSV_CONFIG = { + "issue": { + "file": "issues.csv", + "search_cols": ["symptom_en", "symptom_zh", "cause", "keywords"], + "output_cols": ["id", "severity", "symptom_en", "symptom_zh", "cause", "solution", "code_ref", "workaround"], + }, + "strategy": { + "file": "strategies.csv", + "search_cols": ["name", "description", "best_for", "keywords"], + "output_cols": ["id", "name", "description", "server_required", "best_for", "pros", "cons", "keywords"], + }, +} + +class BM25: + """BM25 ranking algorithm for text search""" + def __init__(self, k1=1.5, b=0.75): + self.k1 = k1 + self.b = b + self.corpus = [] + self.doc_lengths = [] + self.avgdl = 0 + self.idf = {} + self.doc_freqs = defaultdict(int) + self.N = 0 + + def tokenize(self, text): + """Tokenize text: split CJK into character unigrams + bigrams, keep English words.""" + text = str(text).lower() + result = [] + + # Split into CJK and non-CJK segments + segments = re.split(r'([一-鿿㐀-䶿]+)', text) + for seg in segments: + if not seg: + continue + if re.match(r'^[一-鿿㐀-䶿]+$', seg): + # CJK: char unigrams + bigrams + chars = list(seg) + # Unigrams + result.extend(chars) + # Bigrams + for i in range(len(chars) - 1): + result.append(chars[i] + chars[i+1]) + else: + # English/alphanumeric: clean punctuation, keep words + cleaned = re.sub(r'[^\w\s]', ' ', seg) + for w in cleaned.split(): + w = w.strip() + if len(w) > 1 and not w.isdigit(): + result.append(w) + + return result + + def fit(self, corpus): + self.corpus = corpus + self.doc_lengths = [len(doc) for doc in corpus] + self.avgdl = sum(self.doc_lengths) / len(corpus) if corpus else 0 + self.N = len(corpus) + + for doc in corpus: + seen = set() + for term in doc: + if term not in seen: + self.doc_freqs[term] += 1 + seen.add(term) + + for term, df in self.doc_freqs.items(): + self.idf[term] = log((self.N - df + 0.5) / (df + 0.5) + 1.0) + + def score(self, query_terms, doc_idx): + doc = self.corpus[doc_idx] + doc_len = self.doc_lengths[doc_idx] + score = 0.0 + for term in query_terms: + if term in self.idf: + tf = doc.count(term) + score += self.idf[term] * (tf * (self.k1 + 1)) / (tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)) + return score + + def search(self, query, top_n=MAX_RESULTS): + query_terms = self.tokenize(query) + if not query_terms or not self.corpus: + return [] + scores = [(i, self.score(query_terms, i)) for i in range(len(self.corpus))] + scores.sort(key=lambda x: x[1], reverse=True) + return [(idx, score) for idx, score in scores[:top_n] if score > 0] + + +def load_csv(filepath): + """Load CSV file and return list of dicts.""" + full_path = DATA_DIR / filepath + if not full_path.exists(): + return [] + with open(full_path, 'r', encoding='utf-8') as f: + return list(csv.DictReader(f)) + + +def search(query, domain="issue", max_results=MAX_RESULTS): + """Search across issues or strategies domain.""" + if domain not in CSV_CONFIG: + return {"error": f"Unknown domain: {domain}. Available: {list(CSV_CONFIG.keys())}"} + + config = CSV_CONFIG[domain] + data = load_csv(config["file"]) + + if not data: + return {"error": f"No data found for domain: {domain}"} + + # Build corpus + corpus = [] + for row in data: + doc_text = " ".join(str(row.get(col, "")) for col in config["search_cols"]) + corpus.append(BM25().tokenize(doc_text)) + + bm25 = BM25() + bm25.fit(corpus) + results = bm25.search(query, max_results) + + output = [] + for idx, score in results: + row = data[idx] + row["_score"] = round(score, 2) + output.append(row) + + return { + "domain": domain, + "query": query, + "file": config["file"], + "count": len(output), + "results": output, + } diff --git a/.claude/skills/generalupdate-troubleshoot/scripts/search.py b/.claude/skills/generalupdate-troubleshoot/scripts/search.py new file mode 100644 index 0000000..5a790a7 --- /dev/null +++ b/.claude/skills/generalupdate-troubleshoot/scripts/search.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GeneralUpdate Search - BM25 search engine for troubleshooting issues and strategies. +Usage: python3 scripts/search.py "" [--domain ] [--max-results 3] + +Domains: issue (default), strategy +""" +import argparse +import sys +import io +from core import CSV_CONFIG, MAX_RESULTS, search + +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def format_output(result): + if "error" in result: + return f"Error: {result['error']}" + + output = [] + output.append(f"## GeneralUpdate Search Results") + output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}") + output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n") + + for i, row in enumerate(result['results'], 1): + severity = row.get('severity', '') + sev_icon = {'C': '🔴', 'H': '🟠', 'M': '🟡', 'L': '🔵'}.get(severity, '') + output.append(f"### {sev_icon} Result {i} (Score: {row.get('_score', 'N/A')})") + for key, value in row.items(): + if key.startswith('_'): + continue + value_str = str(value) + if len(value_str) > 500: + value_str = value_str[:500] + "..." + output.append(f"- **{key}:** {value_str}") + output.append("") + + return "\n".join(output) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="GeneralUpdate Search") + parser.add_argument("query", help="Search query") + parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), default="issue", help="Search domain") + parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)") + parser.add_argument("--json", action="store_true", help="Output as JSON") + + args = parser.parse_args() + result = search(args.query, args.domain, args.max_results) + + if args.json: + import json + print(json.dumps(result, ensure_ascii=False, indent=2)) + else: + print(format_output(result)) diff --git a/.claude/skills/generalupdate-troubleshoot/scripts/tests/test_search.py b/.claude/skills/generalupdate-troubleshoot/scripts/tests/test_search.py new file mode 100644 index 0000000..bebb065 --- /dev/null +++ b/.claude/skills/generalupdate-troubleshoot/scripts/tests/test_search.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Unit tests for GeneralUpdate BM25 search engine.""" +import sys +import os + +# Set DATA_DIR before importing core — test runs from scripts/ but data is at ../data +os.environ["GENERALUPDATE_DATA_DIR"] = os.path.normpath( + os.path.join(os.path.dirname(__file__), '..', '..', 'data') +) + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +import json +from core import search, CSV_CONFIG, DATA_DIR + + +def test_csv_files_exist(): + """All configured CSV data files must exist.""" + for domain, config in CSV_CONFIG.items(): + filepath = DATA_DIR / config["file"] + assert filepath.exists(), f"Missing CSV: {filepath}" + print(f" ✓ {config['file']} exists") + + +def test_issues_csv_has_all_severities(): + """Issues CSV must contain entries for all severity levels: C, H, M, L.""" + # Use a broad query that matches many issues + result = search("update error fail crash bug", "issue", 100) + assert "error" not in result, f"Search error: {result.get('error')}" + severities = {row["severity"] for row in result["results"]} + for sev in ["C", "H", "M", "L"]: + assert sev in severities, f"Missing severity level: {sev} (found: {severities})" + print(f" ✓ All severities (C/H/M/L) present: found {len(result['results'])} matching entries") + + +def test_issues_csv_required_columns(): + """Verify required columns exist by reading raw CSV.""" + import csv + filepath = DATA_DIR / CSV_CONFIG["issue"]["file"] + with open(filepath, 'r', encoding='utf-8') as f: + rows = list(csv.DictReader(f)) + required = ["id", "severity", "symptom_en", "symptom_zh", "cause", "solution"] + for row in rows: + for col in required: + assert col in row and row[col], f"Missing column '{col}' in issue {row.get('id')}" + print(f" ✓ All required columns present in {len(rows)} issues") + + +def test_strategies_csv_has_all_6(): + """Must have exactly 6 strategy entries.""" + import csv + filepath = DATA_DIR / CSV_CONFIG["strategy"]["file"] + with open(filepath, 'r', encoding='utf-8') as f: + rows = list(csv.DictReader(f)) + assert len(rows) == 6, f"Expected 6 strategies, got {len(rows)}" + ids = {r["id"] for r in rows} + for i in range(1, 7): + sid = f"S{i:02d}" + assert sid in ids, f"Missing strategy: {sid}" + print(f" ✓ All 6 strategies present") + + +def test_chinese_search_upgrade_not_start(): + """"升级后启动不了" should match C1 (top result).""" + result = search("升级后启动不了", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C1", f"Expected C1, got {top['id']}" + assert top["_score"] > 0, f"Zero score for C1" + print(f" ✓ '升级后启动不了' → C1 (score={top['_score']})") + + +def test_chinese_search_garbled(): + """"中文乱码" should match H1 (top result).""" + result = search("中文乱码", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "H1", f"Expected H1, got {top['id']}" + print(f" ✓ '中文乱码' → H1 (score={top['_score']})") + + +def test_english_search_method_not_found(): + """"method not found" should match C2.""" + result = search("method not found", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C2", f"Expected C2, got {top['id']}" + print(f" ✓ 'method not found' → C2 (score={top['_score']})") + + +def test_english_search_zip_slip(): + """"zip slip path traversal" should match C6.""" + result = search("zip slip path traversal", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C6", f"Expected C6, got {top['id']}" + print(f" ✓ 'zip slip path traversal' → C6 (score={top['_score']})") + + +def test_strategy_search_oss(): + """"OSS no backend" should match OSS strategy.""" + result = search("OSS no backend server", "strategy", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "S02", f"Expected S02 (OSS), got {top['id']}" + print(f" ✓ 'OSS no backend' → S02 (score={top['_score']})") + + +def test_strategy_search_signalr(): + """"pus" should match SignalR push strategy.""" + result = search("push real-time connection", "strategy", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "S06", f"Expected S06 (SignalR), got {top['id']}" + print(f" ✓ 'push real-time' → S06 (score={top['_score']})") + + +def test_search_json_output(): + """Search output should have correct JSON structure.""" + result = search("升级后启动不了", "issue", 3) + assert "error" not in result + assert result["domain"] == "issue" + assert result["query"] == "升级后启动不了" + assert result["file"] == "issues.csv" + assert result["count"] >= 1 + assert len(result["results"]) >= 1 + # Each result should have _score + for row in result["results"]: + assert "_score" in row + print(f" ✓ JSON structure correct") + + +def test_search_invalid_domain(): + """Invalid domain should return error.""" + result = search("test", "invalid_domain") + assert "error" in result + print(f" ✓ Invalid domain returns error") + + +def test_search_no_results(): + """Search with gibberish should return 0 results.""" + result = search("zzzzzzzxxxxxxyyyyyyy", "issue", 3) + assert "error" not in result + assert result.get("count", 0) == 0 + print(f" ✓ Gibberish search returns 0 results") + + +def test_bm25_scoring_differentiation(): + """Different queries should produce different top results.""" + r1 = search("garbled encoding chinese", "issue", 1) + r2 = search("zip slip path traversal", "issue", 1) + top1_id = r1["results"][0]["id"] + top2_id = r2["results"][0]["id"] + assert top1_id != top2_id, f"Two different queries returned same top result: {top1_id}" + print(f" ✓ BM25 differentiates queries: {top1_id} vs {top2_id}") + + +def test_all_strategies_searchable(): + """Each of the 6 strategies should be findable by keyword.""" + queries = ["standard client-server", "oss", "silent background", "differential delta", "cross version", "signalr push"] + for i, q in enumerate(queries): + r = search(q, "strategy", 1) + assert r["count"] >= 1, f"Strategy {i+1} not found by query: {q}" + print(f" ✓ All 6 strategies searchable") + + +if __name__ == "__main__": + print(f"\n🧪 GeneralUpdate Search Engine Tests\n") + tests = [fn for fn in dir() if fn.startswith("test_")] + passed = 0 + failed = 0 + for name in tests: + try: + globals()[name]() + print(f" PASS {name}") + passed += 1 + except Exception as e: + print(f" FAIL {name}: {e}") + failed += 1 + print(f"\n{'='*40}") + print(f" Total: {passed + failed} | ✅ {passed} | ❌ {failed}") + if failed: + sys.exit(1) diff --git a/.claude/skills/generalupdate-ui/SKILL.md b/.claude/skills/generalupdate-ui/SKILL.md index 886a4a2..8edc936 100644 --- a/.claude/skills/generalupdate-ui/SKILL.md +++ b/.claude/skills/generalupdate-ui/SKILL.md @@ -29,6 +29,48 @@ allowed-tools: "Read, Write, Edit, Glob, Grep" --- +## 📋 用户需求提取(生成 UI 前必须确认) + +``` +### UI 框架(必需) +- 目标框架: ______(WPF/WinForms/Avalonia/MAUI/控制台/不确定) +- 偏好 UI 库: ______(默认推荐 / LayUI.Wpf / WPFDevelopers / AntdUI / SemiUrsa / 原生) +- 是否已有项目模板: ______(是/否,如果否,从 generalupdate-init 开始) + +### 更新场景(必需) +- 更新窗口角色: ______(Client 端/ Upgrade 端/ 两端都需要) +- 是否需要手动触发更新: ______(是/否,自动启动时检查) +- 是否支持暗黑模式: ______(是/否) + +### 高级 UI 需求(可选) +- 需要自定义品牌色/Logo: ______(是/否) +- 需要多语言支持: ______(是/否) +- 需要无障碍支持: ______(是/否) +``` + +--- + +## 工作流程 + +``` +1. 框架探测 + ├── 扫描 .csproj → PackageReference 识别 UI 库 + ├── 如果无法识别 → 询问用户 + └── 如果无 UI 框架 → 控制台进度条 + +2. 状态代码生成 + ├── IDownloadService 桥接接口 + ├── RealDownloadService 桥接代码(手动适配 GeneralUpdate.Core 事件) + ├── ViewModel(MVVM)或 Code-Behind + └── 窗口/页面 XAML + +3. 集成指导 + ├── 如何引入 GeneralUpdateBootstrap + └── Bootstrap 配置(与 generalupdate-init 配合) +``` + +--- + ## UI 状态机(所有模板覆盖以下状态) ``` @@ -146,7 +188,47 @@ GeneralUpdateBootstrap.AddListenerException --- +## ✅ 集成验证清单(交付前逐项检查) + +### 事件桥接 +- [ ] 所有 6 个事件都已绑定(UpdateInfo, MultiDownloadStatistics, MultiDownloadCompleted, MultiDownloadError, MultiAllDownloadCompleted, Exception) +- [ ] 桥接代码使用正确的 EventArgs 类型(检查命名空间 `GeneralUpdate.Common.Download`) +- [ ] `IsComplated` 注意拼写(v10.4.6 API 中的实际拼写,不是 `IsCompleted`) + +### 线程安全 +- [ ] UI 更新操作在正确的线程上执行(WPF/Avalonia 用 `Dispatcher`,WinForms 用 `Invoke`,MAUI 用 `MainThread`) +- [ ] `MultiDownloadStatistics` 事件中不执行耗时操作(仅更新 UI) +- [ ] 下载完成后的"正在应用"状态有超时保护(建议 > 30 秒显示进度提示) + +### 状态机覆盖 +- [ ] 所有 11 个状态都已实现(Idle → Checking → Latest/Found → Downloading → Paused → Error → Retrying → Applying → Success/Failed → Restart) +- [ ] 下载错误的自动重试次数有限制(不超过 3 次) +- [ ] 用户可取消更新操作 + +### 框架特定检查 +- [ ] **Avalonia**: ViewModel 实现 `INotifyPropertyChanged`,绑定使用 `{Binding}` +- [ ] **WPF**: 使用 `Dispatcher.Invoke` 更新绑定的属性 +- [ ] **WinForms AntdUI**: 使用 `Control.Invoke` 进行跨线程更新 +- [ ] **MAUI**: 检查 `Platform.CurrentActivity` 在 Android 上的生命周期 + +--- + +## ⚠️ 反模式清单 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **通用 ViewModel 直接用在不同框架** | 线程模型不兼容导致跨线程异常 | 按框架分别适配 Dispatcher/Invoke/MainThread | +| 2 | **在下载统计事件中做文件 IO 或网络请求** | 阻塞更新流程,UI 卡顿 | 仅更新 UI 绑定的属性 | +| 3 | **进度条绑定一次性更新到 100%** | 用户看不到中间过程,体验差 | 使用 `e.ProgressPercentage` 逐步更新 | +| 4 | **未处理 MultiDownloadError 事件** | 下载失败时用户无反馈,卡在等待状态 | 至少显示错误信息 + 重试按钮 | +| 5 | **未区分 Client 和 Upgrade 的 UI** | Upgrade 端显示不必要的"下载进度" | Upgrade 端只显示"正在安装,请稍候" | +| 6 | **直接使用 RealDownloadService.cs 不做适配** | 事件绑定不生效 | 必须根据项目结构调整 `IDownloadService` 实现 | +| 7 | **Avalonia/WPF 在 ViewModel 构造函数中启动更新** | UI 还未初始化完成,绑定不生效 | 在 Loaded 事件或 View 层触发检查更新 | + +--- + ## 相关技能 - `/generalupdate-init` — 如果还未配置 Bootstrap +- `/generalupdate-strategy` — 如果想要 Silent 模式不需要 UI - `/generalupdate-troubleshoot` — 如果 UI 显示异常 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..30ad95b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,234 @@ +name: CI — Validate Build + Test + +on: + push: + branches: [main, feat/*, fix/*] + pull_request: + branches: [main] + +jobs: + python-search-test: + name: Python Search Engine + runs-on: ubuntu-latest + defaults: + run: + working-directory: .claude/skills/generalupdate-troubleshoot + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Run BM25 search tests + run: | + python3 -m pytest scripts/tests/ -v + + - name: Smoke test — Chinese search + run: | + result=$(python3 scripts/search.py "升级后启动不了" --domain issue -n 1 --json) + matches=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])") + echo "Search matches: $matches" + [ "$matches" -ge 1 ] && echo "✅ Search works" || (echo "❌ Search failed"; exit 1) + + - name: Smoke test — English search + run: | + result=$(python3 scripts/search.py "method not found" --domain issue -n 1 --json) + matches=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])") + echo "Search matches: $matches" + [ "$matches" -ge 1 ] && echo "✅ English search works" || (echo "❌ English search failed"; exit 1) + + - name: Smoke test — Strategy search + run: | + result=$(python3 scripts/search.py "OSS no backend" --domain strategy -n 1 --json) + matches=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])") + echo "Search matches: $matches" + [ "$matches" -ge 1 ] && echo "✅ Strategy search works" || (echo "❌ Strategy search failed"; exit 1) + + python-codegen-test: + name: Python Code Generator + runs-on: ubuntu-latest + defaults: + run: + working-directory: .claude/scripts + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: List all combinations + run: | + count=$(python3 generate.py --list | grep -c "Total combinations") + echo "Combinations listed: $count" + python3 generate.py --list + + - name: Generate — OSS + WPF + Bowl + run: | + python3 generate.py --strategy oss --framework wpf-layui --bowl \ + --project-name TestApp --version 1.0.0.0 -o /tmp/gen-test-oss + + echo "=== Files generated ===" + find /tmp/gen-test-oss -type f | sort + + echo "=== Verify Bootstrap.cs contains expected values ===" + grep -q "TestApp.exe" /tmp/gen-test-oss/Client/Integration.cs && echo "✅ AppName correct" + grep -q "GeneralUpdate.Bowl.Bowl.MonitorParameter" /tmp/gen-test-oss/Client/Integration.cs && echo "✅ Bowl included" + grep -q "1.0.0.0" /tmp/gen-test-oss/generalupdate.manifest.json && echo "✅ Version correct" + + - name: Generate — Silent + Console (no Bowl) + run: | + python3 generate.py --strategy silent --framework console \ + --project-name MyService --version 2.0.0.0 \ + --update-url https://api.example.com/check -o /tmp/gen-test-silent + + echo "=== Files generated ===" + find /tmp/gen-test-silent -type f | sort + + grep -q "MyService.exe" /tmp/gen-test-silent/Client/Integration.cs && echo "✅ AppName correct" + grep -q "Console.WriteLine" /tmp/gen-test-silent/Client/Integration.cs && echo "✅ Console listeners correct" + + - name: Generate — Standard + Avalonia + Differential + run: | + python3 generate.py --strategy differential --framework avalonia-semiursa \ + --project-name CrossApp --version 3.1.0.0 -o /tmp/gen-test-diff + + echo "=== Files generated ===" + find /tmp/gen-test-diff -type f | sort + + grep -q "CrossApp" /tmp/gen-test-diff/Client/Integration.cs && echo "✅ Correct" + grep -q "Differential" /tmp/gen-test-diff/IssuesWarning.md && echo "✅ Warnings included" + + - name: Validate — All files are valid UTF-8 and non-empty + run: | + for f in $(find /tmp -name "*.cs" -o -name "*.json" -o -name "*.md" 2>/dev/null); do + [ -s "$f" ] || (echo "❌ Empty file: $f"; exit 1) + file "$f" | grep -q "UTF-8\|ASCII\|text" || (echo "❌ Non-text file: $f"; exit 1) + done + echo "✅ All generated files valid" + + dotnet-verify-templates: + name: .NET — Template Build Verification + runs-on: windows-latest + strategy: + matrix: + template: [MinimalIntegration, FullIntegration] + defaults: + run: + shell: pwsh + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Generate test project from ${{ matrix.template }} + run: | + $templatePath = ".claude/skills/generalupdate-init/templates" + $testDir = "C:\tmp\verify-${{ matrix.template }}" + New-Item -ItemType Directory -Path $testDir -Force | Out-Null + + # Create minimal .csproj + @" + + + Exe + net10.0 + enable + enable + + + + + + "@ | Out-File -FilePath "$testDir\TestProject.csproj" -Encoding UTF8 + + # Copy template and r-w AssemblyName to config + $code = Get-Content "$templatePath\$($args[0]).cs" -Raw + $code = $code -replace 'MyApp\.exe', 'TestApp.exe' + $code = $code -replace '1\.0\.0\.0', '1.0.0.1' + $code = $code -replace 'my-product-001', 'test-001' + $code | Out-File -FilePath "$testDir\Program.cs" -Encoding UTF8 + + Write-Host "=== Verifying: $($args[0]) ===" + dotnet build "$testDir\TestProject.csproj" 2>&1 + } -ArgumentList "${{ matrix.template }}" + + - name: Verify build success + run: | + if ($LASTEXITCODE -ne 0) { throw "Build failed for ${{ matrix.template }}" } + echo "✅ ${{ matrix.template }} compiles successfully" + + dotnet-verify-scaffold: + name: .NET — Complete Scaffold Build + runs-on: windows-latest + defaults: + run: + shell: pwsh + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Build scaffold projects + run: | + $scaffoldDir = ".claude/skills/generalupdate-init/project-scaffold" + + # Create a temp solution + $testDir = "C:\tmp\verify-scaffold" + New-Item -ItemType Directory -Path $testDir -Force | Out-Null + + Copy-Item "$scaffoldDir\ClientApp.csproj" "$testDir\ClientApp.csproj" + Copy-Item "$scaffoldDir\UpgradeApp.csproj" "$testDir\UpgradeApp.csproj" + Copy-Item "$scaffoldDir\ClientProgram.cs" "$testDir\ClientProgram.cs" + Copy-Item "$scaffoldDir\UpgradeProgram.cs" "$testDir\UpgradeProgram.cs" + + # Build Client + dotnet build "$testDir\ClientApp.csproj" 2>&1 + if ($LASTEXITCODE -ne 0) { throw "ClientApp build failed" } + echo "✅ ClientApp compiles" + + # Build Upgrade + dotnet build "$testDir\UpgradeApp.csproj" 2>&1 + if ($LASTEXITCODE -ne 0) { throw "UpgradeApp build failed" } + echo "✅ UpgradeApp compiles" + + cli-verify-typescript: + name: CLI — TypeScript Compilation + runs-on: ubuntu-latest + defaults: + run: + working-directory: cli + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: cli/package-lock.json + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: TypeScript compile + run: npx tsc --noEmit + + - name: Ensure entry point structure is valid + run: | + grep -q "commander" src/index.ts && echo "✅ Commander found" + grep -q "initCommand" src/index.ts && echo "✅ initCommand referenced" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..593333e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,159 @@ +name: Release — Publish CLI + GitHub Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 0.1.0)' + required: true + type: string + +jobs: + release: + name: Build and Publish + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + # ====================== + # Validate everything first + # ====================== + + - name: Validate — BM25 search + run: | + cd .claude/skills/generalupdate-troubleshoot + python3 scripts/search.py "升级后启动不了" --domain issue -n 1 --json | \ + python3 -c "import sys,json; d=json.load(sys.stdin); assert d['count']>=1; print('✅ Search OK')" + + - name: Validate — Code generator + run: | + cd .claude/scripts + python3 generate.py --strategy oss --framework wpf-layui --bowl \ + --project-name ReleaseTest --version 0.1.0.0 -o /tmp/release-verify + grep -q "ReleaseTest.exe" /tmp/release-verify/Client/Integration.cs && echo "✅ Generator OK" + + - name: Validate — Scaffold builds + run: | + $scaffoldDir = ".claude/skills/generalupdate-init/project-scaffold" + $testDir = "C:\tmp\release-verify-scaffold" + New-Item -ItemType Directory -Path $testDir -Force | Out-Null + Copy-Item "$scaffoldDir\*" $testDir + dotnet build "$testDir\ClientApp.csproj" 2>&1 + if ($LASTEXITCODE -ne 0) { throw "Scaffold build failed" } + Write-Host "✅ Scaffold builds" + shell: pwsh + + # ====================== + # Build CLI + # ====================== + + - name: Build CLI + run: | + cd cli + npm ci --ignore-scripts + npm run build + echo "✅ CLI built" + + # ====================== + # Package assets for CLI + # ====================== + + - name: Sync assets to CLI + run: | + # Sync search engine + cp -r .claude/skills/generalupdate-troubleshoot/scripts cli/assets/scripts/ + cp -r .claude/skills/generalupdate-troubleshoot/data cli/assets/data/ + # Sync code generator + cp -r .claude/scripts/generate cli/assets/scripts/generate + cp .claude/scripts/generate.py cli/assets/scripts/generate.py + + echo "✅ Assets synced" + + # ====================== + # Generate changelog + # ====================== + + - name: Generate changelog + run: | + previousTag=$(git describe --tags --abbrev=0 --always 2>/dev/null || echo "") + if [ -z "$previousTag" ] || [ "$previousTag" = "${{ github.sha }}" ]; then + log=$(git log --oneline --no-decorate -100) + else + log=$(git log "${previousTag}..HEAD" --oneline --no-decorate) + fi + + features=""; fixes=""; docs=""; chores=""; others="" + while IFS= read -r line; do + line=$(echo "$line" | sed 's/^[a-f0-9]* //') + case "$line" in + feat:*) features="$features- $line\n" ;; + fix:*) fixes="$fixes- $line\n" ;; + docs:*) docs="$docs- $line\n" ;; + chore:*|ci:*|build:*) chores="$chores- $line\n" ;; + *) others="$others- $line\n" ;; + esac + done <<< "$log" + + cat > changelog.md << EOF + ## 🚀 Features + $(echo -e "$features") + + ## 🐛 Bug Fixes + $(echo -e "$fixes") + + ## 📝 Documentation + $(echo -e "$docs") + + ## 🔧 Chores + $(echo -e "$chores") + + ## 📦 Other Changes + $(echo -e "$others") + EOF + echo "✅ Changelog generated" + + # ====================== + # Create GitHub Release + # ====================== + + - name: Create Release + uses: softprops/action-gh-release@v3 + with: + tag_name: v${{ github.event.inputs.version }} + name: Release v${{ github.event.inputs.version }} + body_path: changelog.md + files: | + cli/dist/ + cli/package.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Output — Next steps + run: | + echo "" + echo "==============================" + echo "✅ Release v${{ github.event.inputs.version }} published!" + echo "" + echo "To publish to npm:" + echo " cd cli" + echo " npm publish" + echo "" + echo "==============================" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4fd9f23 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# CLAUDE.md + +This file provides guidance to AI agents (Claude Code, Cursor, etc.) when working with this repository. + +## Project Overview + +GeneralUpdate Skill CodeGen is a **Claude Code skill suite** for integrating [GeneralUpdate](https://github.com/GeneralLibrary/GeneralUpdate) (.NET auto-update) into any .NET application. It provides code generation, strategy decision tree, troubleshooting search, and multi-platform AI support. + +## Quick Start (for AI agents) + +When the user asks about GeneralUpdate or .NET auto-update: + +1. **Root SKILL.md** is the entry point — read it first for the developer roadmap +2. **5 sub-skills** each have their own SKILL.md with step-by-step workflow +3. **Use the search engine** for troubleshooting (BM25, Python): + ```bash + python3 .claude/skills/generalupdate-troubleshoot/scripts/search.py "" --domain issue + ``` +4. **Use the code generator** for production code: + ```bash + python3 .claude/scripts/generate.py --framework wpf --strategy oss --bowl + ``` + +## Architecture + +``` +generalupdate-skill-codegen/ +│ +├── SKILL.md ← Entry point: developer roadmap + decision tree +├── RULES.md ← Technical rules (API, events, NuGet) +├── CLAUDE.md ← THIS FILE: AI agent guidance +│ +├── .claude/ +│ ├── skills/ +│ │ ├── generalupdate-init/ ← Bootstrap + scaffolding +│ │ ├── generalupdate-ui/ ← Update UI windows +│ │ ├── generalupdate-strategy/ ← 6 strategy decision tree +│ │ ├── generalupdate-advanced/ ← Bowl, IPC, AOT, Pipeline +│ │ └── generalupdate-troubleshoot/ ← 50+ known issues + search engine +│ └── scripts/ +│ └── generate.py ← Parameterized code generator (336 combinations) +│ +├── cli/ ← CLI installer (gskill on npm) +│ ├── src/commands/init.ts ← Install for any AI platform +│ ├── src/commands/generate.ts ← Delegate to Python generator +│ ├── src/commands/uninstall.ts ← Clean removal +│ └── src/utils/ ← detect.ts, template.ts, github.ts, extract.ts +│ +├── BUGS.md ← Full code audit +├── clinerules/ ← Claude Code-specific rules +├── cursor/rules/ ← Cursor-specific rules +│ +└── .github/workflows/ + ├── ci.yml ← Validate builds + tests + └── release.yml ← Publish CLI + GitHub Release +``` + +## Source of Truth Rules + +**Edit only in these locations; everything else is auto-synced:** + +| What | Where to Edit | Auto-Synced To | +|------|--------------|----------------| +| SKILL.md (root) | `SKILL.md` | - | +| Sub-skill content | `.claude/skills/*/SKILL.md` | - | +| Templates (.cs) | `.claude/skills/*/templates/` | `cli/assets/` (via sync) | +| Issues/strategies CSV | `.claude/skills/*/data/` | `cli/assets/data/` (via sync) | +| Python scripts | `.claude/skills/*/scripts/` | `cli/assets/scripts/` (via sync) | +| Code generator | `.claude/scripts/generate.py` | `cli/assets/scripts/` (via sync) | +| CLI source | `cli/src/` | Built to `cli/dist/` | +| Platform configs | `cli/assets/templates/platforms/` | - | + +**Sync commands before release:** +```bash +# Sync all data/scripts to CLI assets +python3 .claude/scripts/_sync_all.py + +# Or manually: +cp -r .claude/skills/generalupdate-troubleshoot/data/* cli/assets/data/ +cp -r .claude/skills/generalupdate-troubleshoot/scripts/* cli/assets/scripts/ +cp .claude/scripts/generate.py cli/assets/scripts/generate.py +``` + +## Search Commands + +```bash +# Troubleshooting +python3 .claude/skills/generalupdate-troubleshoot/scripts/search.py "" --domain issue + +# Strategy lookup +python3 .claude/skills/generalupdate-troubleshoot/scripts/search.py "" --domain strategy + +# Code generation +python3 .claude/scripts/generate.py --list +python3 .claude/scripts/generate.py --framework wpf --strategy oss --bowl --project-name MyApp +``` + +## Testing + +```bash +# Search engine tests (15 tests) +python3 .claude/skills/generalupdate-troubleshoot/scripts/tests/test_search.py + +# CI validates: search, codegen, .NET compile, TypeScript +# See .github/workflows/ci.yml +``` + +## Git Workflow + +Never push directly to `main`. Always: + +1. Branch: `git checkout -b feat/...` or `fix/...` +2. Commit: conventional commits (feat:, fix:, docs:, chore:) +3. Push: `git push -u origin ` +4. PR: Create PR against `main` + +## Prerequisites + +- Python 3.10+ (for search engine and code generator) +- .NET SDK 10.0+ (for template verification) +- Node.js 22+ (for CLI development) diff --git a/RULES.md b/RULES.md index e2e40da..c36fea3 100644 --- a/RULES.md +++ b/RULES.md @@ -5,6 +5,12 @@ - 4 scenes: None/UpgradeOnly/MainOnly/Both - IPC: Encrypted file (AES, default) +## Entry Point & Navigation +- Root SKILL.md contains developer roadmap: 6 scenarios → which skill to use → next step +- 5-question decision tree helps first-timers locate their starting skill +- Pre-Delivery Checklist + Anti-Pattern tables in every sub-skill SKILL.md +- Always extract user requirements template before generating code (frontmatter in SKILL.md) + ## Bootstrap (v10.4.6 stable API) - Configinfo + SetConfig() + LaunchAsync() - Events: AddListenerUpdateInfo, AddListenerMultiDownloadStatistics, etc. @@ -43,6 +49,27 @@ Linux/Mac: Hash->Decompress->Patch (no Bowl) - ProgressEventArgs: Progress (DownloadProgress?), DiffProgress (DiffProgress?) — v10.4.6 Core only - ExceptionEventArgs: Exception, Message +## Code Generation +- Use `.claude/scripts/generate.py` for parameterized code generation: + ```bash + python3 .claude/scripts/generate.py --framework wpf --strategy oss --bowl --project-name MyApp + ``` +- Generates 5 files: Bootstrap.cs, manifest.json, UpgradeProgram.cs, DeploymentChecklist.md, IssuesWarning.md +- Covers 336 combinations: 6 strategies × 7 frameworks × 2 Bowl × 4 scenes + +## Troubleshooting Search +- Use BM25 search engine before manual reference.md lookup: + ```bash + python3 skills/generalupdate-troubleshoot/scripts/search.py "" --domain issue + ``` +- CSV database covers 50+ known issues (51 entries: 8C + 11H + 20M + 12L) +- Strategies are also searchable: `--domain strategy` + +## Template Conventions +- Template files use `{{PLACEHOLDER}}` syntax for parameter substitution +- Conditional blocks: `{{#KEY}}...{{/KEY}}` (show if KEY truthy), `{{^KEY}}...{{/KEY}}` (show if falsy) +- Templates stored in `.claude/scripts/generate/templates/` + ## Quick Fixes - Upgrade not starting: Check UpdatePath - Method not found: Align NuGet versions @@ -57,3 +84,4 @@ Linux/Mac: Hash->Decompress->Patch (no Bowl) 3. UpgradeApp.exe exists 4. Server API reachable 5. Logs in Logs/generalupdate-trace *.log +6. Use BM25 search engine for known issues diff --git a/SKILL.md b/SKILL.md index d99b8d6..a738821 100644 --- a/SKILL.md +++ b/SKILL.md @@ -12,6 +12,7 @@ description: | ".NET update", "Claude Code skill suite", "GeneralUpdate Skill CodeGen", "generalupdate-init", "generalupdate-ui", "generalupdate-strategy", "generalupdate-advanced", "generalupdate-troubleshoot", + "generalupdate-migration", "generalupdate-security-audit", "集成GeneralUpdate", "接入自动更新", "更新技能", "升级框架", ".NET自动更新", "双进程更新", "Bootstrap配置", "WPF update", "Avalonia update", "MAUI update", "WinForms update", @@ -20,12 +21,14 @@ description: | "update troubleshooting", "更新失败排查", "升级报错". Also triggers on common .NET + update combinations. - Contains 5 sub-skills: + Contains 7 sub-skills: - generalupdate-init: Dual-project scaffolding + Bootstrap config - generalupdate-ui: Full-state update UI for 6 frameworks - generalupdate-strategy: 6 strategy decision tree + examples - generalupdate-advanced: 10+ extension points + Bowl + IPC + AOT - generalupdate-troubleshoot: 50+ known issues diagnosis + - generalupdate-migration: v9.x → v10 / dev-branch → stable migration + - generalupdate-security-audit: Security audit for update pipeline when_to_use: | - First-time integration of GeneralUpdate into any .NET project - User needs auto-update capability for WPF/WinForms/Avalonia/MAUI/console app @@ -47,15 +50,121 @@ allowed-tools: "Bash, Read, Write, Edit, Glob, Grep, WebSearch" > 兼容性:`v10.4.6`(NuGet 最新稳定版) > 所有 32 个模板文件已通过 `dotnet build` 编译验证(0 errors)。 -## Skills Overview +--- + +## 🧭 开发者集成路线图 + +**你是哪种情况?找到你的入口,按步骤推进:** + +| 你的场景 | 从哪开始 | 做什么 | 完成后下一步 | +|---------|---------|-------|-------------| +| 🆕 **第一次加更新,从零开始** | `/generalupdate-init` | ① 选集成模式 → ② 生成 Bootstrap → ③ 部署 | `/generalupdate-ui`(加界面) | +| 🎨 **已有集成,需要更新界面** | `/generalupdate-ui` | ① 自动检测框架 → ② 生成窗口 → ③ 桥接事件 | `/generalupdate-strategy`(选策略) | +| ⚙️ **要选更新策略(OSS/静默/差分)** | `/generalupdate-strategy` | ① 决策树选策略 → ② 配置服务端 → ③ 示例代码 | `/generalupdate-init`(配置 Bootstrap) | +| 🔧 **需要高级定制(Bowl/IPC/Hooks)** | `/generalupdate-advanced` | ① 选扩展点 → ② 生成模板代码 → ③ 集成 | 部署验证 | +| 🩺 **更新失败/报错/异常** | `/generalupdate-troubleshoot` | ① 症状收集 → ② 匹配已知问题 → ③ 修复 | 回到对应 skill 改配置 | +| 📦 **已有 v9.x 要迁移到 v10** | `/generalupdate-init` | 参考"从旧版迁移"章节 + 重新生成配置 | `/generalupdate-troubleshoot`(检查迁移问题) | + +### 5 个问题快速定位 + +回答以下问题,系统会自动推荐应该从哪个 skill 开始: + +``` +Q1: 你的项目已经能正常编译运行吗? + ├── 能 → Q2 + └── 不能 → 先确保项目能编译,再回来 + +Q2: 你已经有 GeneralUpdate NuGet 包了吗? + ├── 有 → Q4 + └── 没有 / 不确定 → 推荐: /generalupdate-init + +Q3(接 Q2 没有): 你需要显示更新进度给用户看吗? + ├── 要 → 推荐: /generalupdate-ui(会自动引导 init) + └── 不要 → 推荐: /generalupdate-init(生成控制台版) + +Q4(接 Q2 有): 更新成功了吗? + ├── 成功了 → Q5 + └── 失败了/报错 → 推荐: /generalupdate-troubleshoot + +Q5(接 Q4 成功): 你需要什么? + ├── 更省带宽 → 推荐: /generalupdate-strategy → Differential + ├── 后台自动更新 → 推荐: /generalupdate-strategy → Silent + ├── 崩溃自动恢复 → 推荐: /generalupdate-advanced → Bowl + └── 以上都不 → 部署验证,恭喜!🎉 +``` + +--- + +## 📋 用户需求提取模板 -| Skill | Command | Description | Coverage | -|-------|---------|-------------|----------| +当开发者描述需求时,必须提取以下信息。不确定的字段**必须追问**。 + +``` +### 技术栈(必需) +- .NET 版本: ______(.NET 6/8/9/10) +- UI 框架: ______(WPF/WinForms/Avalonia/MAUI/控制台/无) +- 目标平台: ______(Windows/Linux/macOS/多平台) + +### 部署环境(必需) +- 更新策略倾向: ______(标准服务端/OSS/静默/差分/跨版本/推送) +- 是否有后端服务: ______(是/否,如 GeneralSpacestation) +- 如果是 OSS: ______(S3/MinIO/阿里云OSS/其他) + +### 集成阶段(必需) +- 当前阶段: ______(① 从零开始 / ② 已有部分集成 / ③ 遇到问题 / ④ 迁移升级) +- 是否已有 Bootstrap 代码: ______(是/否) +- 是否需要更新 UI: ______(是/否,什么框架) + +### 高级需求(可选) +- 需要崩溃守护(Bowl): ______(是/否) +- 需要 IPC 替换(NamedPipe): ______(是/否) +- 需要 AOT 支持: ______(是/否) +- 其他定制: ______ +``` + +--- + +## 🧩 Skills Overview + +| Skill | Command | 一句话 | 覆盖 | +|-------|---------|--------|------| | 🚀 `generalupdate-init` | `/generalupdate-init` | 双项目脚手架 + Bootstrap 配置(4 种方式) | 4 大场景 + 4 种配置方式 + 完整 API | | 🎨 `generalupdate-ui` | `/generalupdate-ui` | 自动识别 UI 框架,生成全状态更新窗口(11 种状态) | 6 UI 框架 + 全状态机 + 桥接代码 | | ⚙️ `generalupdate-strategy` | `/generalupdate-strategy` | 6 种策略决策树 + 混合组合 + 平台差异 | 6 策略 + 4 组合 + 平台对照 | | 🔧 `generalupdate-advanced` | `/generalupdate-advanced` | 10+ 扩展点 + 4 种 IPC + Bowl + AOT | 10+ 扩展点 + 完整架构图 | | 🩺 `generalupdate-troubleshoot` | `/generalupdate-troubleshoot` | 50+ 已知问题诊断 + 6 步通用排查 | 8 致命 + 11 高 + 20 中 + 12 低 | +| 🔄 `generalupdate-migration` | `/generalupdate-migration` | v9.x → v10 / dev-branch → stable 迁移 | 2 条迁移路径 + API 对照表 | +| 🔒 `generalupdate-security-audit` | `/generalupdate-security-audit` | 安全审计 + 修复建议 | 14 项安全矩阵 + 审计报告模板 | + +--- + +## 🎯 输出格式说明 + +所有代码生成遵循以下结构化输出: + +``` +### 📦 生成内容总览 +- Bootstrap 配置: Minimal/Standard/Full + appsettings.json +- UI 框架: WPF(AntdUI/LayUI/WPFDevelopers)/Avalonia(SemiUrsa)/MAUI +- 更新策略: Client-Server/OSS/Silent/Differential/CVP/Push + +### 🔧 关键决策 +- 选择理由: ______ +- 为什么是这个策略: ______ +- 为什么不是其他策略: ______ + +### ⚠️ 已知问题预警 +- 该配置组合下已知问题: ______ +- 避坑指南: ______ + +### ✅ 部署检查清单 +- [ ] 必填项已填 +- [ ] NuGet 版本一致 +- [ ] UpgradeApp 已发布 +- [ ] 安全配置已验证 +``` + +--- ## Quick Start @@ -73,6 +182,9 @@ allowed-tools: "Bash, Read, Write, Edit, Glob, Grep, WebSearch" "添加 Bowl 崩溃守护 + 自定义 Hooks" → 自动激活 generalupdate-advanced + +"把 v9.x 的项目迁移到 v10" +→ 自动激活 generalupdate-init(参考迁移章节) ``` ### Prerequisites @@ -82,6 +194,54 @@ allowed-tools: "Bash, Read, Write, Edit, Glob, Grep, WebSearch" 3. **GeneralUpdate 服务端**: 对于标准策略,需要部署 [GeneralSpacestation](https://github.com/JusterZhu/GeneralSpacestation) 或兼容的后端服务 4. **双进程架构**: 需要理解 Client + Upgrade 双进程的核心理念 +--- + +## 通用集成验证清单 + +无论使用哪个 skill,完成集成后请逐项检查: + +### Bootstrap 配置 +- [ ] `Configinfo` 的 6 个必填字段都已设置(UpdateUrl, AppSecretKey, AppName, MainAppName, ClientVersion, ProductId, InstallPath) +- [ ] `UpdateUrl` 指向的服务端 API 可正常返回版本信息 +- [ ] `AppSecretKey` 长度 ≥ 16 字符,与服务端一致 +- [ ] `InstallPath` 指向正确的安装目录(生产环境用 `AppDomain.CurrentDomain.BaseDirectory`) +- [ ] `AppType` 设置正确(Client = 1, Upgrade = 2) + +### NuGet & 编译 +- [ ] Client 和 Upgrade 项目使用**完全相同**的 GeneralUpdate NuGet 版本 +- [ ] 如果用 Bowl:项目中只能有 `GeneralUpdate.Bowl`,不能同时有 `GeneralUpdate.Core` +- [ ] 项目能正常 `dotnet build`(0 errors) + +### 部署结构 +- [ ] UpgradeApp.exe 存在于发布目录(首个版本就必须有) +- [ ] `generalupdate.manifest.json` 的 `UpdateAppName` 包含 `.exe` +- [ ] IPC 文件(`UpdateInfo.msg`)路径在 Client/Upgrade 间一致 +- [ ] `Encoding` 设置为 `Encoding.UTF8`(防止 Linux/macOS 中文乱码) + +### 安全(可选但推荐) +- [ ] `AppSecretKey` 使用强密码(大小写 + 数字 + 符号,≥ 32 字符) +- [ ] 生产环境使用 HTTPS 的 UpdateUrl +- [ ] OSS 场景下 Bucket 权限设置为私有 + +--- + +## ⚠️ 通用反模式清单 + +以下错误在所有集成场景中反复出现,务必避免: + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **Core 和 Bowl 引用到同一个项目** | CS0433 类型冲突,编译失败 | 用 Bowl 时只引 Bowl | +| 2 | **Client/Upgrade NuGet 版本号不一致** | 运行时 MethodNotFoundException | 锁定完全相同版本 | +| 3 | **事件监听中做耗时操作(网络 IO / 磁盘 IO)** | Update 进程 UI 卡死,超时被 Kill | 仅更新 UI 状态,耗时操作异步 | +| 4 | **IPC 文件编码未设置 UTF-8** | Linux/macOS 中文乱码 | `Encoding.UTF8` | +| 5 | **UpgradeApp.exe 不随首个版本发布** | 第一次更新时 FileNotFoundException | 首个版本就包含 UpgradeApp | +| 6 | **版本号不是 4 段式(如 1.0.0.0)** | 版本比较逻辑异常 | 始终用 `x.y.z.w` 格式 | +| 7 | **manifest.json 的 mainAppName 不匹配真实进程名** | 更新后主程序找不到 | 和实际 exe 名称一致 | +| 8 | **为旧版 GeneralUpdate 编写的代码直接用在 v10** | API 不兼容,编译失败 | 对照 v10.4.6 稳定版 API 重写 | + +--- + ## Data Sources 所有技能的内容基于以下真实数据: @@ -92,6 +252,8 @@ allowed-tools: "Bash, Read, Write, Edit, Glob, Grep, WebSearch" - **Samples 源码**: CompleteUpdateSample、SilentUpdateSample、OssSample、DifferentialSample、PushSample、BowlSample、ExtensionSample、CompressSample、ImDiskQuickInstallSample - **UI Samples**: SemiUrsa、LayUI、AntdUI、WPFDevelopers、MauiUpdate、AndroidUpdate +--- + ## Skill File Structure ``` @@ -129,11 +291,22 @@ allowed-tools: "Bash, Read, Write, Edit, Glob, Grep, WebSearch" │ ├── CustomHooks.cs / CustomStrategy.cs │ ├── BowlIntegration.cs / NamedPipeIPC.cs │ -└── generalupdate-troubleshoot/ (2 files) - ├── SKILL.md ← 诊断工作流 - └── reference.md ← 50+ 症状清单(C/H/M/L 四级) +├── generalupdate-troubleshoot/ (5+ files) +│ ├── SKILL.md ← 诊断工作流 +│ ├── reference.md ← 50+ 症状清单(C/H/M/L 四级) +│ ├── scripts/search.py ← BM25 搜索引擎 +│ ├── scripts/core.py ← BM25 算法核心 +│ └── data/issues.csv ← 51 条已知问题数据库 +│ +├── generalupdate-migration/ (1 file) +│ └── SKILL.md ← v9.x→v10 / dev-branch→stable 迁移 +│ +└── generalupdate-security-audit/ (1 file) + └── SKILL.md ← 14 项安全审计矩阵 ``` +--- + ## API Compatibility > ⚠️ **NuGet Reference Rules**: @@ -148,12 +321,16 @@ allowed-tools: "Bash, Read, Write, Edit, Glob, Grep, WebSearch" > - ❌ 无 `SilentPollOrchestrator` > - ❌ 无 `ProcessContract` / IPC 替换接口 +--- + ## Version History ### 0.0.1-bate.1 — 2026-06-16 Initial beta release. All templates rewritten for NuGet v10.4.6 stable API. +--- + ## License Apache 2.0 — 与 GeneralUpdate 主项目一致 diff --git a/cli/assets/data/issues.csv b/cli/assets/data/issues.csv new file mode 100644 index 0000000..c814db2 --- /dev/null +++ b/cli/assets/data/issues.csv @@ -0,0 +1,52 @@ +id,severity,symptom_en,symptom_zh,cause,solution,code_ref,workaround,keywords +C1,C,"Upgrade process not started / FileNotFoundException: upgrade application not found","升级进程没启动 / FileNotFoundException","UpgradeApp.exe not shipped with main application in first release","Ensure UpgradeApp.exe is included in the publish directory from the first release","Configinfo.UpdatePath, Configinfo.UpdateAppName","OSS mode: place Upgrade.exe in update/ subdirectory","upgrade not start, file not found, 升级没启动, FileNotFoundException, missing upgrade" +C2,C,"Method not found exception at runtime","运行时 Method not found 异常","Client and Upgrade projects use different GeneralUpdate NuGet versions","Lock Client.csproj and Upgrade.csproj to the exact same NuGet version","GeneralUpdate.Core, .csproj","Pin version in Directory.Build.props to enforce consistency","method not found, MissingMethodException, 方法找不到, NuGet conflict, version mismatch" +C3,C,"BSOD / OutOfMemory / Process crash on differential patch","蓝屏/内存溢出/进程崩溃差分补丁","BsdiffDiffer.WriteInt64 overflows on long.MinValue negation; control value > int.MaxValue truncates to negative","Update to v10.4.6+ (#514 fixed). If cannot update: add MaxInputFileSize limit in differential engine","BsdiffDiffer, PatchMiddleware","Limit patch file size at application level","BSOD, crash, OOM, differential, patch, 蓝屏, 崩溃, 内存溢出, BSDIFF" +C4,C,"PathTooLongException during backup (recursive nesting)","备份递归嵌套 PathTooLongException","StorageManager.Backup() creates backup dir INSIDE install path; empty SkipDirectorys list doesn't trigger default skip logic","Set SkipDirectorys on Configinfo to exclude backup directories","Configinfo.SkipDirectorys, StorageManager.Backup","Set SkipDirectorys to exclude '.backups','backup-'","path too long, backup recursion, PathTooLongException, 路径过长, 备份嵌套" +C5,C,"IPC encryption key hardcoded / weak","IPC 加密密钥硬编码/弱密钥","Default IPC encryption key is a hardcoded string; AppSecretKey is reused as encryption key","Use strong AppSecretKey (>= 32 chars, mixed case + numbers + symbols)","Configinfo.AppSecretKey, ProcessInfoJsonContext","Rotate key periodically, audit encryption in IPC path","IPC encryption, hardcoded key, 加密, 硬编码, security, AppSecretKey" +C6,C,"ZipSlip / path traversal in decompression","ZIP 解压路径穿越漏洞","CompressMiddleware.Extract doesn't validate ../ in zip entry paths against a whitelist before extraction","Validate zip entry paths against target directory; reject entries with ../","CompressMiddleware, ZipMiddleware","Scan entry names before extraction for path traversal patterns","zip slip, path traversal, security, 解压漏洞, 目录穿越" +C7,C,"Cross-tenant version leakage in multi-tenant deployment","多租户部署中跨租户版本泄露","Server API returns version info without tenant isolation; ProductId not validated against tenant context","Update server to validate ProductId against tenant context; add tenant-scoped API keys","GeneralSpacestation, ProductId","Add tenant header validation middleware on server side","multi-tenant, cross-tenant, security, 多租户, 跨租户, ProductId" +C8,C,"EventManager.Instance returned after Dispose","EventManager.Dispose 后 Instance 仍可访问","EventManager.Instance property returns the instance even after Dispose() is called","Call Clear() first, then Dispose(); do not call Instance after Dispose","EventManager.Instance, EventManager.Dispose","Wrap in try-finally; set to null after dispose","EventManager, dispose, singleton, memory leak, after dispose" +H1,H,"Chinese text garbled on Linux/macOS","Linux/macOS 中文乱码","IPC file encoding defaults to system default, not UTF-8","Set Encoding.UTF8 in PipelineContext and Configinfo","PipelineContext Encoding, Configinfo","Always explicitly set Encoding.UTF8","Chinese garbled, encoding, UTF8, 乱码, 编码, Linux, macOS" +H2,H,"Infinite update loop","无限升级循环","manifest.json version number doesn't match actual installed version or write-back fails","Ensure manifest.json version number is correct; implement write-back after update","generalupdate.manifest.json, ClientVersion","Add version write-back logic after successful update","infinite loop, 死循环, version mismatch, manifest" +H3,H,"MultiDownloadCompletedEventArgs.IsComplated typo causes binding failure","MultiDownloadCompletedEventArgs.IsComplated 拼写错误导致绑定失败","v10.4.6 API has typo IsComplated (not IsCompleted). UI bindings using correct spelling get null","Use IsComplated in code; or write a wrapper property that redirects to IsComplated","MultiDownloadCompletedEventArgs","Wrap in adapter class with IsCompleted alias","IsComplated, IsCompleted, typo, spelling, binding, 拼写错误" +H4,H,"OSS mode doesn't distinguish Main vs Upgrade update packages","OSS 模式不区分 Main/Upgrade 更新包","OSS strategy treats all packages the same; no MainOnly/UpgradeOnly differentiation","Accept this behavior for OSS; upgrade to standard server strategy if you need fine-grained control","OSSStrategy","Use standard server strategy if Main/Upgrade differentiation is needed","OSS, MainOnly, UpgradeOnly, 不区分, OSS 策略" +H5,H,"Upgrade.exe must be in update/ subdirectory for OSS","OSS 模式下 Upgrade.exe 必须在 update/ 子目录","OSS strategy scans update/ directory; placing Upgrade.exe elsewhere won't be found","Place Upgrade.exe in update/ subdirectory from the first release","OSSStrategy, UpdatePath","Verify directory structure before first OSS deployment","OSS, update directory, subdirectory, 子目录, OSS 部署" +H6,H,"EventManager concurrency race on add/remove/dispatch","EventManager 并发竞争问题","EventManager uses List<> not concurrent collection; add/remove during dispatch causes race","Use lock or ConcurrentBag for listener storage","EventManager.cs","Avoid modifying listeners while dispatch is in progress","concurrency, EventManager, thread safe, 并发, 线程安全" +H7,H,"Container (IoC) disposed entry can cause ObjectDisposedException","容器释放后访问导致 ObjectDisposedException","AutoFac container holds singleton references; after disposal access to Instance throws","Check container.IsDisposed before accessing Instance; add null check wrapper","GeneralUpdateBootstrap, DI container","Wrap container access in try-catch (ObjectDisposedException)","container, disposed, ObjectDisposedException, IoC, AutoFac" +H8,H,"Cross-version (CVP) jump skips API compatibility checks","跨版本跳转跳过 API 兼容性检查","CVP strategy allows jumping arbitrary number of versions; no intermediate API compatibility validation","Ensure server-side compatibility validation between source and target versions","CrossVersionStrategy, CVP","Test API compatibility for each version pair before deployment","CVP, cross version, API compatibility, 跨版本, API 兼容" +H9,H,"Linux: Pipeline hash algorithm platform-specific differences","Linux 上哈希算法平台差异","HashMiddleware may use platform-specific crypto implementations; hash mismatch between build and deploy","Use cross-platform hash algorithm (SHA256 consistently)","HashMiddleware, HashAlgorithm","Set HashAlgorithmName explicitly to SHA256","Linux, hash, SHA256, 哈希, 平台差异" +H10,H,"SignalR HubConnection dispose-then-reconnect crash","SignalR HubConnection 释放后重连崩溃","HubConnection.Dispose() sets internal state; reconnecting without new instance crashes","Set HubConnection to null after Dispose; create new instance for reconnect","SignalR, HubConnection, Dispose","Always null-check before reconnecting","SignalR, HubConnection, reconnect, 重连, 推送" +H11,H,"Bowl v10.4.6 has no public LaunchAsync method","Bowl v10.4.6 无公开 LaunchAsync 方法","v10.4.6 Bowl class only provides base type definitions; LaunchAsync is not publicly exposed","Use Bowl with v10.5.0+ dev branch; in v10.4.6 only basic type definitions are available","GeneralUpdate.Bowl.Bowl, MonitorParameter","Bowl parameters can be configured but execution requires dev branch","Bowl, LaunchAsync, 崩溃守护, 守护进程" +M1,M,"Differential package referenced unnecessarily","不必要地引用了 Differential 包","Developers add GeneralUpdate.Differential package not knowing types are already in Core","Don't add GeneralUpdate.Differential separately; the types are embedded in GeneralUpdate.Core","GeneralUpdate.Core, GeneralUpdate.Differential","Remove any existing GeneralUpdate.Differential reference from csproj","Differential, NuGet, 差分, 额外引用" +M2,M,"Wrong EventArgs type used in event listeners","事件监听到用了错误的 EventArgs 类型","Using ProgressEventArgs instead of MultiDownloadStatisticsEventArgs or vice versa","Use exact EventArgs type from GeneralUpdate.Common.Download namespace","MultiDownloadStatisticsEventArgs, ProgressEventArgs","Always check the namespace before importing event args types","event args, EventArgs, 事件参数, 错误类型" +M3,M,"Missing AddListenerException leads to silent failures","未订阅 ExceptionEventArgs 导致静默失败","Without AddListenerException, non-critical exceptions are silently swallowed","Always add AddListenerException handler; at minimum log the exception message","AddListenerException, ExceptionEventArgs","Add a default handler that logs to file even in production","silent failure, exception, 静默失败, 未订阅" +M4,M,"Version number not 4-segment (x.y.z.w)","版本号不是 4 段式","Server or client uses 3-segment version (1.0.0); comparison logic expects 4 segments","Always use 4-segment version format: x.y.z.w","ClientVersion, VersionInfo","Pad to 4 segments at server response level","version, 4 segment, 版本号, 三段式" +M5,M,"InstallPath set to relative path causes file resolution failure","InstallPath 使用相对路径导致文件解析失败","Relative InstallPath resolved differently in Client vs Upgrade context","Use absolute path: AppDomain.CurrentDomain.BaseDirectory for production","Configinfo.InstallPath","Use Path.GetFullPath() to resolve at runtime","InstallPath, relative path, 相对路径, 安装路径" +M6,M,"UpdateUrl returns success but empty body causes null reference","UpdateUrl 返回成功但空响应体导致空引用","Server API returns 200 OK with null/empty body; bootstrap doesn't handle null Info.Body","Add null check on e.Info?.Body before iterating VersionInfo list","UpdateInfoEventArgs, VersionInfo","Wrap version list access in null-conditional operator","null reference, empty response, 空响应, 空引用, body null" +M7,M,"Version comparison uses string instead of semantic version","版本比较使用字符串而非语义版本","Version strings compared lexicographically (1.9.0.0 > 1.10.0.0)","Use Version class (System.Version) or a semantic version parser for comparison","ClientVersion, Version.Parse","Cast to System.Version before comparison","version compare, semantic version, 版本比较, 语义版本" +M8,M,"BlackFiles/BlackFormats patterns not applied to upgrade directory","黑名单模式未应用到升级目录","BlackFiles patterns only apply to main app directory; upgrade path files not filtered","Apply same blacklist logic to upgrade directory; or use separate upgrade-specific blacklist","Configinfo.BlackFiles, Configinfo.BlackFormats","Duplicate blacklist entries for both main and upgrade paths","blacklist, BlackFiles, 黑名单, 升级目录" +M9,M,"Update process killed due to watchdog timeout on slow operations","慢操作导致升级进程被监控超时杀死","Upgrade process has a hard timeout; large file operations exceed the limit","Increase timeout for large updates; show progress to watchdog; split large operations","GeneralUpdateBootstrap, Watchdog","Show periodic progress to prevent watchdog timeout","timeout, watchdog, killed, 超时, 被杀" +M10,M,"Single-process mode not supported in v10.4.6","v10.4.6 不支持单进程模式","Developers expect to run update in same process; v10.4.6 requires dual-process architecture","Accept dual-process requirement; document that single-process is not supported","GeneralUpdateBootstrap, AppType","Not supported in v10.4.6; consider v10.5.0+ for single-process","single process, 单进程, dual process, 双进程" +M11,M,"SignalR push update delivery not acknowledged","SignalR 推送更新无送达确认","Push notification sent but no acknowledgment; client may miss the update while offline","Implement client-side acknowledgment; use retry queue on server for unacknowledged pushes","SignalRStrategy, HubConnection","Add ACK from client; server retries unacknowledged pushes","SignalR, acknowledgment, 推送确认, 送达" +M12,M,"Pre-release version included in production update list","预发布版本被包含在生产更新列表中","Server returns pre-release/beta versions in the production version list; client updates to wrong version","Filter versions by release channel; separate pre-release and production version lists","GeneralSpacestation, VersionInfo","Add release channel field to version metadata","pre-release, beta, 预发布, 测试版本" +M13,M,"OssClient.AppType value 3-4 not supported in v10.4.6","v10.4.6 不支持 OssClient.AppType(值 3-4)","v10.4.6 only supports AppType values 1 (Client) and 2 (Upgrade); 3-4 are dev-branch only","Use AppType.ClientApp (1) or AppType.UpgradeApp (2) only","AppType, ClientApp, UpgradeApp","Avoid values 3-4 in v10.4.6","OssClient, AppType, 3-4, not supported, 不支持" +M14,M,"ProgressEventArgs used in v10.4.6 API where MultiDownload* expected","ProgressEventArgs 与 MultiDownload* 混淆","v10.4.6 uses MultiDownload* events for batch downloads; ProgressEventArgs is for single download only","Use MultiDownloadStatisticsEventArgs for batch download; ProgressEventArgs for single file","MultiDownloadStatisticsEventArgs, ProgressEventArgs","Check event type before using in v10.4.6","progress, event, 进度事件, 混淆" +M15,M,"Custom pipeline middleware order wrong causes silent failure","自定义中间件顺序错误导致静默失败","Middleware registered in wrong order (Patch before Compress); no error but update silently broken","Follow correct order: Hash -> Compress -> Patch -> Drivelution","PipelineBuilder, UseMiddleware","Document and verify middleware order","pipeline, middleware order, 管道, 中间件顺序" +M16,M,"ConfigurationProviderFactory.Providers dictionary not thread-safe","ConfigurationProviderFactory.Providers 字典非线程安全","Dictionary used without synchronization; concurrent access in multi-threaded context may corrupt","Use ConcurrentDictionary or external lock for Providers access","ConfigurationProviderFactory","Replace Dictionary with ConcurrentDictionary","thread safety, ConfigurationProviderFactory, 线程安全" +M17,M,"Socket/HttpClient not disposed after download completes","下载完成后 Socket/HttpClient 未释放","HttpClient instances created per request without reuse or disposal","Use singleton HttpClient; or wrap in using block","MultiDownloadExecutor, HttpClient","Configure HttpClient as singleton in DI container","HttpClient, socket leak, 连接泄漏, 未释放" +M18,M,"LaunchAsync() returns Task not Task","LaunchAsync() 返回 Task 而非 Task","Developers expect bool return type for success/failure check; actual return is Bootstrap instance","The returned Bootstrap instance can be inspected; do not expect bool","GeneralUpdateBootstrap.LaunchAsync","Ignore return value; use event listeners for status","LaunchAsync, return type, Task, 返回值" +M19,M,"Silent mode notifications don't respect Do Not Disturb settings","静默模式通知不遵守免打扰设置","Silent update notifications pop up even when system is in DND or presentation mode","Check system DND status before showing notifications; queue notifications for later","SilentStrategy, Notification","Use OS-level DND API before showing notification","silent, notification, DND, 通知, 免打扰" +M20,M,"Service mode (Windows Service) IPC path resolution difference","Windows 服务模式下 IPC 路径解析差异","Windows Services run with different working directory than user apps; IPC file path resolved incorrectly","Use absolute paths for IPC files; ensure service account has write access to IPC directory","IPC, ProcessInfoJsonContext","Always use full absolute path for IPC file","Windows Service, IPC, path, 服务, 路径" +L1,L,"Thread.Sleep in event listener blocks update pipeline","事件监听中使用 Thread.Sleep 阻塞更新管道","Synchronous waits in event handlers block the pipeline execution","Use async event handlers; avoid Thread.Sleep/Task.Wait in any event listener","AddListenerException, AddListenerMultiDownloadStatistics","Replace Sleep with Task.Delay and async pattern","block, sleep, 阻塞, 事件监听" +L2,L,"Hardcoded temporary path in unzip operations","解压操作中硬编码临时路径","CompressMiddleware uses hardcoded temp path; conflicts with system temp policy","Use Path.GetTempPath() or configurable temp directory","CompressMiddleware, ZipMiddleware","Override via environment variable if needed","temp path, hardcoded, 临时路径, 硬编码" +L3,L,"Differential clean/dirty parameter validation missing","差分 clean/dirty 参数缺失验证","DifferentialCore.CleanAsync/Core.DirtyAsync doesn't validate input paths","Validate source/target/patch directories exist before calling","DifferentialCore.CleanAsync, DifferentialCore.DirtyAsync","Add manual directory existence checks","differential, parameter validation, 差分, 参数校验" +L4,L,"ServiceCollection registration not validated before Build","Build 前 ServiceCollection 注册未验证","Multiple registrations of same type; last wins silently","Use TryAdd instead of Add for service registration to detect duplicates","GeneralUpdateBootstrap, ServiceCollection","Replace Add with TryAdd patterns","DI, registration, duplicate, ServiceCollection" +L5,L,"Process memory tracking via private bytes not working-memory","进程内存跟踪使用 private bytes 而非 working set","Private bytes doesn't reflect actual memory pressure; working set is more meaningful for GC pressure","Use WorkingSet64 instead of PrivateMemorySize64 for memory monitoring","ProcessMonitor, MemoryMetrics","Switch to WorkingSet64 for memory threshold monitoring","memory, private bytes, working set, 内存跟踪" +L6,L,"Drivelution middleware exception loses context","Drivelution 中间件异常丢失上下文","Driver installation failure exception is caught without original stack trace or driver name","Wrap exception with driver name and operation context before rethrowing","DrivelutionMiddleware, GeneralDrivelution","Log driver name and operation at entry point","Drivelution, exception, 驱动, 异常" +L7,L,"OSS container/region info hardcoded in example code","OSS 示例代码中硬编码了容器/区域信息","Sample code uses hardcoded OSS endpoint/bucket values; needs env-based configuration","Use environment variables or config file for OSS endpoint and bucket","OSSStrategy, OssSample","Extract OSS config to environment variables","OSS, hardcoded, 硬编码, 示例, endpoint" +L8,L,"SkipDirectorys empty list doesn't use defaults","SkipDirectorys 空列表不使用默认值","Empty SkipDirectorys list skips nothing; default skip patterns not applied","Set SkipDirectorys explicitly even if using default patterns","Configinfo.SkipDirectorys","Always set SkipDirectorys with at least basic patterns","SkipDirectorys, empty list, 跳过目录, 空列表" +L9,L,"Bowl process name may match multiple processes","Bowl 进程名可能匹配多个进程","ProcessNameOrId uses string match; multiple processes with same name monitored instead of one","Use PID instead of process name for Bowl monitoring, or use full path","Bowl, MonitorParameter, ProcessNameOrId","Use exact process name with extension (MyApp.exe) to narrow match","Bowl, process name, multiple, 多个进程" +L10,L,"Linux: missing chmod +x on downloaded UpgradeApp","Linux 上下载的 UpgradeApp 缺少执行权限","File permissions not set after download; no IUpdateHooks in v10.4.6 to fix it","Manually chmod +x after download, or use post-update script","GeneralUpdateBootstrap, LinuxStrategy","Add chmod command to deployment script","Linux, chmod, permission, 权限, 执行" +L11,L,"WinForms: ShowDialog in update event blocks IPC","WinForms ShowDialog 阻塞 IPC 通信","Modal dialog shown during update event blocks the IPC writing thread","Use non-modal forms or async dialog patterns","WinForms, IPC","Replace ShowDialog with Show + event-based close","WinForms, ShowDialog, IPC, 模态框" +L12,L,"VersionRespDTO nullable fields cause null warnings","VersionRespDTO 可空字段导致空警告","Some fields in VersionRespDTO are nullable; no null handling in consuming code","Add null coalescing operators (??) when accessing VersionRespDTO properties","VersionRespDTO, VersionInfo","Use pattern matching before accessing nullable fields","nullable, 可空, null warning, VersionInfo" diff --git a/cli/assets/data/strategies.csv b/cli/assets/data/strategies.csv new file mode 100644 index 0000000..8529aaf --- /dev/null +++ b/cli/assets/data/strategies.csv @@ -0,0 +1,7 @@ +id,name,description,server_required,best_for,pros,cons,keywords +S01,Standard Client-Server,"Standard dual-process update with GeneralSpacestation backend",yes,"First-time users, enterprise apps with backend","Full control over version management; fine-grained Main/Upgrade differentiation; supports all event types","Requires GeneralSpacestation or compatible backend; higher deployment complexity","standard, client-server, GeneralSpacestation, 标准, 客户端-服务端" +S02,OSS Object Storage,"Update via S3/MinIO/cloud object storage; no backend server needed",no,"Small projects, startups, no-backend scenarios, cost-sensitive","Zero server cost; simple deployment; global CDN support; scales automatically","No Main/Upgrade differentiation; Upgrade.exe must be in update/ subdirectory; no real-time version control","OSS, S3, MinIO, 对象存储, 无服务器" +S03,Silent Update,"Background polling update with minimal user interruption",yes,"Long-running apps, kiosk systems, background services","User-unaware updates; configurable poll interval; can pair with any strategy","Requires notification strategy; poll interval tuning; no user feedback loop","silent, background, polling, 静默, 后台, 轮询" +S04,Differential Update,"Delta patch update to save bandwidth (BSDIFF/HDiffPatch)",yes,"Large applications, bandwidth-constrained networks, frequent updates","60-90% bandwidth reduction; quick patch application; works over any transport","Requires differential build pipeline on server; patch size limited (avoid >2GB); BSDIFF integer overflow risk in older versions","differential, delta, patch, BSDIFF, HDiffPatch, 差分, 增量" +S05,Cross-Version CVP,"Skip intermediate versions and jump directly to target version",yes,"Skip multiple versions, forced updates, long-unupdated clients","Bypass intermediate versions; reduce update steps; force minimum version compliance","Requires CVP build pipeline; API compatibility risk for large jumps; full compatibility testing needed","CVP, cross version, skip, 跨版本, 跳版本" +S06,SignalR Push,"Server-initiated push update via SignalR real-time connection",yes,"Real-time apps, urgent updates, managed fleets","Instant update notification; server-controlled rollout; targeted version deployment","Requires persistent connection; offline clients miss pushes; HubConnection lifecycle management; disposal/reconnect complexity","SignalR, push, real-time, 推送, 实时" diff --git a/cli/assets/scripts/core.py b/cli/assets/scripts/core.py new file mode 100644 index 0000000..8b7140d --- /dev/null +++ b/cli/assets/scripts/core.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GeneralUpdate Search Core - BM25 search engine for troubleshooting issues and strategies. +""" +import csv +import re +import os +from pathlib import Path +from math import log +from collections import defaultdict + +# Allow DATA_DIR override via environment variable (for testing) +_default_data_dir = Path(__file__).resolve().parent.parent / "data" +DATA_DIR = Path(os.environ.get("GENERALUPDATE_DATA_DIR", str(_default_data_dir))) +MAX_RESULTS = 3 + +CSV_CONFIG = { + "issue": { + "file": "issues.csv", + "search_cols": ["symptom_en", "symptom_zh", "cause", "keywords"], + "output_cols": ["id", "severity", "symptom_en", "symptom_zh", "cause", "solution", "code_ref", "workaround"], + }, + "strategy": { + "file": "strategies.csv", + "search_cols": ["name", "description", "best_for", "keywords"], + "output_cols": ["id", "name", "description", "server_required", "best_for", "pros", "cons", "keywords"], + }, +} + +class BM25: + """BM25 ranking algorithm for text search""" + def __init__(self, k1=1.5, b=0.75): + self.k1 = k1 + self.b = b + self.corpus = [] + self.doc_lengths = [] + self.avgdl = 0 + self.idf = {} + self.doc_freqs = defaultdict(int) + self.N = 0 + + def tokenize(self, text): + """Tokenize text: split CJK into character unigrams + bigrams, keep English words.""" + text = str(text).lower() + result = [] + + # Split into CJK and non-CJK segments + segments = re.split(r'([一-鿿㐀-䶿]+)', text) + for seg in segments: + if not seg: + continue + if re.match(r'^[一-鿿㐀-䶿]+$', seg): + # CJK: char unigrams + bigrams + chars = list(seg) + # Unigrams + result.extend(chars) + # Bigrams + for i in range(len(chars) - 1): + result.append(chars[i] + chars[i+1]) + else: + # English/alphanumeric: clean punctuation, keep words + cleaned = re.sub(r'[^\w\s]', ' ', seg) + for w in cleaned.split(): + w = w.strip() + if len(w) > 1 and not w.isdigit(): + result.append(w) + + return result + + def fit(self, corpus): + self.corpus = corpus + self.doc_lengths = [len(doc) for doc in corpus] + self.avgdl = sum(self.doc_lengths) / len(corpus) if corpus else 0 + self.N = len(corpus) + + for doc in corpus: + seen = set() + for term in doc: + if term not in seen: + self.doc_freqs[term] += 1 + seen.add(term) + + for term, df in self.doc_freqs.items(): + self.idf[term] = log((self.N - df + 0.5) / (df + 0.5) + 1.0) + + def score(self, query_terms, doc_idx): + doc = self.corpus[doc_idx] + doc_len = self.doc_lengths[doc_idx] + score = 0.0 + for term in query_terms: + if term in self.idf: + tf = doc.count(term) + score += self.idf[term] * (tf * (self.k1 + 1)) / (tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)) + return score + + def search(self, query, top_n=MAX_RESULTS): + query_terms = self.tokenize(query) + if not query_terms or not self.corpus: + return [] + scores = [(i, self.score(query_terms, i)) for i in range(len(self.corpus))] + scores.sort(key=lambda x: x[1], reverse=True) + return [(idx, score) for idx, score in scores[:top_n] if score > 0] + + +def load_csv(filepath): + """Load CSV file and return list of dicts.""" + full_path = DATA_DIR / filepath + if not full_path.exists(): + return [] + with open(full_path, 'r', encoding='utf-8') as f: + return list(csv.DictReader(f)) + + +def search(query, domain="issue", max_results=MAX_RESULTS): + """Search across issues or strategies domain.""" + if domain not in CSV_CONFIG: + return {"error": f"Unknown domain: {domain}. Available: {list(CSV_CONFIG.keys())}"} + + config = CSV_CONFIG[domain] + data = load_csv(config["file"]) + + if not data: + return {"error": f"No data found for domain: {domain}"} + + # Build corpus + corpus = [] + for row in data: + doc_text = " ".join(str(row.get(col, "")) for col in config["search_cols"]) + corpus.append(BM25().tokenize(doc_text)) + + bm25 = BM25() + bm25.fit(corpus) + results = bm25.search(query, max_results) + + output = [] + for idx, score in results: + row = data[idx] + row["_score"] = round(score, 2) + output.append(row) + + return { + "domain": domain, + "query": query, + "file": config["file"], + "count": len(output), + "results": output, + } diff --git a/cli/assets/scripts/generate.py b/cli/assets/scripts/generate.py new file mode 100644 index 0000000..9e222ae --- /dev/null +++ b/cli/assets/scripts/generate.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GeneralUpdate Code Generator — generates production-ready C# integration code. +Usage: python3 scripts/generate.py --framework wpf --strategy oss --output ./Generated + +Supports 288 combinations: 4 scenes × 6 strategies × 6 UI frameworks × 2 Bowl + +Combinations: + Scenes: None, UpgradeOnly, MainOnly, Both + Strategies: standard, oss, silent, differential, cvp, push + Frameworks: wpf-原生, wpf-layui, wpf-wpfdevelopers, winforms-antdui, avalonia-semiursa, maui, console + Bowl: yes, no +""" +import argparse +import os +import sys +from pathlib import Path +from string import Template +import json + +SCRIPT_DIR = Path(__file__).parent +TEMPLATES_DIR = SCRIPT_DIR / "generate" / "templates" + +# ============ CONFIG MATRIX ============ + +STRATEGIES = { + "standard": { + "name": "Standard Client-Server", + "slug": "standard", + "description": "Standard dual-process update with GeneralSpacestation backend", + }, + "oss": { + "name": "OSS Object Storage", + "slug": "oss", + "description": "Update via S3/MinIO/cloud object storage; no backend server needed", + "warning": "OSS模式不区分 Main/Upgrade 更新包。Upgrade.exe 必须放在 update/ 子目录。", + }, + "silent": { + "name": "Silent Update", + "slug": "silent", + "description": "Background polling update with minimal user interruption", + }, + "differential": { + "name": "Differential Update", + "slug": "differential", + "description": "Delta patch update to save bandwidth (BSDIFF/HDiffPatch)", + "warning": "差分包大小建议不超过 2GB,避免 BSDIFF 整数溢出(v10.4.6+ 已修复 #514)。", + }, + "cvp": { + "name": "Cross-Version CVP", + "slug": "cvp", + "description": "Skip intermediate versions and jump directly to target version", + "warning": "跨版本跳转需要服务端 API 兼容性验证,避免跳转后 API 不匹配。", + }, + "push": { + "name": "SignalR Push", + "slug": "push", + "description": "Server-initiated push update via SignalR real-time connection", + "warning": "HubConnection Dispose 后必须置 null。离线客户端可能错过推送。", + }, +} + +UI_FRAMEWORKS = { + "wpf-原生": {"class": "WpfNative", "uses_xaml": True, "needs_dispatcher": True}, + "wpf-layui": {"class": "WpfLayUI", "uses_xaml": True, "needs_dispatcher": True}, + "wpf-wpfdevelopers": {"class": "WpfDevelopers", "uses_xaml": True, "needs_dispatcher": True}, + "winforms-antdui": {"class": "WinFormsAntdUI", "uses_xaml": False, "needs_dispatcher": True}, + "avalonia-semiursa": {"class": "AvaloniaSemiUrsa", "uses_xaml": True, "needs_dispatcher": True}, + "maui": {"class": "MauiApp", "uses_xaml": True, "needs_dispatcher": False}, + "console": {"class": "ConsoleApp", "uses_xaml": False, "needs_dispatcher": False}, +} + + +def load_template(name): + path = TEMPLATES_DIR / name + if not path.exists(): + print(f"⚠️ Template not found: {path}") + return "" + return path.read_text(encoding="utf-8") + + +def render(template_text, variables): + """Simple {{PLACEHOLDER}} substitution with defaults for optional sections.""" + result = template_text + for key, value in variables.items(): + result = result.replace("{{" + key + "}}", str(value)) + + # Handle optional sections: {{#KEY}}content{{/KEY}} or {{^KEY}}content{{/KEY}} + # Simple positive conditional blocks + def replace_conditional(match): + key = match.group(1) + content = match.group(2) + if variables.get(key, False) and str(variables.get(key, "")).lower() in ("true", "yes", "1", key): + return content + return "" + + result = re.sub(r"\{\{#(\w+)\}\}(.*?)\{\{/\1\}\}", replace_conditional, result, flags=re.DOTALL) + + # Negative conditional blocks {{^KEY}}...{{/KEY}} + def replace_negative(match): + key = match.group(1) + content = match.group(2) + if not variables.get(key, False) or str(variables.get(key, "")).lower() in ("false", "no", "0", ""): + return content + return "" + + result = re.sub(r"\{\{(\^)(\w+)\}\}(.*?)\{\{/\2\}\}", replace_negative, result, flags=re.DOTALL) + + return result + + +def generate_bootstrap(strategy, framework, with_bowl, scenes, variables): + """Generate Bootstrap.cs integration code.""" + templ = load_template("Bootstrap.cs.template") + + listener_blocks = [] + + # Always include basic listeners + listener_names = ["MultiDownloadStatistics", "MultiDownloadCompleted", + "MultiDownloadError", "MultiAllDownloadCompleted", "Exception"] + + if framework != "console": + listener_names = ["MultiDownloadStatistics", "MultiDownloadCompleted", + "MultiDownloadError", "MultiDownloadCompleted", + "MultiAllDownloadCompleted", "Exception"] + + if framework == "console": + # Console just writes to stdout + listeners_templ = load_template("listeners_console.cs.template") + elif framework.startswith("wpf") or framework == "avalonia-semiursa": + listeners_templ = load_template("listeners_mvvm.cs.template") + elif framework == "winforms-antdui": + listeners_templ = load_template("listeners_winforms.cs.template") + elif framework == "maui": + listeners_templ = load_template("listeners_maui.cs.template") + else: + listeners_templ = load_template("listeners_console.cs.template") + + listeners_code = render(listeners_templ, variables) + + bowl_notice = "" + if with_bowl: + bowl_notice = load_template("bowl_notice.cs.template") + bowl_notice = render(bowl_notice, variables) + + strategy_notice = "" + if strategy in STRATEGIES and STRATEGIES[strategy].get("warning"): + strategy_notice = f"// ⚠️ {STRATEGIES[strategy]['warning']}\n" + + code = render(templ, { + **variables, + "LISTENERS": listeners_code, + "BOWL_NOTICE": bowl_notice, + "STRATEGY_WARNING": strategy_notice, + }) + + return code + + +def generate_manifest(variables): + """Generate generalupdate.manifest.json.""" + templ = load_template("manifest.json.template") + return render(templ, variables) + + +def generate_upgrade_program(variables): + """Generate UpgradeProgram.cs.""" + templ = load_template("UpgradeProgram.cs.template") + return render(templ, variables) + + +def generate_deployment_checklist(strategy, framework, with_bowl, variables): + """Deployment checklist Markdown.""" + templ = load_template("DeploymentChecklist.md.template") + return render(templ, variables) + + +def generate_issue_warnings(strategy, variables): + """Generate known issue warnings for the specific config combination.""" + warnings_map = { + "oss": """⚠️ OSS 特有已知问题: + - H4: OSS 不区分 Main/Upgrade 更新包,接受此行为 + - H5: Upgrade.exe 必须放在 update/ 子目录 + - L7: 示例代码中 OSS endpoint/bucket 写死,建议用环境变量 + - M13: OssClient.AppType 值 3-4 在 v10.4.6 不支持""", + "silent": """⚠️ 静默更新特有已知问题: + - H2: 无限升级循环 — 确保 manifest.json 版本号正确 + - M19: 静默通知可能不尊重系统的免打扰设置 + - M9: 升级进程超时 — 大文件操作建议增加超时时间""", + "differential": """⚠️ 差分更新特有已知问题: + - C3: BSDIFF 整数溢出 — 差分包 < 2GB + - M1: 不要额外引用 GeneralUpdate.Differential(已嵌入 Core) + - L3: 差分 clean/dirty 参数缺少验证,建议手动检查路径 + - L5: 进程内存跟踪使用 private bytes 而非 working set""", + "cvp": """⚠️ CVP 跨版本特有已知问题: + - H8: 跨版本跳转跳过 API 兼容性检查 — 服务端需要验证 + - C7: 多租户跨租户版本泄露风险 — 确认 ProductId 隔离""", + "push": """⚠️ SignalR 推送特有已知问题: + - H10: HubConnection Dispose 后重连崩溃 — 置 null + - M11: 推送更新无送达确认 — 建议实现 ACK 机制 + - M9: 慢操作超时场景下连接可能断开""", + "standard": """⚠️ 标准策略已知问题(非特有但常见): + - C1: UpgradeApp.exe 必须随首个版本发布 + - C2: Client/Upgrade NuGet 版本必须一致 + - H3: IsComplated 拼写(注意不是 IsCompleted) + - M5: InstallPath 使用相对路径导致文件解析失败 + - M6: UpdateUrl 返回空响应体时做 null 检查""", + } + warning = warnings_map.get(strategy, "该策略组合暂无特别预警。") + + t = load_template("IssuesWarning.md.template") + return render(t, {**variables, "WARNINGS_LIST": warning}) + + +def generate_strategy_checks(strategy): + """Return strategy-specific checklist items.""" + checks = { + "oss": """- [ ] Bucket 权限设置为私有 +- [ ] Upgrade.exe 放在 update/ 子目录 +- [ ] 接受 OSS 不区分 Main/Upgrade 的限制 +- [ ] 包名包含版本号 (如 MyApp_1.0.0.0.zip)""", + "silent": """- [ ] 轮询间隔 30-60 分钟 +- [ ] 下载完成后通知用户重启 +- [ ] 有 WiFi/流量限制考虑""", + "differential": """- [ ] 服务端有差分包生成机制 +- [ ] Pipeline 配置了 PatchMiddleware +- [ ] 差分包 < 2GB(避免 BSDIFF 整数溢出) +- [ ] Linux/macOS 补丁兼容性已验证""", + "cvp": """- [ ] 服务端有 CVP 构建流水线 +- [ ] 源/目标版本间 API 兼容性已验证 +- [ ] 客户端数据库迁移已测试""", + "push": """- [ ] HubConnection 生命周期管理 +- [ ] 自动重连逻辑(3次,间隔递增) +- [ ] Dispose 时将连接置 null +- [ ] 推送失败降级到轮询""", + "standard": """- [ ] GeneralSpacestation 或兼容后端已部署 +- [ ] API 返回合法的版本列表 +- [ ] 4 段式版本号返回""", + } + return checks.get(strategy, "- [ ] 基本配置已验证") + + +def generate(args): + strategy = args.strategy + framework = args.framework + with_bowl = args.bowl + scenes = args.scenes + + output_dir = Path(args.output) + project_name = args.project_name or "MyApp" + app_secret = args.app_secret_key or "CHANGE-ME-TO-A-32-CHAR-SECRET-KEY!" + update_url = args.update_url or "https://your-server.com/Upgrade/Verification" + version = args.version or "1.0.0.0" + product_id = args.product_id or project_name.lower().replace(" ", "-") + "-001" + + from datetime import date + today = date.today().isoformat() + + bowl_lower = "yes" if with_bowl else "no" + variables = { + "PROJECT_NAME": project_name, + "APP_SECRET_KEY": app_secret, + "UPDATE_URL": update_url, + "CLIENT_VERSION": version, + "PRODUCT_ID": product_id, + "STRATEGY": strategy, + "STRATEGY_NAME": STRATEGIES.get(strategy, {}).get("name", strategy), + "FRAMEWORK": framework, + "FRAMEWORK_CLASS": UI_FRAMEWORKS.get(framework, {}).get("class", "App"), + "BOWL": bowl_lower, + "BOWL_UPPER": "Yes" if with_bowl else "No", + "SCENES": scenes, + "INSTALL_PATH": "AppDomain.CurrentDomain.BaseDirectory", + "DATE": today, + "STRATEGY_CHECKS": generate_strategy_checks(strategy), + } + # ISSUE_WARNINGS depends on variables being fully constructed + variables["ISSUE_WARNINGS"] = generate_issue_warnings(strategy, variables) + + # Generate files + files = {} + + # Bootstrap + bootstrap_code = generate_bootstrap(strategy, framework, with_bowl, scenes, variables) + files["Client/Integration.cs"] = bootstrap_code + + # Manifest + manifest_json = generate_manifest(variables) + files["generalupdate.manifest.json"] = manifest_json + + # Upgrade program + upgrade_code = generate_upgrade_program(variables) + files["Upgrade/UpgradeProgram.cs"] = upgrade_code + + # Deployment checklist + checklist = generate_deployment_checklist(strategy, framework, with_bowl, variables) + files["DeploymentChecklist.md"] = checklist + + # Issue warnings + warnings = generate_issue_warnings(strategy, variables) + files["IssuesWarning.md"] = warnings + + # Write files + for relpath, content in files.items(): + full_path = output_dir / relpath + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text(content, encoding="utf-8") + print(f" ✓ {relpath}") + + print(f"\n✅ Generated {len(files)} files for {STRATEGIES[strategy]['name']} + {framework}") + print(f" Output: {output_dir.resolve()}") + + +if __name__ == "__main__": + import re # needed for template rendering + + parser = argparse.ArgumentParser(description="GeneralUpdate Code Generator") + parser.add_argument("--framework", "-f", choices=list(UI_FRAMEWORKS.keys()), default="wpf-原生", + help="Target UI framework") + parser.add_argument("--strategy", "-s", choices=list(STRATEGIES.keys()), default="standard", + help="Update strategy") + parser.add_argument("--bowl", action="store_true", default=False, + help="Include Bowl crash daemon") + parser.add_argument("--scenes", default="Both", + help="Update scenes: None/UpgradeOnly/MainOnly/Both (default: Both)") + parser.add_argument("--output", "-o", default="./Generated", + help="Output directory (default: ./Generated)") + parser.add_argument("--project-name", "-n", default="MyApp", + help="Project name (default: MyApp)") + parser.add_argument("--app-secret-key", help="AppSecretKey (min 32 chars)") + parser.add_argument("--update-url", help="Update API URL") + parser.add_argument("--version", "-v", default="1.0.0.0", + help="Client version (default: 1.0.0.0)") + parser.add_argument("--product-id", help="Product ID (default: -001)") + parser.add_argument("--list", action="store_true", help="List all available combinations") + + args = parser.parse_args() + + if args.list: + print("Available strategies:") + for k, v in STRATEGIES.items(): + print(f" {k:15s} - {v['name']}") + print("\nAvailable UI frameworks:") + for k, v in UI_FRAMEWORKS.items(): + print(f" {k:20s}") + print(f"\nTotal combinations: {len(STRATEGIES)} strategies × {len(UI_FRAMEWORKS)} frameworks × 2 Bowl × 4 scenes = {len(STRATEGIES) * len(UI_FRAMEWORKS) * 2 * 4}") + sys.exit(0) + + generate(args) diff --git a/cli/assets/scripts/generate/templates/Bootstrap.cs.template b/cli/assets/scripts/generate/templates/Bootstrap.cs.template new file mode 100644 index 0000000..ddb7214 --- /dev/null +++ b/cli/assets/scripts/generate/templates/Bootstrap.cs.template @@ -0,0 +1,28 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +var config = new Configinfo +{ + // === 必需 === + UpdateUrl = "{{UPDATE_URL}}", + AppSecretKey = "{{APP_SECRET_KEY}}", + AppName = "{{PROJECT_NAME}}.exe", + MainAppName = "{{PROJECT_NAME}}.exe", + ClientVersion = "{{CLIENT_VERSION}}", + ProductId = "{{PRODUCT_ID}}", + InstallPath = {{INSTALL_PATH}}, + + // === 可选 === + Encoding = System.Text.Encoding.UTF8, +{{#BOWL}} + // Bowl 配置(仅包含 GeneralUpdate.Bowl 包,不重复添加 Core) +{{/BOWL}} +}; + +{{STRATEGY_WARNING}} +{{BOWL_NOTICE}} +await new GeneralUpdateBootstrap() + .SetConfig(config) +{{LISTENERS}} + .LaunchAsync(); diff --git a/cli/assets/scripts/generate/templates/DeploymentChecklist.md.template b/cli/assets/scripts/generate/templates/DeploymentChecklist.md.template new file mode 100644 index 0000000..eaea255 --- /dev/null +++ b/cli/assets/scripts/generate/templates/DeploymentChecklist.md.template @@ -0,0 +1,44 @@ +# Deployment Checklist — {{PROJECT_NAME}} + +Generated for: **{{STRATEGY_NAME}}** + **{{FRAMEWORK}}** | Bowl: **{{BOWL_UPPER}}** | Updated: {{DATE}} + +--- + +## ✅ Pre-Deployment Checklist + +### Bootstrap +- [ ] `Configinfo` 6 个必填字段都已设置 +- [ ] `UpdateUrl` 已指向正确的服务端 API +- [ ] `AppSecretKey` 长度 ≥ 32 字符 + +### NuGet +- [ ] Client 和 Upgrade 项目使用相同 GeneralUpdate 版本 +{{#BOWL}} +- [ ] 只引用了 `GeneralUpdate.Bowl`(未同时引 Core) +{{/BOWL}} +{{^BOWL}} +- [ ] 只引用了 `GeneralUpdate.Core` +{{/BOWL}} + +### Deployment +- [ ] UpgradeApp.exe 存在于发布目录 +- [ ] manifest.json 的 mainAppName 与进程名匹配 +- [ ] Encoding.UTF8 已设置 +- [ ] 版本号为 4 段式 (x.y.z.w) + +### Strategy-Specific +{{STRATEGY_CHECKS}} + +--- + +## ⚠️ Known Issues for This Configuration + +{{ISSUE_WARNINGS}} + +--- + +## Rollback Plan + +- [ ] 保留上一个版本的备份目录 +- [ ] 验证回滚后版本号是否正确 +- [ ] 通知用户回滚事件 diff --git a/cli/assets/scripts/generate/templates/IssuesWarning.md.template b/cli/assets/scripts/generate/templates/IssuesWarning.md.template new file mode 100644 index 0000000..62ada68 --- /dev/null +++ b/cli/assets/scripts/generate/templates/IssuesWarning.md.template @@ -0,0 +1,7 @@ +# Issues Warning — {{STRATEGY_NAME}} + {{FRAMEWORK}} + +This configuration may be affected by the following known issues: + +{{WARNINGS_LIST}} + +Cross-reference with `generalupdate-troubleshoot` reference.md for full details. diff --git a/cli/assets/scripts/generate/templates/UpgradeProgram.cs.template b/cli/assets/scripts/generate/templates/UpgradeProgram.cs.template new file mode 100644 index 0000000..aa9382c --- /dev/null +++ b/cli/assets/scripts/generate/templates/UpgradeProgram.cs.template @@ -0,0 +1,11 @@ +using GeneralUpdate.Core; + +// Upgrade 进程入口 — 从 IPC 文件读取配置,无需 SetConfig +// 注意: Upgrade 项目的 AppType 设为 2 (UpgradeApp) + +await new GeneralUpdateBootstrap() + .AddListenerException((_, e) => + { + System.Console.Error.WriteLine($"升级错误: {e.Message}"); + }) + .LaunchAsync(); diff --git a/cli/assets/scripts/generate/templates/bowl_notice.cs.template b/cli/assets/scripts/generate/templates/bowl_notice.cs.template new file mode 100644 index 0000000..9c11ee2 --- /dev/null +++ b/cli/assets/scripts/generate/templates/bowl_notice.cs.template @@ -0,0 +1,10 @@ +// ⚠️ Bowl 崩溃守护 — 使用 GeneralUpdate.Bowl,不单独引用 Core +// dotnet add package GeneralUpdate.Bowl +var bowlParam = new GeneralUpdate.Bowl.MonitorParameter +{ + ProcessNameOrId = "{{PROJECT_NAME}}.exe", + TargetPath = {{INSTALL_PATH}}, + WorkModel = "Upgrade", + FailDirectory = System.IO.Path.Combine({{INSTALL_PATH}}, "fail"), + BackupDirectory = System.IO.Path.Combine({{INSTALL_PATH}}, "backup"), +}; diff --git a/cli/assets/scripts/generate/templates/listeners_console.cs.template b/cli/assets/scripts/generate/templates/listeners_console.cs.template new file mode 100644 index 0000000..3472487 --- /dev/null +++ b/cli/assets/scripts/generate/templates/listeners_console.cs.template @@ -0,0 +1,25 @@ + .AddListenerUpdateInfo((_, e) => + { + var count = e.Info?.Body?.Count ?? 0; + Console.WriteLine($"发现 {count} 个版本待更新"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + Console.Write($"\r下载进度: {e.ProgressPercentage}% | {e.Speed}/s | 剩余 {e.Remaining}"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + Console.WriteLine($"\n版本 {e.Version} 下载完成 (IsComplated={e.IsComplated})"); + }) + .AddListenerMultiDownloadError((_, e) => + { + Console.Error.WriteLine($"\n下载失败: 版本 {e.Version} — {e.Exception?.Message}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + Console.WriteLine($"\n全部下载完成, 正在安装..."); + }) + .AddListenerException((_, e) => + { + Console.Error.WriteLine($"错误: {e.Message}"); + }) diff --git a/cli/assets/scripts/generate/templates/listeners_maui.cs.template b/cli/assets/scripts/generate/templates/listeners_maui.cs.template new file mode 100644 index 0000000..f9b07f1 --- /dev/null +++ b/cli/assets/scripts/generate/templates/listeners_maui.cs.template @@ -0,0 +1,25 @@ + .AddListenerUpdateInfo((_, e) => + { + // MAUI UI 更新需要 MainThread 和 using Microsoft.Maui.ApplicationModel; + System.Console.WriteLine($"发现 {e.Info?.Body?.Count ?? 0} 个版本待更新"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + System.Console.WriteLine($"下载进度: {e.ProgressPercentage}%"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + System.Console.WriteLine($"版本 {e.Version} 下载完成"); + }) + .AddListenerMultiDownloadError((_, e) => + { + System.Console.Error.WriteLine($"下载失败: {e.Exception?.Message}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + System.Console.WriteLine("全部下载完成, 正在安装..."); + }) + .AddListenerException((_, e) => + { + System.Console.Error.WriteLine($"错误: {e.Message}"); + }) diff --git a/cli/assets/scripts/generate/templates/listeners_mvvm.cs.template b/cli/assets/scripts/generate/templates/listeners_mvvm.cs.template new file mode 100644 index 0000000..5c97771 --- /dev/null +++ b/cli/assets/scripts/generate/templates/listeners_mvvm.cs.template @@ -0,0 +1,30 @@ + .AddListenerUpdateInfo((_, e) => + { + // ViewModel.UpdateCheckResult(e.Info?.Body) + System.Console.WriteLine($"发现 {e.Info?.Body?.Count ?? 0} 个版本待更新"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + // ViewModel: ProgressPercentage, Speed, Remaining, TotalBytesToReceive, BytesReceived + // 注意: 使用正确的 Dispatcher 更新 UI + // WPF: Application.Current.Dispatcher.Invoke(() => { ... }); + // Avalonia: Dispatcher.UIThread.Post(() => { ... }); + // 不要直接使用 Dispatcher.Invoke() — 它在不同框架中语义不同 + System.Console.WriteLine($"下载进度: {e.ProgressPercentage}%"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + System.Console.WriteLine($"版本 {e.Version} 下载完成"); + }) + .AddListenerMultiDownloadError((_, e) => + { + System.Console.Error.WriteLine($"下载失败: 版本 {e.Version} — {e.Exception?.Message}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + System.Console.WriteLine("全部下载完成, 正在安装..."); + }) + .AddListenerException((_, e) => + { + System.Console.Error.WriteLine($"错误: {e.Message}"); + }) diff --git a/cli/assets/scripts/generate/templates/listeners_winforms.cs.template b/cli/assets/scripts/generate/templates/listeners_winforms.cs.template new file mode 100644 index 0000000..cc4a45f --- /dev/null +++ b/cli/assets/scripts/generate/templates/listeners_winforms.cs.template @@ -0,0 +1,26 @@ + .AddListenerUpdateInfo((_, e) => + { + System.Console.WriteLine($"发现 {e.Info?.Body?.Count ?? 0} 个版本待更新"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + // WinForms: use Control.Invoke in your Form class + // this.Invoke((MethodInvoker)(() => { progressBar.Value = e.ProgressPercentage; })); + System.Console.WriteLine($"下载进度: {e.ProgressPercentage}%"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + System.Console.WriteLine($"版本 {e.Version} 下载完成"); + }) + .AddListenerMultiDownloadError((_, e) => + { + System.Console.Error.WriteLine($"下载失败: {e.Exception?.Message}"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + System.Console.WriteLine("全部下载完成, 正在安装..."); + }) + .AddListenerException((_, e) => + { + System.Console.Error.WriteLine($"错误: {e.Message}"); + }) diff --git a/cli/assets/scripts/generate/templates/manifest.json.template b/cli/assets/scripts/generate/templates/manifest.json.template new file mode 100644 index 0000000..7319d50 --- /dev/null +++ b/cli/assets/scripts/generate/templates/manifest.json.template @@ -0,0 +1,8 @@ +{ + "mainAppName": "{{PROJECT_NAME}}.exe", + "updateAppName": "Upgrade{{PROJECT_NAME}}.exe", + "updatePath": "./update/", + "appType": 1, + "version": "{{CLIENT_VERSION}}", + "productId": "{{PRODUCT_ID}}" +} diff --git a/cli/assets/scripts/search.py b/cli/assets/scripts/search.py new file mode 100644 index 0000000..5a790a7 --- /dev/null +++ b/cli/assets/scripts/search.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GeneralUpdate Search - BM25 search engine for troubleshooting issues and strategies. +Usage: python3 scripts/search.py "" [--domain ] [--max-results 3] + +Domains: issue (default), strategy +""" +import argparse +import sys +import io +from core import CSV_CONFIG, MAX_RESULTS, search + +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def format_output(result): + if "error" in result: + return f"Error: {result['error']}" + + output = [] + output.append(f"## GeneralUpdate Search Results") + output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}") + output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n") + + for i, row in enumerate(result['results'], 1): + severity = row.get('severity', '') + sev_icon = {'C': '🔴', 'H': '🟠', 'M': '🟡', 'L': '🔵'}.get(severity, '') + output.append(f"### {sev_icon} Result {i} (Score: {row.get('_score', 'N/A')})") + for key, value in row.items(): + if key.startswith('_'): + continue + value_str = str(value) + if len(value_str) > 500: + value_str = value_str[:500] + "..." + output.append(f"- **{key}:** {value_str}") + output.append("") + + return "\n".join(output) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="GeneralUpdate Search") + parser.add_argument("query", help="Search query") + parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), default="issue", help="Search domain") + parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)") + parser.add_argument("--json", action="store_true", help="Output as JSON") + + args = parser.parse_args() + result = search(args.query, args.domain, args.max_results) + + if args.json: + import json + print(json.dumps(result, ensure_ascii=False, indent=2)) + else: + print(format_output(result)) diff --git a/cli/assets/scripts/tests/test_search.py b/cli/assets/scripts/tests/test_search.py new file mode 100644 index 0000000..bebb065 --- /dev/null +++ b/cli/assets/scripts/tests/test_search.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Unit tests for GeneralUpdate BM25 search engine.""" +import sys +import os + +# Set DATA_DIR before importing core — test runs from scripts/ but data is at ../data +os.environ["GENERALUPDATE_DATA_DIR"] = os.path.normpath( + os.path.join(os.path.dirname(__file__), '..', '..', 'data') +) + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +import json +from core import search, CSV_CONFIG, DATA_DIR + + +def test_csv_files_exist(): + """All configured CSV data files must exist.""" + for domain, config in CSV_CONFIG.items(): + filepath = DATA_DIR / config["file"] + assert filepath.exists(), f"Missing CSV: {filepath}" + print(f" ✓ {config['file']} exists") + + +def test_issues_csv_has_all_severities(): + """Issues CSV must contain entries for all severity levels: C, H, M, L.""" + # Use a broad query that matches many issues + result = search("update error fail crash bug", "issue", 100) + assert "error" not in result, f"Search error: {result.get('error')}" + severities = {row["severity"] for row in result["results"]} + for sev in ["C", "H", "M", "L"]: + assert sev in severities, f"Missing severity level: {sev} (found: {severities})" + print(f" ✓ All severities (C/H/M/L) present: found {len(result['results'])} matching entries") + + +def test_issues_csv_required_columns(): + """Verify required columns exist by reading raw CSV.""" + import csv + filepath = DATA_DIR / CSV_CONFIG["issue"]["file"] + with open(filepath, 'r', encoding='utf-8') as f: + rows = list(csv.DictReader(f)) + required = ["id", "severity", "symptom_en", "symptom_zh", "cause", "solution"] + for row in rows: + for col in required: + assert col in row and row[col], f"Missing column '{col}' in issue {row.get('id')}" + print(f" ✓ All required columns present in {len(rows)} issues") + + +def test_strategies_csv_has_all_6(): + """Must have exactly 6 strategy entries.""" + import csv + filepath = DATA_DIR / CSV_CONFIG["strategy"]["file"] + with open(filepath, 'r', encoding='utf-8') as f: + rows = list(csv.DictReader(f)) + assert len(rows) == 6, f"Expected 6 strategies, got {len(rows)}" + ids = {r["id"] for r in rows} + for i in range(1, 7): + sid = f"S{i:02d}" + assert sid in ids, f"Missing strategy: {sid}" + print(f" ✓ All 6 strategies present") + + +def test_chinese_search_upgrade_not_start(): + """"升级后启动不了" should match C1 (top result).""" + result = search("升级后启动不了", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C1", f"Expected C1, got {top['id']}" + assert top["_score"] > 0, f"Zero score for C1" + print(f" ✓ '升级后启动不了' → C1 (score={top['_score']})") + + +def test_chinese_search_garbled(): + """"中文乱码" should match H1 (top result).""" + result = search("中文乱码", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "H1", f"Expected H1, got {top['id']}" + print(f" ✓ '中文乱码' → H1 (score={top['_score']})") + + +def test_english_search_method_not_found(): + """"method not found" should match C2.""" + result = search("method not found", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C2", f"Expected C2, got {top['id']}" + print(f" ✓ 'method not found' → C2 (score={top['_score']})") + + +def test_english_search_zip_slip(): + """"zip slip path traversal" should match C6.""" + result = search("zip slip path traversal", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C6", f"Expected C6, got {top['id']}" + print(f" ✓ 'zip slip path traversal' → C6 (score={top['_score']})") + + +def test_strategy_search_oss(): + """"OSS no backend" should match OSS strategy.""" + result = search("OSS no backend server", "strategy", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "S02", f"Expected S02 (OSS), got {top['id']}" + print(f" ✓ 'OSS no backend' → S02 (score={top['_score']})") + + +def test_strategy_search_signalr(): + """"pus" should match SignalR push strategy.""" + result = search("push real-time connection", "strategy", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "S06", f"Expected S06 (SignalR), got {top['id']}" + print(f" ✓ 'push real-time' → S06 (score={top['_score']})") + + +def test_search_json_output(): + """Search output should have correct JSON structure.""" + result = search("升级后启动不了", "issue", 3) + assert "error" not in result + assert result["domain"] == "issue" + assert result["query"] == "升级后启动不了" + assert result["file"] == "issues.csv" + assert result["count"] >= 1 + assert len(result["results"]) >= 1 + # Each result should have _score + for row in result["results"]: + assert "_score" in row + print(f" ✓ JSON structure correct") + + +def test_search_invalid_domain(): + """Invalid domain should return error.""" + result = search("test", "invalid_domain") + assert "error" in result + print(f" ✓ Invalid domain returns error") + + +def test_search_no_results(): + """Search with gibberish should return 0 results.""" + result = search("zzzzzzzxxxxxxyyyyyyy", "issue", 3) + assert "error" not in result + assert result.get("count", 0) == 0 + print(f" ✓ Gibberish search returns 0 results") + + +def test_bm25_scoring_differentiation(): + """Different queries should produce different top results.""" + r1 = search("garbled encoding chinese", "issue", 1) + r2 = search("zip slip path traversal", "issue", 1) + top1_id = r1["results"][0]["id"] + top2_id = r2["results"][0]["id"] + assert top1_id != top2_id, f"Two different queries returned same top result: {top1_id}" + print(f" ✓ BM25 differentiates queries: {top1_id} vs {top2_id}") + + +def test_all_strategies_searchable(): + """Each of the 6 strategies should be findable by keyword.""" + queries = ["standard client-server", "oss", "silent background", "differential delta", "cross version", "signalr push"] + for i, q in enumerate(queries): + r = search(q, "strategy", 1) + assert r["count"] >= 1, f"Strategy {i+1} not found by query: {q}" + print(f" ✓ All 6 strategies searchable") + + +if __name__ == "__main__": + print(f"\n🧪 GeneralUpdate Search Engine Tests\n") + tests = [fn for fn in dir() if fn.startswith("test_")] + passed = 0 + failed = 0 + for name in tests: + try: + globals()[name]() + print(f" PASS {name}") + passed += 1 + except Exception as e: + print(f" FAIL {name}: {e}") + failed += 1 + print(f"\n{'='*40}") + print(f" Total: {passed + failed} | ✅ {passed} | ❌ {failed}") + if failed: + sys.exit(1) diff --git a/cli/assets/skills/generalupdate-advanced/SKILL.md b/cli/assets/skills/generalupdate-advanced/SKILL.md new file mode 100644 index 0000000..420fa96 --- /dev/null +++ b/cli/assets/skills/generalupdate-advanced/SKILL.md @@ -0,0 +1,339 @@ +--- +name: generalupdate-advanced +description: | + Reference guide for GeneralUpdate internal architecture — Pipeline, middleware, + Strategy, Differential engine, Bowl crash monitor, FileTree, blacklist, and AOT. + Covers what is and isn't available in v10.4.6 stable release vs dev branch. + Triggers on: "extension points", "custom hooks", "Bowl", "crash dump", "IPC", + "named pipe", "shared memory", "custom strategy", "download pipeline", + "SSL policy", "auth provider", "custom download", "extension management", + "黑名单", "BlackList", "FileTree", "AOT", "NativeAOT", "高级定制", + "自定义策略", "自定义认证", "Bowl守护", "IPC替换". +when_to_use: | + - User wants to customize GeneralUpdate beyond basic Bootstrap config + - User wants crash monitoring and auto-restore (Bowl) + - User needs custom authentication provider + - User asks about AOT compatibility or trim warnings + - User needs file filtering (blacklist) or file tree diffing + - User wants to integrate Drivelution for driver updates + - User already completed basic integration and wants more control +allowed-tools: "Read, Write, Edit, Glob" +--- + +# 🔧 GeneralUpdate 高级定制参考 + +涵盖扩展点架构、Pipeline 管道、差分引擎、Bowl 崩溃守护、事件系统、文件系统工具等。 + +> ⚠️ **API 版本说明**:本指南基于 **NuGet v10.4.6 稳定版**。 +> 以下功能在稳定版中**不存在**(但在开发分支 v10.5.0-beta.2 中已有): +> - `IUpdateHooks` 生命周期钩子 +> - `IProcessInfoProvider` IPC 替换接口 +> - `SilentPollOrchestrator` 静默轮询器 +> - `Option` 可编程配置系统(v10.4.6 仅使用 `Configinfo` 属性) +> - `ISslValidationPolicy` SSL 策略接口 +> +> 各功能的可用性在文中已标注。 + +--- + +## 📋 用户需求提取(高级定制前必须确认) + +``` +### 定制目标(必需) +- 需要什么定制: ______(Bowl 崩溃守护 / IPC 替换 / Pipeline 定制 / 自定义策略 / AOT / Drivelution / 黑名单 / 认证提供者 / 差分引擎) +- 使用的 GeneralUpdate 版本: ______(v10.4.6 稳定版 / v10.5.0+ 开发分支) +- .NET 版本: ______(.NET 6/8/9/10) + +### Bowl(如果选择) +- 被监控进程名: ______ +- 工作模式: ______(Normal / Upgrade) +- 是否需要崩溃 Dump: ______(是/否) +- 备份目录路径: ______ + +### IPC 替换(如果选择) +- 替换方式: ______(NamedPipe / SharedMemory / 自定义) +- 目标平台: ______(Windows / Linux / macOS / 跨平台) +- 安全要求: ______(加密 / 签名 / 无额外安全) + +### AOT(如果选择) +- 当前剪裁警告: ______(有/无) +- 是否使用反射: ______(是/否) +- JSON 序列化需求: ______(有/无) +``` + +--- + +## 1. Pipeline 管道系统(v10.4.6 可用) + +GeneralUpdate 使用 Pipeline 管道模式处理更新包的校验、解压、补丁应用。 + +### PipelineBuilder API + +```csharp +using GeneralUpdate.Common.Internal.Pipeline; +using GeneralUpdate.Common.Internal.Strategy; + +// 创建管道上下文 +var context = new PipelineContext(); +context.Add("ZipFilePath", @"C:\temp\update.zip"); +context.Add("Hash", "sha256-hex-value"); +context.Add("Format", 0); // 0=Zip +context.Add("Encoding", System.Text.Encoding.UTF8); +context.Add("SourcePath", @"C:\Program Files\MyApp"); +context.Add("PatchEnabled", true); + +// 构建并执行管道 +await new PipelineBuilder(context) + .UseMiddleware() // 哈希校验 + .UseMiddleware() // 解压 + .UseMiddleware() // 差分补丁(需安装 Differential 包) + .Build(); +``` + +| 中间件 | 类名 | 命名空间 | 功能 | +|--------|------|---------|------| +| 哈希校验 | `HashMiddleware` | `GeneralUpdate.Core.Pipeline` | SHA256 完整性校验 | +| 解压 | `CompressMiddleware` | `GeneralUpdate.Core.Pipeline` | 解压 ZIP 包 | +| 差分补丁 | `PatchMiddleware` | `GeneralUpdate.Core.Pipeline` | 应用 BSDIFF/HDiffPatch 补丁 | +| 驱动更新 | `DrivelutionMiddleware` | `GeneralUpdate.Core.Pipeline` | Windows 驱动安装 | + +--- + +## 2. 策略系统(v10.4.6 可用) + +GeneralUpdate 内置三种平台策略,通过 `AbstractStrategy` 模板方法模式实现: + +| 策略 | 类名 | 平台 | +|------|------|------| +| Windows | `WindowsStrategy` | Windows | +| Linux | `LinuxStrategy` | Linux | +| OSS | `OSSStrategy` | 跨平台(对象存储) | + +> ⚠️ 稳定版**不支持**通过 `bootstrap.Strategy()` 注入自定义策略。 +> 自定义策略需要继承 `AbstractStrategy` 并直接调用。 + +--- + +## 3. Bowl 崩溃守护(v10.4.6 存在但功能有限) + +Bowl 是一个崩溃监控组件,通过 `MonitorParameter` 配置。 + +> ⚠️ **注意**:v10.4.6 的 Bowl 仅提供基础类型定义,`Bowl` 类没有公开的 `LaunchAsync` 方法。 +> 完整功能在开发分支(v10.5.0-beta.2)中。 + +### MonitorParameter 配置 + +```csharp +using GeneralUpdate.Bowl; +using GeneralUpdate.Bowl.Strategys; + +var param = new MonitorParameter +{ + ProcessNameOrId = "MyApp.exe", + DumpFileName = "v1.0.0.0_fail.dmp", + FailFileName = "v1.0.0.0_fail.json", + TargetPath = @"C:\Program Files\MyApp", + FailDirectory = @"C:\Program Files\MyApp\fail", + BackupDirectory = @"C:\Program Files\MyApp\backup", + WorkModel = "Upgrade", +}; + +// Bowl 实例(v10.4.6 无公开 LaunchAsync,此为占位) +var bowl = new Bowl(); +``` + +完整 Bowl 崩溃守护功能请关注 GeneralUpdate 后续版本。 + +--- + +## 4. EventManager 事件系统(v10.4.6 可用) + +EventManager 是一个全局单例,提供事件的发布和订阅: + +```csharp +using GeneralUpdate.Common.Internal.Event; + +// 添加监听 +EventManager.Instance.AddListener((object? sender, UpdateInfoEventArgs e) => +{ + // 处理版本发现事件 +}); + +// 手动分发事件 +EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, "自定义错误")); + +// 清空所有监听 +EventManager.Instance.Clear(); + +// 释放 +EventManager.Instance.Dispose(); +``` + +> ⚠️ EventManager 是全局单例,`Dispose()` 后 `Instance` 仍然可访问(代码审计发现)。 + +--- + +## 5. 文件系统工具(v10.4.6 可用) + +### BlackList(黑名单) + +`Configinfo` 支持通过以下属性排除文件: + +```csharp +var config = new Configinfo +{ + // ... + BlackFiles = new List { "*.log", "*.tmp" }, + BlackFormats = new List { ".pdb", ".vshost.exe" }, + SkipDirectorys = new List { "logs", "cache", "temp" }, +}; +``` + +### FileTree(文件树对比) + +```csharp +using GeneralUpdate.Common.FileBasic; + +var tree = new FileTree(); +var snapshot = tree.CreateSnapshot(@"C:\Program Files\MyApp"); +// 或从 StorageManager 获取比较结果 +``` + +--- + +## 6. 差分引擎(v10.4.6 可用,需安装 Differential 包) + +安装 `GeneralUpdate.Differential` 包后可用: + +```csharp +// DifferentialCore 提供核心差分能力 +using GeneralUpdate.Differential; + +// 清理模式(服务端):对比新旧版本生成补丁 +await DifferentialCore.CleanAsync(srcDir, tgtDir, patchDir); + +// 脏模式(客户端):应用补丁 +await DifferentialCore.DirtyAsync(installDir, patchDir); +``` + +自定义匹配器(v10.4.6 可用): + +```csharp +using GeneralUpdate.Differential.Matchers; + +// 自定义清理匹配器 +var cleanMatcher = new DefaultCleanMatcher(); // 或实现 ICleanMatcher +var dirtyMatcher = new DefaultDirtyMatcher(); // 或实现 IDirtyMatcher +``` + +--- + +## 7. AOT / NativeAOT 兼容性 + +GeneralUpdate.Core v10.4.6 支持 .NET Native AOT: + +```xml + + true + true + +``` + +JSON 序列化上下文(减少 AOT 大小): + +```csharp +using GeneralUpdate.Common.Internal.JsonContext; + +// 使用内置的 JsonSerializerContext +// VersionRespJsonContext, PacketJsonContext, ProcessInfoJsonContext 等 +``` + +--- + +## 8. Drivelution(Windows 驱动更新) + +`GeneralUpdate.Drivelution` 包提供 Windows 驱动管理: + +```csharp +using GeneralUpdate.Drivelution; + +// 扫描驱动目录 +var allDrivers = GeneralDrivelution.ScanDirectory(driverDir); + +// 验证驱动 +var isValid = GeneralDrivelution.ValidateDriver(driverPath); + +// 安装驱动(DIFx → SetupAPI → PnPUtil 级联) +var result = GeneralDrivelution.InstallDriver(driverPath); +``` + +--- + +## 内容索引 + +| 主题 | 可用性 | 参考 | +|------|--------|------| +| Pipeline 管道 | ✅ v10.4.6 | `GeneralUpdate.Common.Internal.Pipeline` | +| 策略系统 | ✅ v10.4.6 | `GeneralUpdate.Common.Internal.Strategy` | +| FileTree | ✅ v10.4.6 | `GeneralUpdate.Common.FileBasic` | +| BlackList | ✅ v10.4.6 | `Configinfo.BlackFiles` 等属性 | +| 差分引擎 | ✅ 需 `GeneralUpdate.Differential` | `DifferentialCore` | +| AOT | ✅ v10.4.6 | `JsonSerializerContext` 子类 | +| EventManager | ✅ v10.4.6 | `GeneralUpdate.Common.Internal.Event` | +| Bowl 崩溃守护 | ⚠️ 基础类型 | `GeneralUpdate.Bowl.Bowl` | +| IUpdateHooks | ❌ v10.4.6 不支持 | 开发分支 v10.5.0-beta.2 中 | +| 自定义 Strategy 注入 | ❌ v10.4.6 不支持 | 开发分支 v10.5.0-beta.2 中 | +| IPC 替换接口 | ❌ v10.4.6 不支持 | 开发分支 v10.5.0-beta.2 中 | +| SilentPollOrchestrator | ❌ v10.4.6 不支持 | 开发分支 v10.5.0-beta.2 中 | +| Option 系统 | ❌ v10.4.6 不支持 | 仅 Configinfo 属性 | + +--- + +## ✅ 高级定制验证清单 + +### Bowl 崩溃守护 +- [ ] 只引用了 `GeneralUpdate.Bowl`(不单独引用 Core) +- [ ] `MonitorParameter` 的 `ProcessNameOrId` 与实际进程名匹配 +- [ ] `TargetPath` 设置为应用安装根目录,非子目录 +- [ ] `WorkModel` 根据场景选择 Correct(Normal/Upgrade) +- [ ] `FailDirectory` 有写入权限 +- [ ] Linux/macOS 无此功能(Bowl 仅 Windows) + +### Pipeline 定制 +- [ ] `PipelineContext` 中的 Key 名称使用字符串常量拼写正确("ZipFilePath", "Hash", "Format", "Encoding", "SourcePath", "PatchEnabled") +- [ ] 中间件注册顺序正确:Hash → Compress → Patch → Drivelution +- [ ] `Encoding` 设置为 `Encoding.UTF8` + +### AOT/NativeAOT +- [ ] 启用了 `true` +- [ ] 对反射路径添加了 `[DynamicDependency]` 或 `[RequiresUnreferencedCode]` +- [ ] 使用了内置的 `JsonSerializerContext` 子类(减少裁剪) +- [ ] 通过 `dotnet build` 无 AOT 裁剪警告 + +### IPC 替换 +- [ ] 替换方案在目标平台上可用(Linux 无 NamedPipe 服务端,但有客户端) +- [ ] 加密方案与 Client/Upgrade 两端一致 +- [ ] IPC 数据长度有上限保护(防止内存溢出) + +--- + +## ⚠️ 反模式清单(高级定制特有) + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **在 v10.4.6 稳定版上使用开发分支 API(IUpdateHooks 等)** | 编译失败 / 运行时 MissingMethodException | 检查 API 可用性表 | +| 2 | **PipeLineContext Key 拼写错误(如 ZipFilePath 写成 ZipFilePatch)** | Pipeline 运行异常,值未传递 | 使用类库公开的常量或文档中的 Key 名 | +| 3 | **Bowl 的 WorkModel 设为 Upgrade 但进程是主程序** | 监控逻辑错误 | Normal=主线进程,Upgrade=升级进程 | +| 4 | **Windows 上 IPC 使用默认加密密钥** | 加密可被破解(代码审计 #1) | 使用强密钥(≥ 32 字符) | +| 5 | **差分包生成时使用不同版本的源文件结构** | 补丁应用失败,文件找不到 | 源和目标版本的文件结构必须一致 | +| 6 | **AOT 项目中使用了大量反射且未标记 DynamicDependency** | 运行时 TypeLoadException / 被剪裁 | 使用源代码生成器或显式标记保留 | +| 7 | **Pipeline 中 PatchMiddleware 排在 CompressMiddleware 前面** | 未解压就试图打补丁 | 顺序必须是 Compress→Patch | +| 8 | **自定义 Strategy 直接操作 private 方法** | 下游版本更新后 API 兼容性破裂 | 通过受保护的抽象方法扩展 | + +--- + +## 相关技能 + +- `/generalupdate-init` — Bootstrap 配置 +- `/generalupdate-strategy` — 更新策略选择 +- `/generalupdate-troubleshoot` — 问题诊断 diff --git a/cli/assets/skills/generalupdate-advanced/reference.md b/cli/assets/skills/generalupdate-advanced/reference.md new file mode 100644 index 0000000..b7aae5b --- /dev/null +++ b/cli/assets/skills/generalupdate-advanced/reference.md @@ -0,0 +1,102 @@ +# GeneralUpdate 扩展参考 + +> ⚠️ 基于 **NuGet v10.4.6 稳定版** API。开发分支功能已标注。 + +## 注入方法调用链(v10.4.6) + +```csharp +new GeneralUpdateBootstrap() + .SetConfig(config) // 必需:Configinfo 配置 + .AddListener*(handler) // 事件监听(可选) + .LaunchAsync() // 执行更新 +``` + +## Pipeline 构建 + +```csharp +var context = new PipelineContext(); +context.Add("ZipFilePath", path); +context.Add("Hash", hash); +context.Add("Format", 0); +context.Add("Encoding", Encoding.UTF8); +context.Add("SourcePath", sourcePath); +context.Add("PatchEnabled", true); + +await new PipelineBuilder(context) + .UseMiddleware() + .UseMiddleware() + .UseMiddlewareIf(condition) // 条件性添加中间件 + .Build(); +``` + +## 事件参数属性 + +| EventArgs | 命名空间 | 关键属性 | +|-----------|---------|---------| +| `UpdateInfoEventArgs` | `Common.Download` | `Info` (VersionRespDTO?) | +| `MultiDownloadStatisticsEventArgs` | `Common.Download` | `ProgressPercentage`, `Speed`, `Remaining` | +| `MultiDownloadCompletedEventArgs` | `Common.Download` | `Version` (object), `IsComplated` (bool) | +| `MultiDownloadErrorEventArgs` | `Common.Download` | `Exception`, `Version` (object) | +| `MultiAllDownloadCompletedEventArgs` | `Common.Download` | `IsAllDownloadCompleted`, `FailedVersions` | +| `ExceptionEventArgs` | `Common.Internal` | `Exception`, `Message` | + +## Bowl 选项 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `ProcessNameOrId` | string | 被监控的进程名或 PID | +| `TargetPath` | string | 应用安装根目录 | +| `DumpFileName` | string | Dump 文件名 | +| `FailFileName` | string | 故障报告文件名 | +| `FailDirectory` | string | 崩溃报告输出目录 | +| `BackupDirectory` | string | 备份目录 | +| `WorkModel` | string | 工作模式("Upgrade"/"Normal") | +| `ExtendedField` | string | 扩展字段(版本号等) | + +## HTTP 认证提供者 + +通过 `Configinfo.Scheme` + `Configinfo.Token` 在 Configinfo 中配置: + +| 方案 | Scheme 值 | Token | +|------|-----------|-------| +| HMAC (默认) | 无需设置(使用 AppSecretKey) | — | +| Bearer Token | `"Bearer"` | JWT Token | +| Basic Auth | `"Basic"` | Base64(username:password) | + +## AOT JSON 上下文 + +```csharp +using GeneralUpdate.Common.Internal.JsonContext; + +// 可用的 JsonSerializerContext 子类: +// VersionRespJsonContext — 版本响应 +// PacketJsonContext — 更新包 +// ProcessInfoJsonContext — 进程信息(IPC) +// GlobalConfigInfoOSSJsonContext — OSS 配置 +// VersionOSSJsonContext — OSS 版本 +// HttpParameterJsonContext — HTTP 参数 +// ReportRespJsonContext — 上报响应 +// FileNodesJsonContext — 文件节点 +``` + +## 差分引擎 + +```csharp +using GeneralUpdate.Differential; +using GeneralUpdate.Differential.Matchers; + +// 清理模式(服务端生成补丁) +await DifferentialCore.CleanAsync(srcDir, tgtDir, patchDir); + +// 脏模式(客户端应用补丁) +await DifferentialCore.DirtyAsync(installPath, patchDir); + +// 自定义匹配器 +class MyMatcher : ICleanMatcher +{ + public ComparisonResult Match(string srcDir, string tgtDir) + { + // 自定义文件匹配逻辑 + } +} +``` diff --git a/cli/assets/skills/generalupdate-advanced/templates/BowlIntegration.cs b/cli/assets/skills/generalupdate-advanced/templates/BowlIntegration.cs new file mode 100644 index 0000000..a90dc37 --- /dev/null +++ b/cli/assets/skills/generalupdate-advanced/templates/BowlIntegration.cs @@ -0,0 +1,36 @@ +using GeneralUpdate.Bowl; +using GeneralUpdate.Bowl.Strategys; + +/// +/// 【Skill 参考】Bowl 崩溃守护 +/// +/// ⚠️ 注意:v10.4.6 稳定版中 Bowl 仅提供基础类型定义。 +/// Bowl.LaunchAsync() 等完整功能在开发分支(v10.5.0-beta.2)中可用。 +/// +/// 此模板展示 v10.4.6 的实际 API 调用方式。 +/// +/// NuGet: dotnet add package GeneralUpdate.Bowl +/// +public static class BowlIntegration +{ + public static void ConfigureBowl() + { + // v10.4.6 中的 Bowl API: + // Bowl 类有公开构造函数,但无公开 LaunchAsync 方法 + // 完整崩溃守护功能请关注后续版本 + + var param = new MonitorParameter + { + ProcessNameOrId = "MyApp.exe", + DumpFileName = "v1.0.0.0_fail.dmp", + FailFileName = "v1.0.0.0_fail.json", + TargetPath = @"C:\Program Files\MyApp", + FailDirectory = @"C:\Program Files\MyApp\fail", + BackupDirectory = @"C:\Program Files\MyApp\backup", + WorkModel = "Upgrade", + }; + + var bowl = new Bowl(); + Console.WriteLine("[Bowl] Bowl 实例已创建。完整监控功能需 v10.5+。"); + } +} diff --git a/cli/assets/skills/generalupdate-advanced/templates/CustomHooks.cs b/cli/assets/skills/generalupdate-advanced/templates/CustomHooks.cs new file mode 100644 index 0000000..544962f --- /dev/null +++ b/cli/assets/skills/generalupdate-advanced/templates/CustomHooks.cs @@ -0,0 +1,19 @@ +/// +/// 【Skill 参考】自定义生命周期 Hooks +/// +/// ⚠️ 注意:v10.4.6 稳定版中不存在 IUpdateHooks 接口和 HookContext 类型。 +/// 此功能在开发分支(v10.5.0-beta.2)中可用。 +/// +/// 在 v10.4.6 中,可以通过在 GeneralUpdateBootstrap +/// 的事件回调中添加自定义逻辑来实现类似功能。 +/// +public class MyCustomHooks +{ + // v10.4.6 稳定版不支持 IUpdateHooks + // 使用 Bootstrap 事件处理流程中的回调代替 + + public static void Example() + { + Console.WriteLine("[Hooks] Hooks 功能需要 v10.5+。当前使用事件回调替代。"); + } +} diff --git a/cli/assets/skills/generalupdate-advanced/templates/CustomStrategy.cs b/cli/assets/skills/generalupdate-advanced/templates/CustomStrategy.cs new file mode 100644 index 0000000..362038a --- /dev/null +++ b/cli/assets/skills/generalupdate-advanced/templates/CustomStrategy.cs @@ -0,0 +1,37 @@ +using GeneralUpdate.Common.Internal.Pipeline; + +/// +/// 【Skill 参考】自定义平台策略 +/// +/// ⚠️ 注意:v10.4.6 稳定版不支持通过 bootstrap.Strategy() 注入自定义策略。 +/// 自定义策略需要直接继承 AbstractStrategy 并手动调用。 +/// +/// AbstractStrategy 模板方法: +/// - Create(UpdateContext) — 初始化 +/// - ExecuteAsync() — 执行策略主体 +/// - StartAppAsync() — 启动主应用 +/// - BuildPipeline(PipelineContext) — 构建平台特定中间件链 +/// +public class MyCustomStrategy +{ + // v10.4.6 稳定版不支持自定义策略注入 + // 此模板作为开发分支(v10.5.0-beta.2)特性的参考 + + public static async Task ExamplePipelineAsync() + { + var context = new PipelineContext(); + context.Add("ZipFilePath", @"C:\temp\update.zip"); + context.Add("Hash", ""); + context.Add("Format", 0); + context.Add("Encoding", System.Text.Encoding.UTF8); + context.Add("SourcePath", @"C:\Program Files\MyApp"); + context.Add("PatchEnabled", true); + + await new PipelineBuilder(context) + .UseMiddleware() + .UseMiddleware() + .Build(); + + Console.WriteLine("[CustomStrategy] 管道执行完成"); + } +} diff --git a/cli/assets/skills/generalupdate-advanced/templates/NamedPipeIPC.cs b/cli/assets/skills/generalupdate-advanced/templates/NamedPipeIPC.cs new file mode 100644 index 0000000..9e44247 --- /dev/null +++ b/cli/assets/skills/generalupdate-advanced/templates/NamedPipeIPC.cs @@ -0,0 +1,102 @@ +using System.IO.Pipes; +using System.Text; +using System.Text.Json; + +/// +/// 【Skill 参考】命名管道 IPC +/// +/// ⚠️ 注意:v10.4.6 稳定版中不存在 IProcessInfoProvider 接口。 +/// IPC 实现在当前版本中不可替换。 +/// +/// 此代码作为 NamedPipe 通信模式的参考实现, +/// 实际替换 IPC 需要 v10.5.0-beta.2 开发分支。 +/// +public class NamedPipeIpcProvider : IAsyncDisposable +{ + private const string PipeNamePrefix = "GeneralUpdate_IPC_"; + private NamedPipeServerStream? _server; + private NamedPipeClientStream? _client; + private readonly CancellationTokenSource _cts = new(); + + public async Task ServerWaitAsync(int processId, int timeoutMs = 30000) + { + var pipeName = PipeNamePrefix + processId; + _server = new NamedPipeServerStream( + pipeName, PipeDirection.InOut, + maxNumberOfServerInstances: 1, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + + using var timeoutCts = new CancellationTokenSource(timeoutMs); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, timeoutCts.Token); + try + { + await _server.WaitForConnectionAsync(linkedCts.Token); + return pipeName; + } + catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) + { + throw new TimeoutException($"NamedPipe 等待连接超时 ({timeoutMs}ms)"); + } + } + + public async Task ClientConnectAsync(string pipeName, int timeoutMs = 30000) + { + _client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + await _client.ConnectAsync(timeoutMs, _cts.Token); + } + + public async Task SendAsync(T data) + { + var stream = GetStream(); + var json = JsonSerializer.Serialize(data); + var bytes = Encoding.UTF8.GetBytes(json); + var lengthBytes = BitConverter.GetBytes(bytes.Length); + if (!BitConverter.IsLittleEndian) Array.Reverse(lengthBytes); + await stream.WriteAsync(lengthBytes, _cts.Token); + await stream.WriteAsync(bytes, _cts.Token); + await stream.FlushAsync(_cts.Token); + } + + public async Task ReceiveAsync() + { + var stream = GetStream(); + var lengthBuffer = new byte[4]; + var lengthRead = 0; + while (lengthRead < 4) + { + var read = await stream.ReadAsync(lengthBuffer, lengthRead, 4 - lengthRead, _cts.Token); + if (read == 0) throw new EndOfStreamException("管道连接已关闭"); + lengthRead += read; + } + // 先处理大端序,再解析长度 + if (!BitConverter.IsLittleEndian) Array.Reverse(lengthBuffer); + var length = BitConverter.ToInt32(lengthBuffer); + if (length <= 0 || length > 10 * 1024 * 1024) // 限制最大 10MB + throw new InvalidDataException($"无效的消息长度: {length}"); + var buffer = new byte[length]; + var offset = 0; + while (offset < length) + { + var read = await stream.ReadAsync(buffer, offset, length - offset, _cts.Token); + if (read == 0) throw new EndOfStreamException("管道连接已关闭"); + offset += read; + } + var json = Encoding.UTF8.GetString(buffer); + return JsonSerializer.Deserialize(json); + } + + private Stream GetStream() + { + if (_server?.IsConnected == true) return _server; + if (_client?.IsConnected == true) return _client; + throw new InvalidOperationException("NamedPipe 未连接"); + } + + public async ValueTask DisposeAsync() + { + await _cts.CancelAsync(); + _server?.Dispose(); + _client?.Dispose(); + _cts.Dispose(); + } +} diff --git a/cli/assets/skills/generalupdate-init/SKILL.md b/cli/assets/skills/generalupdate-init/SKILL.md new file mode 100644 index 0000000..510ec25 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/SKILL.md @@ -0,0 +1,367 @@ +--- +name: generalupdate-init +description: | + Integrate GeneralUpdate auto-update into any .NET application. Generates Bootstrap + configuration code, manifest files, and dual-project (Client+Upgrade) scaffolding. + Covers 4 update scenes, Configinfo configuration, appsettings.json, HTTP auth (HMAC/Basic/Bearer), + and complete deployment checklist. Triggers on: "add auto update", "integrate GeneralUpdate", + "configure bootstrap", "我需要自动更新", "配置更新", "初始化GeneralUpdate", "添加更新功能", + "接入更新", "升级框架". Also triggers when user mentions their project type + update. + Always pair with generalupdate-ui if a UI framework is detected, and with + generalupdate-strategy if the user asks about different update approaches. +when_to_use: | + - First-time integration of GeneralUpdate into a .NET project + - User wants Bootstrap configuration code (Minimal or Full) + - User needs the Client + Upgrade dual-project structure explained + - User asks about manifest.json, Configinfo, or generalupdate.manifest.json + - User mentions their specific .NET framework (WPF/WinForms/Avalonia/MAUI/console) + - User asks about deployment considerations or CI/CD integration + - Best used as the entry point; guide to other skills as needed +allowed-tools: "Bash, Read, Write, Edit, Glob, Grep, WebSearch" +--- + +# 🚀 GeneralUpdate 集成完全指南 + +帮助开发者在任意 .NET 应用中集成 GeneralUpdate 自动更新。从零开始,覆盖所有配置方式、部署场景和生产环境考量。 + +> ⚠️ **针对 NuGet v10.4.6 稳定版**。开发分支(v10.5.0-beta.2)有不同 API。 + +--- + +## 📋 用户需求提取 + +在生成代码前,必须先提取以下信息。**不确定的必须追问:** + +``` +### 项目状态 +- 现有项目类型: ______(新项目 / 已有项目 / 从旧版迁移) +- .NET 版本: ______ +- UI 框架: ______(WPF/WinForms/Avalonia/MAUI/控制台/无) +- 目标平台: ______(Windows/Linux/macOS/多平台) + +### 更新需求 +- 是否需要显示进度 UI: ______(是/否) +- 是否有后端服务: ______(是/否) +- 更新策略倾向: ______(标准/OSS/静默/差分/跨版本/推送) +- 是否需要崩溃守护 Bowl: ______(是/否) + +### 已有配置(如果存在) +- 是否已安装 NuGet: ______(是/否,版本号) +- 是否已有 Configinfo 配置: ______(是/否) +- 是否已有 manifest.json: ______(是/否) +``` + +--- + +## 工作流程(按顺序执行) + +### Step 1:探测项目状态 + +``` +├── 检查 .csproj → 目标框架、UI 类型、是否有 NuGet 引用 +├── 检查是否存在 generalupdate.manifest.json +├── 检查是否存在 Configinfo/Bootstrap 配置代码 +└── 检查项目结构 → 是否已有独立的 Upgrade 项目 +``` + +### Step 2:选择集成模式 + +基于需求提取结果,选择以下模式之一: + +| 模式 | 适用场景 | 产出 | +|------|---------|------| +| **[Minimal]** | 新用户快速上手,控制台/服务应用 | 3 行 Bootstrap 代码 | +| **[Standard]** | 需要精确控制更新过程 | Configinfo + 完整事件监听 | +| **[Scaffold]** | 团队项目,从零开始 | 完整 Client + Upgrade 双项目结构 | + +### Step 3:生成输出 + +``` +├── NuGet 安装命令(按平台选 Core/Bowl) +├── Bootstrap 配置代码(按模式) +├── manifest.json 模板 +├── 部署检查清单 +└── 已知问题预警(针对你的配置组合) +``` + +### Step 4:引导下一步 + +``` +├── 需要 UI → /generalupdate-ui +├── 选择策略 → /generalupdate-strategy +├── 需要 Bowl 守护 → /generalupdate-advanced +└── 遇到问题 → /generalupdate-troubleshoot +``` + +--- + +## 核心概念:4 大更新场景 + +GeneralUpdate 根据服务端返回的包类型决定更新策略: + +| 场景 | 行为 | +|------|------| +| **None** | 无需更新,直接启动主程序 | +| **UpgradeOnly** | 只更新升级程序自身:Client 原地解压 Upgrade 包 | +| **MainOnly** | 只更新主程序:Client → IPC → 启动 Upgrade 进程 | +| **Both** | 两者都更新 | + +**双进程架构**: +``` +App.exe (Client) 负责: + ├── 版本验证(HTTP 请求服务端) + ├── 下载所有更新包 + ├── IPC 写入(加密文件传递参数给 Upgrade) + └── 启动 Upgrade.exe 然后自己退出 + +Upgrade.exe (Upgrade 进程) 负责: + ├── 读取 IPC 文件 + ├── 应用更新(解压/补丁/替换文件) + └── 启动主程序然后自己退出 +``` + +--- + +## Configinfo 配置详解 + +### Configinfo 完整属性 + +```csharp +var config = new Configinfo +{ + // === 必需 === + UpdateUrl = "https://your-server.com/Upgrade/Verification", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", + + // === 可选 === + ReportUrl = "https://your-server.com/Upgrade/Report", + UpdateLogUrl = "https://your-server.com/Upgrade/Log", + UpgradeClientVersion = "1.0.0.0", + + // === 安全认证 === + Scheme = "Bearer", // HTTP 认证方案 + Token = "your-token", // HTTP 认证令牌 + + // === 黑名单(备份/复制时排除)=== + BlackFiles = new List { "*.log", "*.tmp" }, + BlackFormats = new List { ".pdb" }, + SkipDirectorys = new List { "logs", "cache" }, +}; +``` + +### 应用角色(AppType) + +`AppType` 是一个 class,包含两个静态字段: + +| 字段 | 值 | 说明 | +|------|-----|------| +| `AppType.ClientApp` | 1 | 标准客户端(主程序) | +| `AppType.UpgradeApp` | 2 | 标准升级程序 | + +> ⚠️ v10.4.6 不支持 OssClient(值 3-4),这些在开发分支中。 + +### 事件监听器完整清单 + +```csharp +// 全部 6 个事件 +.AddListenerUpdateInfo((_, e) => { + /* 版本验证结果(e.Info?.Body 含 VersionInfo 列表) */ +}) +.AddListenerMultiDownloadStatistics((_, e) => { + /* 批量下载进度(e.ProgressPercentage, e.Speed, e.Remaining) */ +}) +.AddListenerMultiDownloadCompleted((_, e) => { + /* 每版本下载完成(e.Version, e.IsComplated) */ +}) +.AddListenerMultiDownloadError((_, e) => { + /* 下载错误(e.Exception, e.Version) */ +}) +.AddListenerMultiAllDownloadCompleted((_, e) => { + /* 全部下载完成(e.IsAllDownloadCompleted, e.FailedVersions) */ +}) +.AddListenerException((_, e) => { + /* 异常(e.Message, e.Exception) */ +}) +``` + +--- + +## 集成方式的完整代码 + +### 方式 A:Minimal — 使用 Configinfo + +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; + +var config = new Configinfo +{ + UpdateUrl = "https://your-server.com/api", + AppSecretKey = "your-32-char-secret-key-here!", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = "." +}; + +await new GeneralUpdateBootstrap() + .SetConfig(config) + .LaunchAsync(); +``` + +### 方式 B:Standard — Configinfo + 事件 + 监听 + +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +var config = new Configinfo +{ + UpdateUrl = "https://your-server.com/Upgrade/Verification", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = AppDomain.CurrentDomain.BaseDirectory, + ReportUrl = "https://your-server.com/Upgrade/Report", +}; + +await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerUpdateInfo((_, e) => + { + Console.WriteLine($"发现 {e.Info?.Body?.Count ?? 0} 个版本"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + { + Console.WriteLine($"进度: {e.ProgressPercentage}% | {e.Speed}"); + }) + .AddListenerMultiDownloadCompleted((_, e) => + { + Console.WriteLine($"版本 {e.Version} 下载完成"); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + Console.WriteLine($"全部完成 (IsAllDownloadCompleted={e.IsAllDownloadCompleted})"); + }) + .AddListenerMultiDownloadError((_, e) => + { + Console.WriteLine($"下载失败: 版本 {e.Version} — {e.Exception?.Message}"); + }) + .AddListenerException((_, e) => + { + Console.WriteLine($"异常: {e.Message}"); + }) + .LaunchAsync(); +``` + +### Upgrade 进程配置 + +```csharp +using GeneralUpdate.Core; + +// Upgrade 模式从 IPC 文件读取配置,无需 SetConfig +await new GeneralUpdateBootstrap() + .AddListenerException((_, e) => + Console.WriteLine($"错误: {e.Message}")) + .LaunchAsync(); +``` + +--- + +## 生产环境部署检查清单 + +### 发布目录结构 + +``` +publish/ +├── MyApp.exe ← MainAppName(主程序) +├── generalupdate.manifest.json +└── update/ + └── UpgradeApp.exe ← 升级程序,必须随首个版本发布 +``` + +### 双进程验证 + +| 检查项 | 说明 | +|--------|------| +| UpgradeApp.exe 存在于发布目录 | 首个版本就必须有 | +| Client 和 Upgrade 使用相同 AppSecretKey | IPC 加密通信依赖此 Key | +| Client 和 Upgrade 使用相同 NuGet 版本号 | 版本不一致导致 "Method not found" | +| Upgrade 进程不需要网络 | 所有数据由 Client 预下载 | + +--- + +## ⚠️ 已知问题 + +### NuGet 类型冲突 +`GeneralUpdate.Core` 和 `GeneralUpdate.Bowl` **不能同时引用**(CS0433 类型冲突)。 +请根据需求选择: +- 使用 Core:`dotnet add package GeneralUpdate.Core` +- 使用 Bowl:**只引用** `GeneralUpdate.Bowl`(它传递依赖 Core 所有功能) +- 差分类型已内嵌在 Core,**无需额外** `GeneralUpdate.Differential` 包 + +### 稳定版功能限制 +v10.4.6 无 `IUpdateHooks`、无可编程 `Option`、无静默轮询器。 +这些功能在开发分支(v10.5.0-beta.2)中可用。 + +--- + +## ✅ 集成验证清单(交付前逐项检查) + +### Bootstrap 配置 +- [ ] `Configinfo` 的 6 个必填字段都已设置(UpdateUrl, AppSecretKey, AppName, MainAppName, ClientVersion, ProductId, InstallPath) +- [ ] `UpdateUrl` 指向的服务端 API 可正常返回版本信息 +- [ ] `AppSecretKey` 长度 ≥ 16 字符,与服务端一致 +- [ ] `AppType` 设置正确(Client = 1, Upgrade = 2) +- [ ] 生产环境使用 `AppDomain.CurrentDomain.BaseDirectory` 作为 InstallPath + +### NuGet & 编译 +- [ ] Client 和 Upgrade 项目使用**完全相同**的 GeneralUpdate NuGet 版本 +- [ ] 如果用 Bowl:项目中只能有 `GeneralUpdate.Bowl`,不能同时有 `GeneralUpdate.Core` +- [ ] 项目能正常 `dotnet build`(0 errors) +- [ ] 无需额外引用 `GeneralUpdate.Differential`(已嵌入 Core) + +### 部署结构 +- [ ] UpgradeApp.exe 存在于发布目录(首个版本就必须有) +- [ ] `generalupdate.manifest.json` 的 `UpdateAppName` 包含 `.exe` +- [ ] IPC 文件(`UpdateInfo.msg`)路径在 Client/Upgrade 间一致 +- [ ] `Encoding` 设置为 `Encoding.UTF8`(防止 Linux/macOS 中文乱码) + +### 迁移场景(从 v9.x 升级) +- [ ] 检查旧代码中是否有 `SetSource()` / `SetOption()` / `Hooks()` 等不存在的方法 +- [ ] `AppType` 原来是 enum 吗?v10.4.6 中是 class,`ClientApp = 1`, `UpgradeApp = 2` +- [ ] `LaunchAsync()` 在 v10.4.6 中返回 `Task`(不是 `Task`) +- [ ] 删除 `OssClient` 相关引用(v10.4.6 不支持) + +--- + +## ⚠️ 反模式清单 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **Core 和 Bowl 引用到同一个项目** | CS0433 类型冲突,编译失败 | 用 Bowl 时只引 Bowl(传递依赖 Core) | +| 2 | **Client/Upgrade NuGet 版本号不一致** | 运行时 MethodNotFoundException | 锁定完全相同版本 | +| 3 | **UpgradeApp.exe 不随首个版本发布** | 第一次更新时 FileNotFoundException | 首个版本就包含 UpgradeApp | +| 4 | **事件监听中做耗时操作(网络 IO / 磁盘 IO)** | Update 进程 UI 卡死,超时被 Kill | 仅更新 UI 状态,耗时操作异步 | +| 5 | **IPC 文件编码未设置 UTF-8** | Linux/macOS 中文乱码 | `Encoding.UTF8` | +| 6 | **版本号不是 4 段式(如 1.0.0.0)** | 版本比较逻辑异常 | 始终用 `x.y.z.w` 格式 | +| 7 | **manifest.json 的 mainAppName 不匹配真实进程名** | 更新后主程序找不到 | 和实际 exe 名称一致 | +| 8 | **为 v9.x 编写的代码直接用在 v10** | API 不兼容,编译失败 | 对照 v10.4.6 稳定版 API 重写 | + +--- + +## 相关技能 + +- `/generalupdate-ui` — UI 框架自动检测 + 更新窗口代码生成 +- `/generalupdate-strategy` — 6 种更新策略选择与配置 +- `/generalupdate-advanced` — 高级定制(适用于开发分支) +- `/generalupdate-troubleshoot` — 已知问题诊断 diff --git a/cli/assets/skills/generalupdate-init/project-scaffold/ClientApp.csproj b/cli/assets/skills/generalupdate-init/project-scaffold/ClientApp.csproj new file mode 100644 index 0000000..b23c677 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/project-scaffold/ClientApp.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/cli/assets/skills/generalupdate-init/project-scaffold/ClientProgram.cs b/cli/assets/skills/generalupdate-init/project-scaffold/ClientProgram.cs new file mode 100644 index 0000000..19eb1c3 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/project-scaffold/ClientProgram.cs @@ -0,0 +1,42 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +string updateUrl = args.Length > 0 ? args[0] : "{{UPDATE_URL}}"; +string secretKey = args.Length > 1 ? args[1] : "{{APP_SECRET_KEY}}"; + +Console.WriteLine($"[Client] 启动版本检查: {updateUrl}"); + +var config = new Configinfo +{ + UpdateUrl = updateUrl, + AppSecretKey = secretKey, + AppName = "{{PROJECT_NAME}}.exe", + MainAppName = "{{PROJECT_NAME}}.exe", + ClientVersion = GetCurrentVersion(), + ProductId = "{{PRODUCT_ID}}", + InstallPath = "." +}; + +await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerUpdateInfo((_, e) => + Console.WriteLine($"[Client] 发现 {e.Info?.Body?.Count ?? 0} 个版本")) + .AddListenerMultiDownloadStatistics((_, e) => + Console.WriteLine($"[Client] 下载进度: {e.ProgressPercentage}% | {e.Speed}")) + .AddListenerMultiDownloadCompleted((_, e) => + Console.WriteLine($"[Client] 版本 {e.Version} 下载完成")) + .AddListenerMultiAllDownloadCompleted((_, e) => + Console.WriteLine("[Client] 全部下载完成")) + .AddListenerException((_, e) => + Console.WriteLine($"[Client] 错误: {e.Message}")) + .LaunchAsync(); + +Console.WriteLine("[Client] 更新流程完成,进程即将退出/继续执行"); + +static string GetCurrentVersion() +{ + var version = System.Reflection.Assembly.GetEntryAssembly() + ?.GetName()?.Version; + return version?.ToString() ?? "1.0.0.0"; +} diff --git a/cli/assets/skills/generalupdate-init/project-scaffold/UpgradeApp.csproj b/cli/assets/skills/generalupdate-init/project-scaffold/UpgradeApp.csproj new file mode 100644 index 0000000..0842860 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/project-scaffold/UpgradeApp.csproj @@ -0,0 +1,17 @@ + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/cli/assets/skills/generalupdate-init/project-scaffold/UpgradeProgram.cs b/cli/assets/skills/generalupdate-init/project-scaffold/UpgradeProgram.cs new file mode 100644 index 0000000..916ead1 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/project-scaffold/UpgradeProgram.cs @@ -0,0 +1,19 @@ +using GeneralUpdate.Core; + +Console.WriteLine("[Upgrade] 升级程序启动"); + +try +{ + // Upgrade 模式从 IPC 读取配置,无需 SetConfig + await new GeneralUpdateBootstrap() + .AddListenerException((_, e) => + Console.WriteLine($"[Upgrade] 错误: {e.Message}")) + .LaunchAsync(); + + Console.WriteLine("[Upgrade] 更新完成,主程序已启动"); +} +catch (Exception ex) +{ + Console.WriteLine($"[Upgrade] 严重错误: {ex}"); + Environment.ExitCode = 1; +} diff --git a/cli/assets/skills/generalupdate-init/reference.md b/cli/assets/skills/generalupdate-init/reference.md new file mode 100644 index 0000000..0114d37 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/reference.md @@ -0,0 +1,133 @@ +# GeneralUpdate 参考手册 + +> ⚠️ **针对 NuGet v10.4.6 稳定版 API**。该版本使用 `Configinfo` 配置,无可编程 `Option` 系统。 + +## NuGet 包 + +所有包的最新版本:[nuget.org/profiles/GeneralLibrary](https://www.nuget.org/profiles/GeneralLibrary) + +| 包名 | 用途 | 必需 | .NET 版本 | 典型版本 | +|------|------|------|-----------|---------| +| `GeneralUpdate.Core` | 核心引擎(Bootstrap/下载/事件) | ✅ 是 | net8.0;net10.0 | 10.4.6 | +| `GeneralUpdate.Differential` | BSDIFF/HDiffPatch 差分补丁 | ❌ 可选 | net8.0;net10.0 | 10.4.6 | +| `GeneralUpdate.Bowl` | 进程崩溃监控、MiniDump、自动回滚 | ❌ 可选 | net8.0;net10.0 | 10.4.6 | +| `GeneralUpdate.Extension` | 插件管理系统 | ❌ 可选 | net8.0;net10.0 | ≥ 1.0.0 | +| `GeneralUpdate.Drivelution` | Windows 驱动更新 | ❌ 可选 | net8.0;net10.0 | 10.4.6 | + +> ⚠️ **NuGet 类型冲突**: +> - `GeneralUpdate.Differential` 的 `DifferentialCore` 等类型已内嵌在 `GeneralUpdate.Core` 中,**不需额外引用**(直接使用 Core 即可) +> - `GeneralUpdate.Bowl` 和 `GeneralUpdate.Core` **不能同时引用**(两者都发布了 `GeneralUpdate.Common` 导致 CS0433) +> - 使用 Bowl 时**只引用 `GeneralUpdate.Bowl`**(它传递依赖 Core 的所有功能) + +## Configinfo 字段完整说明 + +| 字段 | 类型 | 必需 | 说明 | +|------|------|:----:|------| +| `UpdateUrl` | string | ✅ | 版本验证 API 地址 | +| `AppSecretKey` | string | ✅ | HMAC 认证 + IPC 加密密钥 | +| `AppName` | string | ✅ | 当前应用进程名(含 .exe) | +| `MainAppName` | string | ✅ | 主程序文件名(含 .exe) | +| `ClientVersion` | string | ✅ | 当前版本号(4 段式) | +| `ProductId` | string | ✅ | 产品标识 | +| `InstallPath` | string | ✅ | 应用安装目录 | +| `UpgradeClientVersion` | string | ❌ | Upgrade 进程版本号 | +| `ReportUrl` | string | ❌ | 状态上报 API 地址 | +| `UpdateLogUrl` | string | ❌ | 更新日志 API 地址 | +| `Scheme` | string | ❌ | HTTP 认证方案(Bearer/Basic 等) | +| `Token` | string | ❌ | HTTP 认证令牌 | +| `BlackFiles` | List\ | ❌ | 备份排除文件名模式 | +| `BlackFormats` | List\ | ❌ | 备份排除扩展名 | +| `SkipDirectorys` | List\ | ❌ | 备份排除目录名 | +| `Bowl` | string | ❌ | Bowl 配置参数 | + +## 后端 API 协议 + +### 版本验证 + +``` +POST {UpdateUrl} +Content-Type: application/json + +{ + "appKey": "client-app-key", + "appType": 1, + "clientVersion": "1.0.0.0", + "productId": "my-product-001", + "platform": "Windows", + "tenantId": "default" +} +``` + +> `appType`: 1=Client, 2=Upgrade + +成功响应: +```json +{ + "code": 200, + "message": "", + "body": [ + { + "version": "1.1.0.0", + "url": "https://storage/packages/v1.1.0.0.zip", + "hash": "sha256-hex", + "size": 1048576, + "name": "update.zip", + "appType": 1, + "isForcibly": false + } + ] +} +``` + +### 状态上报 + +``` +POST {ReportUrl} +Content-Type: application/json + +{ + "recordId": "升级记录 ID", + "type": 1, + "status": 0, + "message": "更新成功" +} +``` + +## 事件参数属性 + +| EventArgs | 关键属性 | 说明 | +|-----------|---------|------| +| `UpdateInfoEventArgs` | `Info` (VersionRespDTO?) → `Info.Body` (List\) | 版本验证结果 | +| `MultiDownloadStatisticsEventArgs` | `ProgressPercentage`, `Speed`, `Remaining`, `TotalBytesToReceive`, `BytesReceived` | 下载进度 | +| `MultiDownloadCompletedEventArgs` | `Version` (object), `IsComplated` (bool) | 版本下载完成 | +| `MultiDownloadErrorEventArgs` | `Exception`, `Version` (object) | 下载错误 | +| `MultiAllDownloadCompletedEventArgs` | `IsAllDownloadCompleted` (bool), `FailedVersions` (IList) | 全部完成 | +| `ExceptionEventArgs` | `Exception`, `Message` | 异常 | + +## AppType 类 + +``` +AppType.ClientApp = 1 // 标准客户端 +AppType.UpgradeApp = 2 // 标准升级程序 +``` + +## 框架兼容性矩阵 + +| 框架 | 最低 SDK 版本 | SignalR 支持 | +|------|:------------:|:-----------:| +| WPF (Windows) | .NET 8 (`net8.0-windows`) | ✅ | +| WinForms (Windows) | .NET 8 (`net8.0-windows`) | ✅ | +| Avalonia | .NET 8 | ✅ | +| MAUI | .NET 10 | ❌ | +| 控制台 | .NET 8 | ✅ | +| Linux 桌面 | .NET 8 | ❌ | +| macOS 桌面 | .NET 8 | ❌ | + +## 常见问题快速链接 + +| 问题 | 参考文件 | +|------|---------| +| "Method not found" NuGet 版本冲突 | `/generalupdate-troubleshoot` | +| 静默模式不生效 | `/generalupdate-troubleshoot` | +| 升级进程没启动 | `/generalupdate-troubleshoot` | +| Linux 下无权限 | `/generalupdate-troubleshoot` | diff --git a/cli/assets/skills/generalupdate-init/templates/FullIntegration.cs b/cli/assets/skills/generalupdate-init/templates/FullIntegration.cs new file mode 100644 index 0000000..3818882 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/templates/FullIntegration.cs @@ -0,0 +1,135 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +/// +/// GeneralUpdate 完整集成示例 — 包含所有配置选项和事件监听 +/// +/// 覆盖: +/// - Configinfo 完整配置 +/// - 全部 6 个事件监听器 +/// - 升级场景理解 +/// - 错误处理 +/// - Upgrade 进程配置 +/// +/// 针对 NuGet v10.4.6 稳定版 +/// +public static class FullIntegration +{ + public static async Task RunAsync() + { + try + { + // ========== 1. 构建配置 ========== + var config = new Configinfo + { + // --- 必填 --- + UpdateUrl = "{{UPDATE_URL}}", + AppSecretKey = "{{APP_SECRET_KEY}}", + + // --- 应用信息 --- + AppName = "{{PROJECT_NAME}}.exe", + MainAppName = "{{PROJECT_NAME}}.exe", + ClientVersion = "{{CLIENT_VERSION}}", // ⚠️ 4 段式 + ProductId = "{{PRODUCT_ID}}", + InstallPath = AppDomain.CurrentDomain.BaseDirectory, + + // --- 可选 --- + ReportUrl = "https://your-server.com/Upgrade/Report", + UpdateLogUrl = "https://your-server.com/Upgrade/Log", + }; + + // ========== 2. 配置 Bootstrap ========== + var bootstrap = await new GeneralUpdateBootstrap() + .SetConfig(config) + + // 事件:版本发现 + .AddListenerUpdateInfo(OnUpdateInfo) + // 事件:批量下载进度 + .AddListenerMultiDownloadStatistics(OnDownloadStats) + // 事件:每版本下载完成 + .AddListenerMultiDownloadCompleted(OnDownloadCompleted) + // 事件:全部下载完成 + .AddListenerMultiAllDownloadCompleted(OnAllDownloadCompleted) + // 事件:下载错误 + .AddListenerMultiDownloadError(OnDownloadError) + // 事件:异常 + .AddListenerException(OnException) + + // ========== 3. 执行 ========== + .LaunchAsync(); + + // LaunchAsync 返回 bootstrap 实例 + // 有更新 → 进程退出由 Upgrade 进程重启 + // 无更新 → 继续执行 + } + catch (Exception ex) + { + Console.WriteLine($"[严重] 更新异常: {ex.Message}"); + } + } + + // ========== 事件处理函数 ========== + + private static void OnUpdateInfo(object? sender, UpdateInfoEventArgs e) + { + if (e.Info?.Body != null) + { + Console.WriteLine($"[版本发现] 版本数: {e.Info.Body.Count}"); + foreach (var v in e.Info.Body) + Console.WriteLine($" ├─ {v.Version} (AppType={v.AppType}) {v.Name ?? ""}"); + } + } + + private static void OnDownloadStats(object? sender, MultiDownloadStatisticsEventArgs e) + { + Console.Write($"\r[下载] {e.ProgressPercentage:F0}% | {e.Speed} | 剩余 {e.Remaining:hh\\:mm\\:ss}"); + } + + private static void OnDownloadCompleted(object? sender, MultiDownloadCompletedEventArgs e) + { + Console.WriteLine($"\n[下载完成] 版本: {e.Version} (IsComplated={e.IsComplated})"); + } + + private static void OnAllDownloadCompleted(object? sender, MultiAllDownloadCompletedEventArgs e) + { + Console.WriteLine($"[全部完成] 成功: {e.IsAllDownloadCompleted}, 失败版本数: {e.FailedVersions?.Count ?? 0}"); + } + + private static void OnDownloadError(object? sender, MultiDownloadErrorEventArgs e) + { + Console.WriteLine($"\n[下载错误] 版本: {e.Version} — {e.Exception?.Message}"); + } + + private static void OnException(object? sender, ExceptionEventArgs e) + { + Console.WriteLine($"[异常] {e.Message}"); + } +} + +/// +/// Upgrade 进程配置(供 UpgradeApp.exe 使用) +/// +public static class UpgradeProcessIntegration +{ + public static async Task RunAsync() + { + Console.WriteLine("[Upgrade] 升级程序启动 — 从 IPC 读取配置"); + + try + { + // Upgrade 模式不需要 SetConfig — 配置由 IPC 传递 + await new GeneralUpdateBootstrap() + .AddListenerException((_, e) => + Console.WriteLine($"[Upgrade] 错误: {e.Message}")) + .LaunchAsync(); + + Console.WriteLine("[Upgrade] 更新完成,主程序已启动"); + } + catch (Exception ex) + { + Console.WriteLine($"[Upgrade] 严重错误: {ex}"); + Environment.ExitCode = 1; + } + } +} diff --git a/cli/assets/skills/generalupdate-init/templates/MinimalIntegration.cs b/cli/assets/skills/generalupdate-init/templates/MinimalIntegration.cs new file mode 100644 index 0000000..ac28a79 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/templates/MinimalIntegration.cs @@ -0,0 +1,45 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; + +/// +/// GeneralUpdate 最小集成示例 +/// +/// 这 3 行代码包含了完整的更新流程: +/// 版本验证 → 下载 → IPC → 启动升级进程 → 替换文件 → 重启 +/// +/// 使用条件(必须是满足以下所有条件): +/// 1. 项目发布目录存在 generalupdate.manifest.json(自动发现应用元数据) +/// 2. UpgradeApp.exe 已放置在 InstallPath/update/ 子目录中 +/// 3. 后端已部署 GeneralSpacestation 或兼容 API +/// 4. 当前应用为主程序,另有一个独立的升级程序 +/// +/// 如果以上条件不满足,请使用 FullIntegration.cs 显式配置。 +/// +/// NuGet: dotnet add package GeneralUpdate.Core +/// ⚠️ 针对 NuGet v10.4.6 稳定版 +/// +public static class MinimalIntegration +{ + public static async Task RunAsync() + { + // 1. 创建配置对象 + var config = new Configinfo + { + UpdateUrl = "{{UPDATE_URL}}", + AppSecretKey = "{{APP_SECRET_KEY}}", + AppName = "{{PROJECT_NAME}}.exe", + MainAppName = "{{PROJECT_NAME}}.exe", + ClientVersion = "{{CLIENT_VERSION}}", + ProductId = "{{PRODUCT_ID}}", + InstallPath = "{{INSTALL_PATH}}" + }; + + // 2. 启动更新 + // LaunchAsync 返回 bootstrap 实例。 + // 有更新 → 更新完成后当前进程退出(由 Upgrade 进程重启) + // 无更新 → 继续执行 + await new GeneralUpdateBootstrap() + .SetConfig(config) + .LaunchAsync(); + } +} diff --git a/cli/assets/skills/generalupdate-init/templates/generalupdate.manifest.json b/cli/assets/skills/generalupdate-init/templates/generalupdate.manifest.json new file mode 100644 index 0000000..741a152 --- /dev/null +++ b/cli/assets/skills/generalupdate-init/templates/generalupdate.manifest.json @@ -0,0 +1,9 @@ +{ + "MainAppName": "{{PROJECT_NAME}}.exe", + "UpdateAppName": "Upgrade{{PROJECT_NAME}}.exe", + "ProductId": "{{PRODUCT_ID}}", + "InstallPath": ".", + "UpdatePath": "update", + "ClientVersion": "{{CLIENT_VERSION}}", + "UpgradeClientVersion": "{{CLIENT_VERSION}}" +} diff --git a/cli/assets/skills/generalupdate-migration/SKILL.md b/cli/assets/skills/generalupdate-migration/SKILL.md new file mode 100644 index 0000000..8ce5c64 --- /dev/null +++ b/cli/assets/skills/generalupdate-migration/SKILL.md @@ -0,0 +1,145 @@ +--- +name: generalupdate-migration +description: | + Guide developers through migrating GeneralUpdate from older versions to the + latest stable API (v10.4.6). Covers v9.x → v10 and dev-branch (v10.5.0-beta.2) + → stable (v10.4.6) migration paths. Detects breaking API changes, deprecated + types, and provides automated migration scripts. + Triggers on: "migrate", "migration", "upgrade from v9", "upgrade from v10.5", + "迁移", "旧版本升级", "API 变更", "breaking changes", "不再兼容", + "v10.4.6", "v10.5.0", "开发分支", "稳定版迁移", + "IUpdateHooks not found", "SetSource not found", "OssClient missing", + "ProcessContract missing", "Option system". +when_to_use: | + - User has an existing GeneralUpdate integration and wants to upgrade to latest + - User reports compilation errors after updating NuGet package + - User's code uses v10.5.0+ dev-branch APIs (IUpdateHooks, SetSource, etc.) + - User is on v9.x and needs to migrate to the dual-process architecture + - User sees "missing method" or "type not found" after package update + - User asks about API compatibility between versions + - Run AFTER generalupdate-init if migration is the primary goal +allowed-tools: "Read, Write, Edit, Glob, Grep, Bash" +--- + +# 🔄 GeneralUpdate 迁移指南 + +帮助开发者从旧版本 GeneralUpdate 迁移到最新稳定版 API(v10.4.6)。 + +> ⚠️ **目标版本:NuGet v10.4.6 稳定版** +> 开发分支(v10.5.0-beta.2)API 与稳定版有根本性差异。 + +--- + +## 📋 迁移前需求提取 + +``` +### 当前状态 +- 当前 GeneralUpdate 版本: ______(v9.x / v10.0-10.3 / v10.5.0-beta.x / 不确定) +- 当前 .NET 版本: ______ +- UI 框架: ______ +- 是否使用了 Bowl: ______(是/否) +- 是否使用了 Differential: ______(是/否) + +### 迁移后目标 +- 目标版本: ______(v10.4.6 稳定版 / 继续用开发分支) +- 是否需要新的功能(Bowl/IPC 替换/AOT): ______ +``` + +--- + +## 迁移路径 + +### 路径 A:v9.x → v10.4.6 稳定版 + +这是最大的跳跃。v9.x 和 v10 的架构完全不同。 + +``` +v9.x (单进程, HttpClient 直连) + ↓ + Breaking Changes: + ├── 单进程 → 双进程架构(Client + Upgrade) + ├── HttpClient 直连 → GeneralSpacestation 服务端 + ├── 无 IPC → AES 加密 IPC 文件 + ├── 无 manifest.json → 必须携带 manifest + └── API 命名空间全部重命名 + ↓ +v10.4.6 (双进程, Configinfo + Bootstrap) +``` + +**迁移步骤:** + +```csharp +// ❌ v9.x 写法(不复存在) +// var updater = new GeneralUpdater("https://api/method"); +// updater.Start(); + +// ✅ v10.4.6 写法 +await new GeneralUpdateBootstrap() + .SetConfig(new Configinfo + { + UpdateUrl = "https://your-server.com/Upgrade/Verification", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = "." + }) + .LaunchAsync(); +``` + +| v9.x API | v10.4.6 对应 | 说明 | +|----------|-------------|------| +| `GeneralUpdater` | `GeneralUpdateBootstrap` | 完全重命名 | +| `SetApiUrl()` / `SetMethod()` | `Configinfo.UpdateUrl` | 统一到 Configinfo | +| `CheckUpdateAsync()` | `.LaunchAsync()` | 异步改为返回 Bootstrap 实例 | +| 单进程直接更新 | Client + Upgrade 双进程 | 必须创建独立 Upgrade 项目 | +| N/A | `generalupdate.manifest.json` | 必须随首发版本发布 | + +### 路径 B:v10.5.0-beta.x (开发分支) → v10.4.6 稳定版 + +如果你已经在用开发分支的 API(如 `IUpdateHooks`、`Option` 系统),回退到稳定版需要重写: + +| 开发分支 API (v10.5.0-beta.x) | 稳定版替代 (v10.4.6) | 处理方式 | +|-------------------------------|---------------------|---------| +| `new Option()` / `SetOption()` | 不存在 | 改用 `Configinfo` 属性直接设置 | +| `.Hooks()` / `IUpdateHooks` | 不存在 | 去除 Hooks 引用;在事件监听中做等价逻辑 | +| `.Strategy()` / `IStrategy` | 不存在 | 直接用内置策略;或手动调用 `AbstractStrategy` | +| `SilentPollOrchestrator` | 不存在 | 手动实现定时器 + 调用 Bootstrap | +| `ISslValidationPolicy` | 不存在 | 在 `HttpClientHandler` 层级配置 | +| `IProcessInfoProvider` / `ProcessContract` | 不存在 | 接受默认加密文件 IPC;无法替换 | +| `OssClient (AppType=3,4)` | 不存在 | 只使用 AppType=1(Client) 和 2(Upgrade) | +| 硬编码版本号 | `Configinfo.ClientVersion` | 建议使用 `Assembly.GetEntryAssembly()?.GetName()?.Version` | + +--- + +## 迁移验证清单 + +### 编译验证 +- [ ] `dotnet build` 无错误 +- [ ] 无 `MissingMethodException` 的风险(检查所有方法名是否存在于 v10.4.6) +- [ ] 无 `CS0433` 类型冲突(Core + Bowl 不同时引用) + +### 架构验证 +- [ ] 项目已拆分为 Client + Upgrade 两个独立项目 +- [ ] Upgrade 项目 `AppType = 2` +- [ ] Client 项目 `AppType = 1` +- [ ] `generalupdate.manifest.json` 存在且配置正确 + +### 运行验证 +- [ ] 版本检查 API 可正常返回 +- [ ] 下载后 Upgrade 进程可正常启动 +- [ ] 更新完成后主程序可正常重启 +- [ ] IPC 文件编码设为 `Encoding.UTF8` + +--- + +## ⚠️ 迁移反模式 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **直接在项目中替换 NuGet 版本不修改代码** | 大量编译错误 | 先清理旧 API 引用再升级 NuGet | +| 2 | **认为 v9.x 的配置对象就是 Configinfo** | Configinfo 属性名完全不同 | 对照文档重新写 Configinfo | +| 3 | **试图在 v10.4.6 中使用 dev-branch 的 API** | MissingMethodException | 检查 API 可用性表 | +| 4 | **迁移后不测试 Upgrade 进程** | 主程序能更新但 Upgrade 崩溃 | 两端都要测试 | +| 5 | **保留旧的 v9.x 引用不删除** | 类型冲突 | 清空 csproj 重新添加引用 | diff --git a/cli/assets/skills/generalupdate-security-audit/SKILL.md b/cli/assets/skills/generalupdate-security-audit/SKILL.md new file mode 100644 index 0000000..6326810 --- /dev/null +++ b/cli/assets/skills/generalupdate-security-audit/SKILL.md @@ -0,0 +1,131 @@ +--- +name: generalupdate-security-audit +description: | + Security audit guide for GeneralUpdate deployments. Covers IPC encryption, + AppSecretKey strength, HTTPS enforcement, cross-tenant isolation, ZipSlip + protection, and dependency vulnerability scanning. Generates audit report + with severity ratings and remediation steps. + Triggers on: "security", "audit", "安全审计", "安全审查", "漏洞", + "vulnerability", "penetration", "渗透测试", "encryption", "IPC encryption", + "AppSecretKey", "HTTPS", "cross-tenant", "ZipSlip", "路径穿越", + "hardcoded key", "硬编码", "密钥泄露", "信息泄露", "privilege", + "权限提升", "security review", "安全检查". +when_to_use: | + - User asks about security of their GeneralUpdate deployment + - User wants to audit their update pipeline for vulnerabilities + - User is deploying in a multi-tenant environment + - User's security team requires a compliance review + - User mentions penetration testing or security assessment + - User is using GeneralUpdate in a regulated industry (finance, healthcare, gov) + - Run AFTER generalupdate-init as a post-integration check +allowed-tools: "Read, Write, Edit, Glob, Grep" +--- + +# 🔒 GeneralUpdate 安全审计指南 + +全面覆盖 GeneralUpdate 部署的安全风险面。基于代码审计发现(17 CRITICAL/HIGH 项)和最佳实践。 + +--- + +## 📋 审计前信息收集 + +``` +### 部署环境 +- 部署模式: ______(内网 / 公网 / 混合) +- 租户模式: ______(单租户 / 多租户) +- 客户端数量: ______ +- 客户端操作系统: ______(Windows / Linux / macOS / 混合) + +### 服务端 +- 后端类型: ______(GeneralSpacestation / 自定义 / OSS) +- 传输协议: ______(HTTP / HTTPS) +- 认证方式: ______(Bearer / Basic / HMAC / 无) +- API 是否公开访问: ______(是 / 否,有网络隔离) + +### 客户端 +- GeneralUpdate 版本: ______ +- 是否使用 IPC: ______(是 / 否) +- 是否使用 Bowl: ______(是 / 否) +- 是否使用 Differential: ______(是 / 否) +``` + +--- + +## 安全审计矩阵 + +| # | 检查项 | 严重度 | 描述 | 修复措施 | +|---|--------|--------|------|---------| +| S01 | **AppSecretKey 强度** | 🔴 CRITICAL | 密钥长度不足、纯字母、与示例代码相同 | 使用 ≥ 32 字符,大小写+数字+符号的随机密钥 | +| S02 | **IPC 加密** | 🔴 CRITICAL | 默认 IPC 加密密钥硬编码在二进制中 | 确保 AppSecretKey 唯一且服务端/客户端一致 | +| S03 | **HTTPS 传输** | 🟠 HIGH | UpdateUrl 使用 HTTP 而非 HTTPS | 生产环境强制 HTTPS;配置 HSTS | +| S04 | **ZipSlip 路径穿越** | 🔴 CRITICAL | 解压 ZIP 时未验证 ../ 路径 | 验证压缩包条目路径是否在目标目录内 | +| S05 | **多租户隔离** | 🔴 CRITICAL | 服务端未按 ProductId 隔离租户 | 服务端添加租户身份验证中间件 | +| S06 | **事件日志泄露** | 🟡 MEDIUM | ExceptionEventArgs 日志可能包含敏感路径 | 脱敏后记录,过滤路径和密钥 | +| S07 | **差分包签名** | 🟠 HIGH | 差分补丁无数字签名验证 | 对更新包进行 Authenticode 签名 | +| S08 | **临时目录权限** | 🟡 MEDIUM | 临时解压目录权限可能过大 | 设置仅为当前用户可读写 | +| S09 | **OSS Bucket 权限** | 🟠 HIGH | 更新包存储 Bucket 设为公共读 | 设置为私有,使用预签名 URL | +| S10 | **依赖版本漏洞** | 🟡 MEDIUM | GeneralUpdate 及其依赖可能存在已知 CVE | 定期检查 NuGet 依赖安全公告 | +| S11 | **回滚攻击** | 🟠 HIGH | 攻击者可提交降级版本号强制安装旧版本 | 服务端校验版本号单调递增 | +| S12 | **下载完整性** | 🟠 HIGH | 下载的更新包无完整性校验 | 确保 Pipeline 包含 HashMiddleware | +| S13 | **Bowl 提权** | 🟡 MEDIUM | Bowl 崩溃守护以高权限运行可能被滥用 | 以最小必要权限运行 Bowl | +| S14 | **信息泄露通过 manifest** | 🔵 LOW | manifest.json 中的 ProductId、版本号可被枚举 | 非公开环境下不暴露 manifest 文件 | + +--- + +## 审计报告输出格式 + +完成审计后按以下格式输出: + +``` +## 🔒 GeneralUpdate 安全审计报告 + +### 概要 +- 项目: ______ +- 审计日期: ______ +- 总体评分: A/B/C/D/F +- 严重问题: ______ 个 +- 高风险: ______ 个 +- 中风险: ______ 个 +- 低风险: ______ 个 + +### 严重问题(必须立即修复) +- S01 AppSecretKey 强度: ⚠️ 当前密钥长度为 X,需要 ≥ 32 + 修复: ______ + +### 高风险(建议尽快修复) +... + +### 中风险(评估后修复) +... + +### 低风险(记录在案) +... + +### 修复建议优先级 +1. 立即:S01, S03, S04 +2. 本周:S05, S07, S09 +3. 本月:S08, S10, S11 +``` + +--- + +## 安全配置检查清单 + +- [ ] AppSecretKey 长度 ≥ 32 字符,混合大小写+数字+符号 +- [ ] 生产环境使用 HTTPS +- [ ] IPC 文件编码设为 Encoding.UTF8 +- [ ] Pipeline 包含 HashMiddleware 做完整性校验 +- [ ] OSS Bucket 权限设为私有 +- [ ] 服务端按 ProductId 隔离租户 +- [ ] 版本号严格单调递增 +- [ ] 更新包进行 Authenticode 签名 +- [ ] Zip 解压有路径穿越防护 +- [ ] 日志中不记录敏感信息 + +--- + +## 相关技能 + +- `/generalupdate-init` — 修复审计发现的问题 +- `/generalupdate-advanced` — IPC 替换、自定义认证 +- `/generalupdate-troubleshoot` — 已知安全问题参考 diff --git a/cli/assets/skills/generalupdate-strategy/SKILL.md b/cli/assets/skills/generalupdate-strategy/SKILL.md new file mode 100644 index 0000000..1623652 --- /dev/null +++ b/cli/assets/skills/generalupdate-strategy/SKILL.md @@ -0,0 +1,215 @@ +--- +name: generalupdate-strategy +description: | + Configure GeneralUpdate update strategies for any deployment scenario. + Covers 6 strategies with decision tree, 4 Client-Upgrade update scenes, + and platform-specific considerations (Windows/Linux/Mac). + Each strategy includes real issue workarounds from GitHub/Gitee. + Triggers on: "configure strategy", "OSS update", "silent update", "differential update", + "cross version update", "push update", "SignalR push", "更新策略", "OSS方案", + "静默更新", "差分更新", "跨版本", "推送更新", "决策", "选策略", + "how to update without a server", "background update", "reduce download size", + "skip versions", "force update", "CVP", "chain packages". + Also triggers when user mentions specific deployment constraints. +when_to_use: | + - User asks about different ways to deliver updates (strategy selection) + - User wants silent/background mode for long-running apps + - User wants differential/delta updates to save bandwidth + - User mentions OSS / S3 / MinIO / object storage (no server) + - User wants cross-version jump (skip intermediate versions) + - User wants server-push (SignalR) updates + - User is confused about which strategy fits their use case + - Best used after generalupdate-init +allowed-tools: "Read, Write, Edit, Glob" +--- + +# ⚙️ GeneralUpdate 更新策略完全指南 + +> ⚠️ **针对 NuGet v10.4.6 稳定版**。该版本使用 `Configinfo` 配置,无可编程 `Option` 系统。 + +--- + +## 📋 用户需求提取(推荐策略前必须确认) + +``` +### 部署环境 +- 是否有后端服务: ______(是/否/计划中) +- 服务端类型: ______(GeneralSpacestation / 自定义 API / S3/MinIO / 无) +- 客户端数量: ______(几十/几百/几千/万+) +- 客户端是否 7×24 运行: ______(是/否) + +### 更新需求 +- 是否需要节省带宽: ______(是/否 → 推荐差分) +- 是否需要跳过中间版本: ______(是/否 → 推荐 CVP) +- 是否需要服务端主动触发: ______(是/否 → 推荐 SignalR) +- 是否需要用户无感知: ______(是/否 → 推荐静默) +- 是否需要显示更新进度: ______(是/否 → 推荐标准 + UI) + +### 约束条件 +- 目标平台: ______(Windows/Linux/macOS/多平台) +- 网络环境: ______(内网/公网/离线) +- 是否需要崩溃恢复: ______(是/否 → 配合 Bowl) +``` + +--- + +## 策略决策树(详细版) + +``` +你的应用有后端服务吗? +├── 有 +│ ├── 需要服务端主动推送更新? +│ │ └── YES → ⑥ SignalR 推送(需额外部署 SignalR Hub) +│ └── NO +│ ├── 需要节省下载带宽? +│ │ ├── YES → ④ 差分更新(生成补丁包,减少 60-90% 体积) +│ │ └── NO +│ │ ├── 需要跳过中间版本直达最新? +│ │ │ ├── YES → ⑤ 跨版本 CVP(需服务端额外构建) +│ │ │ └── NO +│ │ │ └── ① 标准客户端-服务端(推荐新手入门) +│ └── 需要后台无声升级? +│ └── YES → ③ 静默更新(基于标准或 OSS + 定时轮询) +│ +└── 没有(只有对象存储 S3/MinIO) + ├── 需要节省带宽? + │ ├── YES → ④ 差分更新(OSS + 差分补丁,v10.4.6 支持有限) + │ └── NO + │ └── ② OSS 标准(最低成本,零服务端) + │ + └── 需要后台无声升级? + └── YES → ③ 静默更新(OSS + 定时检查) + +### 混合策略组合 + +常见组合方案: +| 场景 | 策略组合 | 说明 | +|------|---------|------| +| 标准 Web 应用 | ① 标准 + 🎨 UI | 有后端,显示进度 | +| 无服务端节省带宽 | ② OSS + ④ 差分 | 零服务端 + 增量更新 | +| 长期运行后台服务 | ③ 静默(基于 ① 或 ②) | 用户无感知 | +| 强制升级 | ⑤ CVP + ⑥ SignalR | 跳过旧版本,主动推送 | +| 企业级高可靠 | ① 标准 + Bowl + ③ 静默 | 完整链路 | +``` + +--- + +## 6 种策略详细对比 + +| 策略 | 服务端 | 说明 | +|------|:------:|------| +| **① 标准客户端-服务端** | ✅ GeneralSpacestation | 有后端的中大型应用(推荐入门) | +| **② OSS 对象存储** | ❌ 仅 S3/MinIO | 无后端,最低成本 | +| **③ 静默更新** | ✅ 同①或② | 后台无声升级 | +| **④ 差分更新** | ✅ 需差分构建 | 增量补丁节省带宽 | +| **⑤ 跨版本 CVP** | ✅ 需 CVP 构建 | 跳过中间版本直跳 | +| **⑥ SignalR 推送** | ✅ 需 SignalR Hub | 服务端主动推送 | + +--- + +## 集成代码 + +所有策略使用相同的 `Configinfo` 配置模式: + +```csharp +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; + +var config = new Configinfo +{ + UpdateUrl = "https://your-server.com/api", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", +}; + +await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListener*(...) + .LaunchAsync(); +``` + +具体示例参见 `examples/` 目录下的策略文件。 + +--- + +## 平台特定差异 + +| 平台 | 特性 | +|------|------| +| **Windows** | 完整功能 | +| **Linux** | 部分功能(无 Bowl) | +| **macOS** | 同 Linux | + +--- + +## 已知问题 + +| # | 问题 | 规避方案 | +|---|------|---------| +| 1 | OSS 模式不区分 Main/Upgrade 更新 | 接受此行为 | +| 2 | UpgradeApp.exe 必须放在 update/ 子目录 | 按规范部署 | +| 3 | NuGet 版本冲突导致 "Method not found" | Client 和 Upgrade 使用相同版本号 | +| 4 | 无限升级循环 | 确保 manifest.json 版本号正确 | +| 5 | SignalR HubConnection Dispose 后重连崩溃 | Dispose 时将连接置 null | + +--- + +## ✅ 策略选择验证清单 + +### 策略匹配度 +- [ ] 选定的策略与部署环境匹配(有后端→标准/无后端→OSS) +- [ ] 带宽需求与策略匹配(大文件→差分,版本多→CVP) +- [ ] 用户体验目标与策略匹配(需要交互→标准+UI,后台→静默) +- [ ] 平台兼容性确认(Linux/macOS 不支持 Bowl) + +### OSS 策略 +- [ ] Bucket 权限设置为私有 +- [ ] 更新包的 URL 可公开访问或使用预签名 URL +- [ ] Upgrade.exe 放在 `update/` 子目录(OSS 特有要求) +- [ ] 没有区分 Main/Upgrade 独立更新包(OSS 限制,接受) + +### 静默策略 +- [ ] 轮询间隔合理(建议 30-60 分钟,太短耗电/流量) +- [ ] 有"新版本可用"的系统通知或托盘图标提示 +- [ ] 下载完成后再通知用户重启,而非下载前 +- [ ] 后台下载有流量/电量优化(WiFi 下才下载大包) + +### SignalR 推送 +- [ ] HubConnection 的生命周期管理完善 +- [ ] 重连逻辑(自动重试 3 次,间隔递增) +- [ ] Dispose 时将 HubConnection 置 null(否则重连崩溃) +- [ ] 推送消息有超时保护和降级策略(推送失败→回退到轮询) + +### 差分策略 +- [ ] 服务端有差分包生成机制(`DifferentialCore.CleanAsync`) +- [ ] 客户端 Pipeline 配置了 PatchMiddleware +- [ ] 注意大文件差分可能触发的整数溢出(v10.4.6 已修复 #514) +- [ ] Linux/macOS 上 BSDIFF 补丁兼容性已验证 + +--- + +## ⚠️ 反模式清单 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **有后端却选 OSS** | 浪费后端服务能力,失去版本管理 | 有后端 → 标准策略 | +| 2 | **低频轮询(每天 1 次)** | 用户等很久才收到更新 | 静默模式 30-60 分钟轮询 | +| 3 | **高频轮询(每分钟 1 次)** | 浪费带宽和电池 | 静默模式建议 ≥ 30 分钟 | +| 4 | **SignalR 连接永不释放** | 内存泄漏 | 页面/应用关闭时 Dispose HubConnection | +| 5 | **差分包太大(> 2GB)** | 整数溢出导致进程崩溃(BSD-514) | 分多个版本发布,或用全量包 | +| 6 | **CVP 跳版本不测试中间版本 API 变更** | 客户端数据迁移失败 | 在服务端做好版本兼容测试 | +| 7 | **OSS 包名不包含版本号** | 客户端版本比较逻辑异常 | `MyApp_1.0.0.0.zip` 格式命名 | +| 8 | **静默更新后不通知用户重启** | 用户不知道新版本已下载 | 下载完成后通知 + 延迟重启选项 | + +--- + +## 相关技能 + +- `/generalupdate-init` — 如果还未配置 Bootstrap +- `/generalupdate-ui` — 如果需要更新界面 +- `/generalupdate-troubleshoot` — 如果遇到问题 +- `/generalupdate-advanced` — 高级定制(适用于开发分支) diff --git a/cli/assets/skills/generalupdate-strategy/examples/ClientServerStrategy.cs b/cli/assets/skills/generalupdate-strategy/examples/ClientServerStrategy.cs new file mode 100644 index 0000000..a41b4e9 --- /dev/null +++ b/cli/assets/skills/generalupdate-strategy/examples/ClientServerStrategy.cs @@ -0,0 +1,43 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +/// +/// 标准客户端-服务端更新策略 +/// +/// 适用于已部署 GeneralSpacestation 或兼容后端的应用。 +/// 后端要求: +/// - POST /Upgrade/Verification — 版本验证 +/// - (可选) POST /Upgrade/Report — 状态上报 +/// +/// NuGet: dotnet add package GeneralUpdate.Core +/// ⚠️ 针对 NuGet v10.4.6 稳定版 +/// +public static class ClientServerStrategy +{ + public static async Task RunAsync() + { + var config = new Configinfo + { + UpdateUrl = "https://your-server.com/api", + AppSecretKey = "your-32-char-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", + }; + + await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerUpdateInfo((_, e) => + Console.WriteLine($"[版本发现] 发现 {e.Info?.Body?.Count ?? 0} 个版本")) + .AddListenerMultiDownloadStatistics((_, e) => + Console.WriteLine($"进度: {e.ProgressPercentage}% | {e.Speed}")) + .AddListenerMultiDownloadCompleted((_, e) => + Console.WriteLine($"下载完成: {e.Version} (IsComplated={e.IsComplated})")) + .AddListenerException((_, e) => + Console.WriteLine($"错误: {e.Message}")) + .LaunchAsync(); + } +} diff --git a/cli/assets/skills/generalupdate-strategy/examples/CrossVersionStrategy.cs b/cli/assets/skills/generalupdate-strategy/examples/CrossVersionStrategy.cs new file mode 100644 index 0000000..f282c58 --- /dev/null +++ b/cli/assets/skills/generalupdate-strategy/examples/CrossVersionStrategy.cs @@ -0,0 +1,46 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +/// +/// 跨版本直跳更新(CVP — Cross-Version Package) +/// +/// 适用于用户长期未更新(如 v1.0 → v3.0),中间版本逐个下载太慢的场景。 +/// +/// 服务端构建:取两个全量包 ZIP,通过 DiffPipeline 生成差分包。 +/// 客户端优先尝试 CVP 包,失败自动退化为链式重试(v5.0+ 特性)。 +/// +/// NuGet: dotnet add package GeneralUpdate.Core +/// +public static class CrossVersionStrategy +{ + public static async Task RunAsync() + { + var config = new Configinfo + { + UpdateUrl = "https://your-server.com/api", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", + }; + + await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerUpdateInfo((_, e) => + { + if (e.Info?.Body != null) + foreach (var v in e.Info.Body) + Console.WriteLine($"[CVP] 版本: {v.Version} (跨版本: {v.IsForcibly})"); + }) + .AddListenerMultiDownloadStatistics((_, e) => + Console.WriteLine($"[CVP] 下载: {e.ProgressPercentage}%")) + .AddListenerMultiDownloadCompleted((_, e) => + Console.WriteLine($"[CVP] 包下载完成: {e.Version}")) + .AddListenerException((_, e) => + Console.WriteLine($"[CVP] 错误: {e.Message}")) + .LaunchAsync(); + } +} diff --git a/cli/assets/skills/generalupdate-strategy/examples/DifferentialStrategy.cs b/cli/assets/skills/generalupdate-strategy/examples/DifferentialStrategy.cs new file mode 100644 index 0000000..6a5a8a4 --- /dev/null +++ b/cli/assets/skills/generalupdate-strategy/examples/DifferentialStrategy.cs @@ -0,0 +1,52 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +/// +/// 差分增量更新策略 +/// +/// 适用于应用体积大(>100MB)或带宽受限的场景。 +/// 仅传输变动的文件部分,节省 60-90% 带宽。 +/// +/// 工作原理: +/// 服务端调用 DiffPipeline 生成 .patch 文件 +/// 客户端下载后自动应用补丁 +/// +/// NuGet: +/// dotnet add package GeneralUpdate.Core +/// +/// ⚠️ 注意:GeneralUpdate.Core v10.4.6 稳定版已内置差分支持, +/// 无需额外安装 GeneralUpdate.Differential 包。 +/// +/// ⚠️ 已知问题: +/// 1. 同名文件在不同目录可能封包出错 +/// 2. 旧 patch 临时文件残留可能导致后续更新失败 +/// +public static class DifferentialStrategy +{ + public static async Task RunAsync() + { + var config = new Configinfo + { + UpdateUrl = "https://your-server.com/api", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", + + // 服务端配置差分时自动启用 + }; + + await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerMultiDownloadStatistics((_, e) => + Console.WriteLine($"[差分] 下载: {e.ProgressPercentage}% | {e.Speed}")) + .AddListenerMultiDownloadCompleted((_, e) => + Console.WriteLine($"[差分] 版本 {e.Version} 处理完成")) + .AddListenerException((_, e) => + Console.WriteLine($"[差分] 错误: {e.Message}")) + .LaunchAsync(); + } +} diff --git a/cli/assets/skills/generalupdate-strategy/examples/OssStrategy.cs b/cli/assets/skills/generalupdate-strategy/examples/OssStrategy.cs new file mode 100644 index 0000000..2f36cd2 --- /dev/null +++ b/cli/assets/skills/generalupdate-strategy/examples/OssStrategy.cs @@ -0,0 +1,48 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +/// +/// OSS 对象存储更新策略 +/// +/// 适用于没有后端服务,只有对象存储的场景(AWS S3 / MinIO / 阿里云OSS)。 +/// 工作原理:读取 versions.json 版本清单 → 比较版本 → 下载 ZIP → 更新 +/// +/// OSS 上需要的文件: +/// your-bucket/ +/// ├── versions.json ← 版本清单 +/// └── v1.1.0.0/ +/// └── update.zip +/// +/// ⚠️ 已知问题: +/// 1. OSS 模式不区分 MainApp 和 UpgradeApp 更新 +/// 2. UpgradeApp.exe 必须放在 update/ 子目录中 +/// +/// NuGet: dotnet add package GeneralUpdate.Core +/// +public static class OssStrategy +{ + public static async Task RunAsync() + { + var config = new Configinfo + { + UpdateUrl = "https://your-storage.com/versions.json", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", + }; + + await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerMultiDownloadStatistics((_, e) => + Console.WriteLine($"OSS 下载: {e.ProgressPercentage}%")) + .AddListenerMultiDownloadCompleted((_, e) => + Console.WriteLine($"OSS 版本 {e.Version} 下载完成")) + .AddListenerException((_, e) => + Console.WriteLine($"OSS 错误: {e.Message}")) + .LaunchAsync(); + } +} diff --git a/cli/assets/skills/generalupdate-strategy/examples/PushStrategy.cs b/cli/assets/skills/generalupdate-strategy/examples/PushStrategy.cs new file mode 100644 index 0000000..c9f4a8c --- /dev/null +++ b/cli/assets/skills/generalupdate-strategy/examples/PushStrategy.cs @@ -0,0 +1,90 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; +using Microsoft.AspNetCore.SignalR.Client; + +/// +/// SignalR 推送更新策略 +/// +/// 适用于需要服务端主动控制更新时机的场景。 +/// 客户端连接 SignalR Hub,服务端推送更新通知后触发更新。 +/// +/// NuGet: +/// dotnet add package GeneralUpdate.Core +/// dotnet add package Microsoft.AspNetCore.SignalR.Client +/// +/// ⚠️ 已知问题: +/// HubConnection Dispose 后不置 null,重连时抛 ObjectDisposedException。 +/// 解决方案:在 Dispose 后将 _connection 置 null。 +/// +public static class PushStrategy +{ + public static async Task RunAsync() + { + var updateUrl = "https://your-server.com/api"; + var secretKey = "your-secret-key"; + var hubUrl = "https://your-server.com/hub/upgrade"; + + var config = new Configinfo + { + UpdateUrl = updateUrl, + AppSecretKey = secretKey, + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", + }; + + // 连接到 SignalR Hub + var connection = new HubConnectionBuilder() + .WithUrl(hubUrl) + .WithAutomaticReconnect() + .Build(); + + connection.On("OnPushUpgrade", async (message) => + { + Console.WriteLine($"[推送] 收到更新通知: {message}"); + await StartUpdateAsync(config); + }); + + connection.On("OnForceUpgrade", async (message) => + { + Console.WriteLine($"[推送] 收到强制更新通知: {message}"); + await StartUpdateAsync(config); + }); + + try + { + await connection.StartAsync(); + Console.WriteLine("[推送] 已连接到更新推送服务"); + } + catch (Exception ex) + { + Console.WriteLine($"[推送] 连接失败: {ex.Message}"); + } + + // 保持应用运行 + await Task.Delay(Timeout.Infinite); + } + + private static async Task StartUpdateAsync(Configinfo config) + { + try + { + await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerMultiDownloadStatistics((_, e) => + Console.WriteLine($"[推送更新] 下载: {e.ProgressPercentage}%")) + .AddListenerMultiDownloadCompleted((_, e) => + Console.WriteLine($"[推送更新] 下载完成: {e.Version}")) + .AddListenerException((_, e) => + Console.WriteLine($"[推送更新] 错误: {e.Message}")) + .LaunchAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"[推送更新] 启动失败: {ex.Message}"); + } + } +} diff --git a/cli/assets/skills/generalupdate-strategy/examples/SilentStrategy.cs b/cli/assets/skills/generalupdate-strategy/examples/SilentStrategy.cs new file mode 100644 index 0000000..88714d4 --- /dev/null +++ b/cli/assets/skills/generalupdate-strategy/examples/SilentStrategy.cs @@ -0,0 +1,43 @@ +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Download; + +/// +/// 静默后台更新策略 +/// +/// 适用于用户长期不关闭应用的场景(如桌面工具、监控面板)。 +/// GeneralUpdate 在检测到更新后自动后台下载,下次启动时应用新版本。 +/// +/// ⚠️ 注意:v10.4.6 稳定版没有 SilentPollOrchestrator 或 SetOption API。 +/// 静默行为由配置和启动参数控制。应用关闭时自动触发升级。 +/// +/// NuGet: dotnet add package GeneralUpdate.Core +/// +public static class SilentStrategy +{ + public static async Task RunAsync() + { + var config = new Configinfo + { + UpdateUrl = "https://your-server.com/api", + AppSecretKey = "your-secret-key", + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", + }; + + await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerUpdateInfo((_, e) => + Console.WriteLine($"[静默] 发现 {e.Info?.Body?.Count ?? 0} 个版本")) + .AddListenerMultiDownloadStatistics((_, e) => + Console.WriteLine($"[静默] 后台下载: {e.ProgressPercentage}%")) + .AddListenerMultiDownloadCompleted((_, e) => + Console.WriteLine($"[静默] 版本 {e.Version} 就绪")) + .AddListenerException((_, e) => + Console.WriteLine($"[静默] 错误: {e.Message}")) + .LaunchAsync(); + } +} diff --git a/cli/assets/skills/generalupdate-troubleshoot/SKILL.md b/cli/assets/skills/generalupdate-troubleshoot/SKILL.md new file mode 100644 index 0000000..d7daa15 --- /dev/null +++ b/cli/assets/skills/generalupdate-troubleshoot/SKILL.md @@ -0,0 +1,152 @@ +--- +name: generalupdate-troubleshoot +description: | + Complete diagnostic reference for ALL known GeneralUpdate issues — powered by 50+ + real GitHub/Gitee issues and full code audit findings (17 critical/high + 14 medium + 10 low). + Covers: install failures, dual-process issues, IPC corruption, OSS failures, silent mode bugs, + differential patch errors, cross-version (CVP) problems, Bowl crash daemon, SignalR push, + file system issues, version comparison, cross-tenant leakage, and AOT compatibility. + Provides root causes, fixes, workarounds, and a 6-step universal diagnostic workflow. + Triggers on: "update failed", "not working", "error", "bug", "crash", "exception", + "升级失败", "失败", "报错", "不行", "不工作", "没反应", "问题", "排查", + "method not found", "file locked", "path too long", "Chinese garbled", "乱码", + "version incorrect", "版本不对", "infinite loop", "死循环", "bowl not working", + "push not working", "signalr error", "IPC error", "AOT trim", "跨租户", "权限", + "permission denied", "access denied", "silent mode", "静默", "差分", "增量". + Load this skill AUTOMATICALLY when user reports any error, unexpected behavior, + or failure symptom — run the diagnostic workflow before escalating. +when_to_use: | + - User reports an error, failure, crash, or unexpected behavior during ANY step + - User says "it's not working", "something went wrong", has error screenshots + - User describes specific error messages, exception stack traces, or symptoms + - User asks about known bugs, issues, or problems with GeneralUpdate + - User asks for debugging help, diagnostic steps, troubleshooting + - User mentions Upgrade process not starting, silent mode not working, or Bowl issues + - User reports version numbers being wrong or update loops + - User is on Linux/macOS and has permission or IPC problems + - User mentions cross-tenant or multi-tenant deployment of server + - User asks about AOT trimming or NativeAOT compatibility + - User just completed integration and hit an error — run diagnostics + - Always run AFTER generalupdate-init or generalupdate-ui if the user hits issues +allowed-tools: "Read, Write, Edit, Glob, Grep, Bash" +--- + +# 🩺 GeneralUpdate 故障排查 + +综合性诊断系统 — 覆盖 50+ 已知问题,均可追溯到 GitHub/Gitee Issue 或代码审计发现。 + +--- + +## 📋 用户症状提取(诊断前必须收集) + +``` +### 必填信息 +- 症状描述: ______ +- 错误信息/堆栈: ______ +- GeneralUpdate 版本: ______ +- 平台: ______(Windows / Linux / macOS) +- .NET 版本: ______ +- 更新策略: ______(标准 / OSS / 静默 / 差分 / 跨版本 / 推送) +- 最近是否改过配置: ______(是/否,改了啥) + +### 可选信息 +- 事件监听中是否有异常(ExceptionEventArgs): ______ +- 是否有日志(Logs/generalupdate-trace *.log): ______ +- 问题是否可复现: ______(是/否,频率) +- 首次出现时间点: ______ +``` + +--- + +## 工作流程 + +``` +1. 症状收集 + ├── 用户描述的症状是什么? + ├── 错误信息/堆栈是什么? + ├── GeneralUpdate 版本号? + ├── 平台(Windows/Linux/macOS)? + └── 更新策略(标准/OSS/静默)? + +2. 症状匹配 + ├── 优先:python3 scripts/search.py "<症状>" --domain issue + │ └── 匹配到 → 给出根因 + 修复 + 代码 + └── 未匹配 → 降级到 reference.md 全文搜索 + +3. 提供修复 + ├── 具体的代码修改、配置调整、版本升级建议 + └── 预防措施(如何避免再发生) + +4. 验证 + └── 确认修复后问题解决 +``` + +## 症状搜索(推荐) + +优先使用 BM25 搜索引擎精确匹配已知问题,而不是在 reference.md 中手动查找: + +```bash +# 自然语言搜索已知问题 +python3 skills/generalupdate-troubleshoot/scripts/search.py "升级后应用启动不了" --domain issue +python3 skills/generalupdate-troubleshoot/scripts/search.py "方法找不到 MethodNotFound" --domain issue +python3 skills/generalupdate-troubleshoot/scripts/search.py "中文乱码 garbled" --domain issue + +# 搜索策略相关问题 +python3 skills/generalupdate-troubleshoot/scripts/search.py "OSS 权限问题" --domain strategy +``` + +## 症状分级 + +reference.md 中的问题按严重度分级: + +| 级别 | 颜色 | 含义 | 数量 | +|:----:|:----:|------|:----:| +| C | 🔴 **致命** | 阻断性故障、数据损坏、安全漏洞 | 8 | +| H | 🟠 **高** | 场景阻断、功能失效、需要升级 | 11 | +| M | 🟡 **中** | 功能异常、需要配置调整 | 20 | +| L | 🔵 **低** | 代码气味、边缘情况、已知行为 | 12 | + +**完整清单请查阅 `reference.md`** + +--- + +## ✅ 通用诊断前检查清单 + +在深入诊断前,先快速排查最常见的原因: + +### 运行环境检查 +- [ ] 目标机器安装了正确的 .NET 运行时(版本与发布框架匹配) +- [ ] 目标机器上有写入权限(InstallPath 目录可写) +- [ ] 防火墙未阻断 UpdateUrl 的通信端口 +- [ ] 磁盘空间充足(至少 2× 更新包大小) +- [ ] Linux/macOS:UpgradeApp 有 `chmod +x` 执行权限 + +### 版本检查 +- [ ] Client 和 Upgrade 项目 NuGet 版本**完全一致** +- [ ] 服务端返回的版本号是 4 段式(如 1.0.0.0) +- [ ] manifest.json 中 `mainAppName` 与实际进程名匹配 +- [ ] `AppType` 设置正确(Client = 1, Upgrade = 2) + +### 配置检查 +- [ ] `Configinfo` 的 6 个必填字段都已设置 +- [ ] `UpdateUrl` 可通过 HTTP GET 访问并返回合法 JSON +- [ ] `AppSecretKey` 与服务端配置一致(长度 ≥ 16 字符) +- [ ] UpgradeApp.exe 存在于发布目录的 `update/` 子目录中 + +### 日志检查 +- [ ] 查看 `Logs/generalupdate-trace-*.log`(如有) +- [ ] 检查事件监听中的 `ExceptionEventArgs` +- [ ] 检查 `MultiDownloadErrorEventArgs` 中的异常 + +--- + +## ⚠️ 诊断阶段的反模式 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **只看错误信息不看事件** | 错过 ExceptionEventArgs 中的详细信息 | 订阅所有 6 个事件 | +| 2 | **日志文件路径不对就认为无日志** | 漏掉关键诊断信息 | 在 InstallPath/Logs 下查找 | +| 3 | **只检查 Client 不检查 Upgrade 进程** | 问题在 Upgrade 端但诊断方向全错 | 两端都要检查 | +| 4 | **升级问题直接改代码** | 可能是服务端配置问题而非客户端 Bug | 优先检查服务端返回的版本信息 | +| 5 | **忽略 NuGet 版本一致性** | 方向错,"Method not found" 根因是版本不一致 | 第一个就要检查版本 | +| 6 | **只在 Debug 环境测试** | Release 环境可能缺少运行时文件 | 在发布/生产环境复现 diff --git a/cli/assets/skills/generalupdate-troubleshoot/data/issues.csv b/cli/assets/skills/generalupdate-troubleshoot/data/issues.csv new file mode 100644 index 0000000..c814db2 --- /dev/null +++ b/cli/assets/skills/generalupdate-troubleshoot/data/issues.csv @@ -0,0 +1,52 @@ +id,severity,symptom_en,symptom_zh,cause,solution,code_ref,workaround,keywords +C1,C,"Upgrade process not started / FileNotFoundException: upgrade application not found","升级进程没启动 / FileNotFoundException","UpgradeApp.exe not shipped with main application in first release","Ensure UpgradeApp.exe is included in the publish directory from the first release","Configinfo.UpdatePath, Configinfo.UpdateAppName","OSS mode: place Upgrade.exe in update/ subdirectory","upgrade not start, file not found, 升级没启动, FileNotFoundException, missing upgrade" +C2,C,"Method not found exception at runtime","运行时 Method not found 异常","Client and Upgrade projects use different GeneralUpdate NuGet versions","Lock Client.csproj and Upgrade.csproj to the exact same NuGet version","GeneralUpdate.Core, .csproj","Pin version in Directory.Build.props to enforce consistency","method not found, MissingMethodException, 方法找不到, NuGet conflict, version mismatch" +C3,C,"BSOD / OutOfMemory / Process crash on differential patch","蓝屏/内存溢出/进程崩溃差分补丁","BsdiffDiffer.WriteInt64 overflows on long.MinValue negation; control value > int.MaxValue truncates to negative","Update to v10.4.6+ (#514 fixed). If cannot update: add MaxInputFileSize limit in differential engine","BsdiffDiffer, PatchMiddleware","Limit patch file size at application level","BSOD, crash, OOM, differential, patch, 蓝屏, 崩溃, 内存溢出, BSDIFF" +C4,C,"PathTooLongException during backup (recursive nesting)","备份递归嵌套 PathTooLongException","StorageManager.Backup() creates backup dir INSIDE install path; empty SkipDirectorys list doesn't trigger default skip logic","Set SkipDirectorys on Configinfo to exclude backup directories","Configinfo.SkipDirectorys, StorageManager.Backup","Set SkipDirectorys to exclude '.backups','backup-'","path too long, backup recursion, PathTooLongException, 路径过长, 备份嵌套" +C5,C,"IPC encryption key hardcoded / weak","IPC 加密密钥硬编码/弱密钥","Default IPC encryption key is a hardcoded string; AppSecretKey is reused as encryption key","Use strong AppSecretKey (>= 32 chars, mixed case + numbers + symbols)","Configinfo.AppSecretKey, ProcessInfoJsonContext","Rotate key periodically, audit encryption in IPC path","IPC encryption, hardcoded key, 加密, 硬编码, security, AppSecretKey" +C6,C,"ZipSlip / path traversal in decompression","ZIP 解压路径穿越漏洞","CompressMiddleware.Extract doesn't validate ../ in zip entry paths against a whitelist before extraction","Validate zip entry paths against target directory; reject entries with ../","CompressMiddleware, ZipMiddleware","Scan entry names before extraction for path traversal patterns","zip slip, path traversal, security, 解压漏洞, 目录穿越" +C7,C,"Cross-tenant version leakage in multi-tenant deployment","多租户部署中跨租户版本泄露","Server API returns version info without tenant isolation; ProductId not validated against tenant context","Update server to validate ProductId against tenant context; add tenant-scoped API keys","GeneralSpacestation, ProductId","Add tenant header validation middleware on server side","multi-tenant, cross-tenant, security, 多租户, 跨租户, ProductId" +C8,C,"EventManager.Instance returned after Dispose","EventManager.Dispose 后 Instance 仍可访问","EventManager.Instance property returns the instance even after Dispose() is called","Call Clear() first, then Dispose(); do not call Instance after Dispose","EventManager.Instance, EventManager.Dispose","Wrap in try-finally; set to null after dispose","EventManager, dispose, singleton, memory leak, after dispose" +H1,H,"Chinese text garbled on Linux/macOS","Linux/macOS 中文乱码","IPC file encoding defaults to system default, not UTF-8","Set Encoding.UTF8 in PipelineContext and Configinfo","PipelineContext Encoding, Configinfo","Always explicitly set Encoding.UTF8","Chinese garbled, encoding, UTF8, 乱码, 编码, Linux, macOS" +H2,H,"Infinite update loop","无限升级循环","manifest.json version number doesn't match actual installed version or write-back fails","Ensure manifest.json version number is correct; implement write-back after update","generalupdate.manifest.json, ClientVersion","Add version write-back logic after successful update","infinite loop, 死循环, version mismatch, manifest" +H3,H,"MultiDownloadCompletedEventArgs.IsComplated typo causes binding failure","MultiDownloadCompletedEventArgs.IsComplated 拼写错误导致绑定失败","v10.4.6 API has typo IsComplated (not IsCompleted). UI bindings using correct spelling get null","Use IsComplated in code; or write a wrapper property that redirects to IsComplated","MultiDownloadCompletedEventArgs","Wrap in adapter class with IsCompleted alias","IsComplated, IsCompleted, typo, spelling, binding, 拼写错误" +H4,H,"OSS mode doesn't distinguish Main vs Upgrade update packages","OSS 模式不区分 Main/Upgrade 更新包","OSS strategy treats all packages the same; no MainOnly/UpgradeOnly differentiation","Accept this behavior for OSS; upgrade to standard server strategy if you need fine-grained control","OSSStrategy","Use standard server strategy if Main/Upgrade differentiation is needed","OSS, MainOnly, UpgradeOnly, 不区分, OSS 策略" +H5,H,"Upgrade.exe must be in update/ subdirectory for OSS","OSS 模式下 Upgrade.exe 必须在 update/ 子目录","OSS strategy scans update/ directory; placing Upgrade.exe elsewhere won't be found","Place Upgrade.exe in update/ subdirectory from the first release","OSSStrategy, UpdatePath","Verify directory structure before first OSS deployment","OSS, update directory, subdirectory, 子目录, OSS 部署" +H6,H,"EventManager concurrency race on add/remove/dispatch","EventManager 并发竞争问题","EventManager uses List<> not concurrent collection; add/remove during dispatch causes race","Use lock or ConcurrentBag for listener storage","EventManager.cs","Avoid modifying listeners while dispatch is in progress","concurrency, EventManager, thread safe, 并发, 线程安全" +H7,H,"Container (IoC) disposed entry can cause ObjectDisposedException","容器释放后访问导致 ObjectDisposedException","AutoFac container holds singleton references; after disposal access to Instance throws","Check container.IsDisposed before accessing Instance; add null check wrapper","GeneralUpdateBootstrap, DI container","Wrap container access in try-catch (ObjectDisposedException)","container, disposed, ObjectDisposedException, IoC, AutoFac" +H8,H,"Cross-version (CVP) jump skips API compatibility checks","跨版本跳转跳过 API 兼容性检查","CVP strategy allows jumping arbitrary number of versions; no intermediate API compatibility validation","Ensure server-side compatibility validation between source and target versions","CrossVersionStrategy, CVP","Test API compatibility for each version pair before deployment","CVP, cross version, API compatibility, 跨版本, API 兼容" +H9,H,"Linux: Pipeline hash algorithm platform-specific differences","Linux 上哈希算法平台差异","HashMiddleware may use platform-specific crypto implementations; hash mismatch between build and deploy","Use cross-platform hash algorithm (SHA256 consistently)","HashMiddleware, HashAlgorithm","Set HashAlgorithmName explicitly to SHA256","Linux, hash, SHA256, 哈希, 平台差异" +H10,H,"SignalR HubConnection dispose-then-reconnect crash","SignalR HubConnection 释放后重连崩溃","HubConnection.Dispose() sets internal state; reconnecting without new instance crashes","Set HubConnection to null after Dispose; create new instance for reconnect","SignalR, HubConnection, Dispose","Always null-check before reconnecting","SignalR, HubConnection, reconnect, 重连, 推送" +H11,H,"Bowl v10.4.6 has no public LaunchAsync method","Bowl v10.4.6 无公开 LaunchAsync 方法","v10.4.6 Bowl class only provides base type definitions; LaunchAsync is not publicly exposed","Use Bowl with v10.5.0+ dev branch; in v10.4.6 only basic type definitions are available","GeneralUpdate.Bowl.Bowl, MonitorParameter","Bowl parameters can be configured but execution requires dev branch","Bowl, LaunchAsync, 崩溃守护, 守护进程" +M1,M,"Differential package referenced unnecessarily","不必要地引用了 Differential 包","Developers add GeneralUpdate.Differential package not knowing types are already in Core","Don't add GeneralUpdate.Differential separately; the types are embedded in GeneralUpdate.Core","GeneralUpdate.Core, GeneralUpdate.Differential","Remove any existing GeneralUpdate.Differential reference from csproj","Differential, NuGet, 差分, 额外引用" +M2,M,"Wrong EventArgs type used in event listeners","事件监听到用了错误的 EventArgs 类型","Using ProgressEventArgs instead of MultiDownloadStatisticsEventArgs or vice versa","Use exact EventArgs type from GeneralUpdate.Common.Download namespace","MultiDownloadStatisticsEventArgs, ProgressEventArgs","Always check the namespace before importing event args types","event args, EventArgs, 事件参数, 错误类型" +M3,M,"Missing AddListenerException leads to silent failures","未订阅 ExceptionEventArgs 导致静默失败","Without AddListenerException, non-critical exceptions are silently swallowed","Always add AddListenerException handler; at minimum log the exception message","AddListenerException, ExceptionEventArgs","Add a default handler that logs to file even in production","silent failure, exception, 静默失败, 未订阅" +M4,M,"Version number not 4-segment (x.y.z.w)","版本号不是 4 段式","Server or client uses 3-segment version (1.0.0); comparison logic expects 4 segments","Always use 4-segment version format: x.y.z.w","ClientVersion, VersionInfo","Pad to 4 segments at server response level","version, 4 segment, 版本号, 三段式" +M5,M,"InstallPath set to relative path causes file resolution failure","InstallPath 使用相对路径导致文件解析失败","Relative InstallPath resolved differently in Client vs Upgrade context","Use absolute path: AppDomain.CurrentDomain.BaseDirectory for production","Configinfo.InstallPath","Use Path.GetFullPath() to resolve at runtime","InstallPath, relative path, 相对路径, 安装路径" +M6,M,"UpdateUrl returns success but empty body causes null reference","UpdateUrl 返回成功但空响应体导致空引用","Server API returns 200 OK with null/empty body; bootstrap doesn't handle null Info.Body","Add null check on e.Info?.Body before iterating VersionInfo list","UpdateInfoEventArgs, VersionInfo","Wrap version list access in null-conditional operator","null reference, empty response, 空响应, 空引用, body null" +M7,M,"Version comparison uses string instead of semantic version","版本比较使用字符串而非语义版本","Version strings compared lexicographically (1.9.0.0 > 1.10.0.0)","Use Version class (System.Version) or a semantic version parser for comparison","ClientVersion, Version.Parse","Cast to System.Version before comparison","version compare, semantic version, 版本比较, 语义版本" +M8,M,"BlackFiles/BlackFormats patterns not applied to upgrade directory","黑名单模式未应用到升级目录","BlackFiles patterns only apply to main app directory; upgrade path files not filtered","Apply same blacklist logic to upgrade directory; or use separate upgrade-specific blacklist","Configinfo.BlackFiles, Configinfo.BlackFormats","Duplicate blacklist entries for both main and upgrade paths","blacklist, BlackFiles, 黑名单, 升级目录" +M9,M,"Update process killed due to watchdog timeout on slow operations","慢操作导致升级进程被监控超时杀死","Upgrade process has a hard timeout; large file operations exceed the limit","Increase timeout for large updates; show progress to watchdog; split large operations","GeneralUpdateBootstrap, Watchdog","Show periodic progress to prevent watchdog timeout","timeout, watchdog, killed, 超时, 被杀" +M10,M,"Single-process mode not supported in v10.4.6","v10.4.6 不支持单进程模式","Developers expect to run update in same process; v10.4.6 requires dual-process architecture","Accept dual-process requirement; document that single-process is not supported","GeneralUpdateBootstrap, AppType","Not supported in v10.4.6; consider v10.5.0+ for single-process","single process, 单进程, dual process, 双进程" +M11,M,"SignalR push update delivery not acknowledged","SignalR 推送更新无送达确认","Push notification sent but no acknowledgment; client may miss the update while offline","Implement client-side acknowledgment; use retry queue on server for unacknowledged pushes","SignalRStrategy, HubConnection","Add ACK from client; server retries unacknowledged pushes","SignalR, acknowledgment, 推送确认, 送达" +M12,M,"Pre-release version included in production update list","预发布版本被包含在生产更新列表中","Server returns pre-release/beta versions in the production version list; client updates to wrong version","Filter versions by release channel; separate pre-release and production version lists","GeneralSpacestation, VersionInfo","Add release channel field to version metadata","pre-release, beta, 预发布, 测试版本" +M13,M,"OssClient.AppType value 3-4 not supported in v10.4.6","v10.4.6 不支持 OssClient.AppType(值 3-4)","v10.4.6 only supports AppType values 1 (Client) and 2 (Upgrade); 3-4 are dev-branch only","Use AppType.ClientApp (1) or AppType.UpgradeApp (2) only","AppType, ClientApp, UpgradeApp","Avoid values 3-4 in v10.4.6","OssClient, AppType, 3-4, not supported, 不支持" +M14,M,"ProgressEventArgs used in v10.4.6 API where MultiDownload* expected","ProgressEventArgs 与 MultiDownload* 混淆","v10.4.6 uses MultiDownload* events for batch downloads; ProgressEventArgs is for single download only","Use MultiDownloadStatisticsEventArgs for batch download; ProgressEventArgs for single file","MultiDownloadStatisticsEventArgs, ProgressEventArgs","Check event type before using in v10.4.6","progress, event, 进度事件, 混淆" +M15,M,"Custom pipeline middleware order wrong causes silent failure","自定义中间件顺序错误导致静默失败","Middleware registered in wrong order (Patch before Compress); no error but update silently broken","Follow correct order: Hash -> Compress -> Patch -> Drivelution","PipelineBuilder, UseMiddleware","Document and verify middleware order","pipeline, middleware order, 管道, 中间件顺序" +M16,M,"ConfigurationProviderFactory.Providers dictionary not thread-safe","ConfigurationProviderFactory.Providers 字典非线程安全","Dictionary used without synchronization; concurrent access in multi-threaded context may corrupt","Use ConcurrentDictionary or external lock for Providers access","ConfigurationProviderFactory","Replace Dictionary with ConcurrentDictionary","thread safety, ConfigurationProviderFactory, 线程安全" +M17,M,"Socket/HttpClient not disposed after download completes","下载完成后 Socket/HttpClient 未释放","HttpClient instances created per request without reuse or disposal","Use singleton HttpClient; or wrap in using block","MultiDownloadExecutor, HttpClient","Configure HttpClient as singleton in DI container","HttpClient, socket leak, 连接泄漏, 未释放" +M18,M,"LaunchAsync() returns Task not Task","LaunchAsync() 返回 Task 而非 Task","Developers expect bool return type for success/failure check; actual return is Bootstrap instance","The returned Bootstrap instance can be inspected; do not expect bool","GeneralUpdateBootstrap.LaunchAsync","Ignore return value; use event listeners for status","LaunchAsync, return type, Task, 返回值" +M19,M,"Silent mode notifications don't respect Do Not Disturb settings","静默模式通知不遵守免打扰设置","Silent update notifications pop up even when system is in DND or presentation mode","Check system DND status before showing notifications; queue notifications for later","SilentStrategy, Notification","Use OS-level DND API before showing notification","silent, notification, DND, 通知, 免打扰" +M20,M,"Service mode (Windows Service) IPC path resolution difference","Windows 服务模式下 IPC 路径解析差异","Windows Services run with different working directory than user apps; IPC file path resolved incorrectly","Use absolute paths for IPC files; ensure service account has write access to IPC directory","IPC, ProcessInfoJsonContext","Always use full absolute path for IPC file","Windows Service, IPC, path, 服务, 路径" +L1,L,"Thread.Sleep in event listener blocks update pipeline","事件监听中使用 Thread.Sleep 阻塞更新管道","Synchronous waits in event handlers block the pipeline execution","Use async event handlers; avoid Thread.Sleep/Task.Wait in any event listener","AddListenerException, AddListenerMultiDownloadStatistics","Replace Sleep with Task.Delay and async pattern","block, sleep, 阻塞, 事件监听" +L2,L,"Hardcoded temporary path in unzip operations","解压操作中硬编码临时路径","CompressMiddleware uses hardcoded temp path; conflicts with system temp policy","Use Path.GetTempPath() or configurable temp directory","CompressMiddleware, ZipMiddleware","Override via environment variable if needed","temp path, hardcoded, 临时路径, 硬编码" +L3,L,"Differential clean/dirty parameter validation missing","差分 clean/dirty 参数缺失验证","DifferentialCore.CleanAsync/Core.DirtyAsync doesn't validate input paths","Validate source/target/patch directories exist before calling","DifferentialCore.CleanAsync, DifferentialCore.DirtyAsync","Add manual directory existence checks","differential, parameter validation, 差分, 参数校验" +L4,L,"ServiceCollection registration not validated before Build","Build 前 ServiceCollection 注册未验证","Multiple registrations of same type; last wins silently","Use TryAdd instead of Add for service registration to detect duplicates","GeneralUpdateBootstrap, ServiceCollection","Replace Add with TryAdd patterns","DI, registration, duplicate, ServiceCollection" +L5,L,"Process memory tracking via private bytes not working-memory","进程内存跟踪使用 private bytes 而非 working set","Private bytes doesn't reflect actual memory pressure; working set is more meaningful for GC pressure","Use WorkingSet64 instead of PrivateMemorySize64 for memory monitoring","ProcessMonitor, MemoryMetrics","Switch to WorkingSet64 for memory threshold monitoring","memory, private bytes, working set, 内存跟踪" +L6,L,"Drivelution middleware exception loses context","Drivelution 中间件异常丢失上下文","Driver installation failure exception is caught without original stack trace or driver name","Wrap exception with driver name and operation context before rethrowing","DrivelutionMiddleware, GeneralDrivelution","Log driver name and operation at entry point","Drivelution, exception, 驱动, 异常" +L7,L,"OSS container/region info hardcoded in example code","OSS 示例代码中硬编码了容器/区域信息","Sample code uses hardcoded OSS endpoint/bucket values; needs env-based configuration","Use environment variables or config file for OSS endpoint and bucket","OSSStrategy, OssSample","Extract OSS config to environment variables","OSS, hardcoded, 硬编码, 示例, endpoint" +L8,L,"SkipDirectorys empty list doesn't use defaults","SkipDirectorys 空列表不使用默认值","Empty SkipDirectorys list skips nothing; default skip patterns not applied","Set SkipDirectorys explicitly even if using default patterns","Configinfo.SkipDirectorys","Always set SkipDirectorys with at least basic patterns","SkipDirectorys, empty list, 跳过目录, 空列表" +L9,L,"Bowl process name may match multiple processes","Bowl 进程名可能匹配多个进程","ProcessNameOrId uses string match; multiple processes with same name monitored instead of one","Use PID instead of process name for Bowl monitoring, or use full path","Bowl, MonitorParameter, ProcessNameOrId","Use exact process name with extension (MyApp.exe) to narrow match","Bowl, process name, multiple, 多个进程" +L10,L,"Linux: missing chmod +x on downloaded UpgradeApp","Linux 上下载的 UpgradeApp 缺少执行权限","File permissions not set after download; no IUpdateHooks in v10.4.6 to fix it","Manually chmod +x after download, or use post-update script","GeneralUpdateBootstrap, LinuxStrategy","Add chmod command to deployment script","Linux, chmod, permission, 权限, 执行" +L11,L,"WinForms: ShowDialog in update event blocks IPC","WinForms ShowDialog 阻塞 IPC 通信","Modal dialog shown during update event blocks the IPC writing thread","Use non-modal forms or async dialog patterns","WinForms, IPC","Replace ShowDialog with Show + event-based close","WinForms, ShowDialog, IPC, 模态框" +L12,L,"VersionRespDTO nullable fields cause null warnings","VersionRespDTO 可空字段导致空警告","Some fields in VersionRespDTO are nullable; no null handling in consuming code","Add null coalescing operators (??) when accessing VersionRespDTO properties","VersionRespDTO, VersionInfo","Use pattern matching before accessing nullable fields","nullable, 可空, null warning, VersionInfo" diff --git a/cli/assets/skills/generalupdate-troubleshoot/data/strategies.csv b/cli/assets/skills/generalupdate-troubleshoot/data/strategies.csv new file mode 100644 index 0000000..8529aaf --- /dev/null +++ b/cli/assets/skills/generalupdate-troubleshoot/data/strategies.csv @@ -0,0 +1,7 @@ +id,name,description,server_required,best_for,pros,cons,keywords +S01,Standard Client-Server,"Standard dual-process update with GeneralSpacestation backend",yes,"First-time users, enterprise apps with backend","Full control over version management; fine-grained Main/Upgrade differentiation; supports all event types","Requires GeneralSpacestation or compatible backend; higher deployment complexity","standard, client-server, GeneralSpacestation, 标准, 客户端-服务端" +S02,OSS Object Storage,"Update via S3/MinIO/cloud object storage; no backend server needed",no,"Small projects, startups, no-backend scenarios, cost-sensitive","Zero server cost; simple deployment; global CDN support; scales automatically","No Main/Upgrade differentiation; Upgrade.exe must be in update/ subdirectory; no real-time version control","OSS, S3, MinIO, 对象存储, 无服务器" +S03,Silent Update,"Background polling update with minimal user interruption",yes,"Long-running apps, kiosk systems, background services","User-unaware updates; configurable poll interval; can pair with any strategy","Requires notification strategy; poll interval tuning; no user feedback loop","silent, background, polling, 静默, 后台, 轮询" +S04,Differential Update,"Delta patch update to save bandwidth (BSDIFF/HDiffPatch)",yes,"Large applications, bandwidth-constrained networks, frequent updates","60-90% bandwidth reduction; quick patch application; works over any transport","Requires differential build pipeline on server; patch size limited (avoid >2GB); BSDIFF integer overflow risk in older versions","differential, delta, patch, BSDIFF, HDiffPatch, 差分, 增量" +S05,Cross-Version CVP,"Skip intermediate versions and jump directly to target version",yes,"Skip multiple versions, forced updates, long-unupdated clients","Bypass intermediate versions; reduce update steps; force minimum version compliance","Requires CVP build pipeline; API compatibility risk for large jumps; full compatibility testing needed","CVP, cross version, skip, 跨版本, 跳版本" +S06,SignalR Push,"Server-initiated push update via SignalR real-time connection",yes,"Real-time apps, urgent updates, managed fleets","Instant update notification; server-controlled rollout; targeted version deployment","Requires persistent connection; offline clients miss pushes; HubConnection lifecycle management; disposal/reconnect complexity","SignalR, push, real-time, 推送, 实时" diff --git a/cli/assets/skills/generalupdate-troubleshoot/reference.md b/cli/assets/skills/generalupdate-troubleshoot/reference.md new file mode 100644 index 0000000..e98e8fd --- /dev/null +++ b/cli/assets/skills/generalupdate-troubleshoot/reference.md @@ -0,0 +1,657 @@ +# GeneralUpdate 故障排查参考手册(完整版) + +> 覆盖 50+ 症状,均来自 GitHub Issues(#308–#517)、Gitee Issues(30个)、 +> 及全面代码审计(17 CRITICAL/HIGH 项 + 14 MEDIUM 项 + 10 INFO 项) +> 排查日期:2026-06-16 + +--- + +## 使用方式 + +按症状查找 → 确认根因 → 应用修复。如果症状不匹配,运行通用诊断流程(见底部)。 + +--- + +## 🔴 一级:致命/阻断性故障 + +### C1. 升级进程没启动 / "FileNotFoundException: upgrade application not found" + +| 来源 | 根因 | 诊断 | +|------|------|------| +| #485, #ID3H5V | UpgradeApp.exe 未随主程序发布 | 检查 `UpdatePath` + `UpdateAppName` | + +**修复**: +1. UpgradeApp.exe **必须**从首个版本就和主程序一起发布 +2. OSS 模式下 Upgrade.exe 必须放在 `update/` 子目录(#485) +3. `generalupdate.manifest.json` 中 `UpdateAppName` 必须包含 `.exe` + +``` +发布目录结构: +/InstallPath + ├── MyApp.exe + ├── generalupdate.manifest.json + └── update/ + └── UpgradeApp.exe ← 必须存在 +``` + +--- + +### C2. "Method not found" — NuGet 版本冲突 + +| 来源 | 根因 | 诊断 | +|------|------|------| +| #I7MCA5 | Client 和 Upgrade 使用不同版本 NuGet | 检查两个项目的 csproj | + +**修复**:Client.csproj 和 Upgrade.csproj 使用完全相同版本: +```xml + +``` + +--- + +### C3. BSOD / 内存溢出 / 进程崩溃 — BSDIFF 整数溢出 + +| 来源 | 代码审计 #3, #4 | +|------|----------------| +| **根因** | `BsdiffDiffer.WriteInt64` 对 `long.MinValue` 求反溢出;control 值 `> int.MaxValue` 转型截断产生负值 | + +**影响**:恶意构造的 patch 文件或超过 2GB 的正常 patch 可导致进程崩溃或 OOM +**修复**:更新到 v10.4.6+(#514 已修复)。如无法更新,在差分引擎中添加 `MaxInputFileSize` 限制 + +--- + +### C4. 备份递归嵌套 → PathTooLongException(路径超长) + +| 来源 | #501 | +|------|------| +| **根因** | `StorageManager.Backup()` 在 `InstallPath` **内部**创建备份目录,且空列表 `new List()` 不触发默认跳过目录逻辑 | + +**修复**: +```csharp +// 使用 Configinfo 的 SkipDirectorys 属性 +var config = new Configinfo +{ + // ... + SkipDirectorys = new List { ".backups", "backup-" } +}; +``` + +--- + +### C5. ZIP 解压路径穿越 — 恶意包可覆盖任意文件 + +| 来源 | 代码审计 #7 | +|------|-----------| +| **根因** | `ZipCompressionStrategy.Decompress` 只做 `Regex.Replace` 清理,未验证 `Path.GetFullPath(combinedPath).StartsWith(Path.GetFullPath(unZipDir))` | + +**影响**:攻击者通过 `../../evil.exe` 条目逃逸到任意目录 +**修复**:更新到 v10.4.6+(已修复)。旧版本手动添加路径校验 + +--- + +### C6. 硬编码 AES 密钥 — IPC 加密形同虚设 + +| 来源 | 代码审计 #1, #2, #14 | +|------|---------------------| +| **根因** | AES 密钥由常量 `SHA256("GeneralUpdate.IPC.EnvironmentProvider.v1")` 派生,IV 16 字节中仅第 1 字节非零 | + +**影响**:任何拿到反编译代码的人可解密 IPC 文件 +**修复**:使用 NamedPipe IPC(见 advanced/templates/NamedPipeIPC.cs);或部署 DPAPI 加密 + +--- + +### C7. 跨租户数据泄漏(服务端) + +| 来源 | 代码审计 #15 | +|------|------------| +| **影响范围** | 11 处服务端漏洞:包/客户端/分组/升级记录/文件/租户隔离均缺失 | + +**修复**:升级到 GeneralSpacestation 最新版;紧急措施:为每个租户部署独立实例 + +| 具体漏洞 | 所在文件 | 影响 | +|---------|---------|------| +| GroupId 过滤条件取反 | `ClientService.cs:36-37` | 分组查询返回错误客户端 | +| UserService 可改 TenantId | `UserService.cs:338-356` | 租户间权限提升 | +| 升级记录无租户 ID | `UpgradeService.cs:242-256` | 租户隔离彻底失效 | +| 全局包可见于所有租户 | `UpgradeService.cs:49-57` | 跨租户数据暴露 | +| 文件删除无租户过滤 | `FileService.cs:98-108` | 任意文件可删 | + +--- + +### C8. PushJob 静默吞异常 — Quartz 不知作业失败 + +| 来源 | 代码审计 #16 | +|------|------------| +| **根因** | `PushJob.Execute` 被 `try-catch(Exception)` 包裹只 `LogError`,Quartz 不触发重试 | + +**影响**:推送任务对运维完全不可见 +**修复**:在 catch 中 rethrow 或移除外层 catch + +--- + +## 🟠 二级:高优先级 / 场景阻断 + +### H1. 静默模式不生效 + +| 来源 | #484, #471, #443, #IJQ0Q5 | +|------|--------------------------| +| **根因**(多重): | | +| | ① `ProcessExit` 事件不保证触发(FailFast/TerminateProcess/Ctrl+C 下不触发) | +| | ② `manifest.json` 默认非空字段阻塞 `AppMetadataDiscoverer.Discover()` | +| | ③ 静默模式下 `PatchMiddleware` 抛出异常(未注入 DiffPipeline) | +| | ④ `manifest.json` 的 MainAppName 默认值 "GeneralUpdate.Core.exe" 在静默启动时阻塞身份发现 | + +**修复**: +```csharp +// ① 应用关闭时显式触发(替代 ProcessExit 依赖) +public void OnAppClosing() +{ + // v10.4.6 稳定版无 SilentOrchestrator + // 应用正常退出即可,GeneralUpdate 内部处理 +} + +// ② manifest 字段确保填写正确的版本号 + +--- + +### H2. 无限升级循环(每次启动都检查到"新版本") + +| 来源 | #475, #467, 代码审计 #20 | +|------|------------------------| +| **根因**(多重): | | +| | ① 场景判断与 DownloadPlan 不一致(服务端说有更新但无包可下载) | +| | ② manifest.json 未 WriteBack 版本号 | +| | ③ Version 为 null/空 → 被转为默认值 "1.0.0.0" → 永远比服务端旧 | + +**修复**: +1. 更新到 v10.4.6+(已修复无限升级循环 + 场景判断) + +--- + +### H3. 循环:Process.Start 启动进程后未检查返回值 + +| 来源 | 代码审计 H2 | +|------|-----------| +| **根因** | 5 个 Strategy 文件中 `Process.Start()` 返回值未检查(null → 静默失败) | + +**修复**:更新到 v10.4.6+(已修复,失败时抛异常) + +--- + +### H4. UpdateReporter 注入不生效 / ReportUrl 未配置抛异常 + +| 来源 | #470 | +|------|------| +| **根因** | `UpdateReporter()` 注册的实现未被消费;`ProcessInfo` 构造函数将 `ReportUrl` 作为必填 | + +**修复**:更新到 v10.4.6+(已修复) + +--- + +### H5. Sync-over-async 死锁 — GetAwaiter().GetResult() + +| 来源 | #451, 代码审计 #6 | +|------|-----------------| +| **根因** | `AppDomain.ProcessExit` 事件处理程序同步调用 `.GetAwaiter().GetResult()`,在 WPF/WinForms 的 SynchronizationContext 上死锁 | + +**影响**:桌面应用使用静默模式时,进程退出可能挂起 +**修复**:更新到 v10.4.6+(已修复,改为 `ConfigureAwait(false)` + Task.Run) + +--- + +### H6. 前钩子 (SafeOnBeforeUpdateAsync) 异常时返回 true(应返回 false) + +| 来源 | 代码审计 H4 | +|------|-----------| +| **根因** | `ClientStrategy.cs:1015-1026` 异常时返回 `true`(放行更新),应返回 `false`(中止) | + +**影响**:Hooks 中的 `OnBeforeUpdateAsync` 即使抛异常也会继续更新 +**修复**:更新到 v10.4.6+ + +--- + +### H7. Scenario = Both 误判 — DownloadPlan 为空但判断为有更新 + +| 来源 | #465, #475 | +|------|-----------| +| **根因** | `HttpDownloadSource.ListAsync()` 只检查 `Body.Count > 0`,未验证 `AppType` 匹配;`DownloadPlanBuilder.Build()` 版本过滤后可能返回空列表 | + +**修复**:更新到 v10.4.6+(已修复场景判断逻辑) + +--- + +### H8. OSS 模式:下载完成但没有更新 + +| 来源 | #485, #487 | +|------|-----------| +| **根因** | ① OSS 不区分 Main/Upgrade 更新(HasMainUpdate 和 HasUpgradeUpdate 总是相同)② SSL 验证不覆盖文件下载 | + +**修复**:更新到 v10.4.6+(已修复场景判断逻辑) + +--- + +### H9. PatchMiddleware 在静默模式必定抛异常 + +| 来源 | #471 | +|------|------| +| **根因** | `SilentPollOrchestrator.CreateStrategy()` 创建裸 `WindowsStrategy`,未注入 `DiffPipeline` | + +**修复**:更新到 v10.4.6+(已修复) + +--- + +### H10. HttpClient 无限超时 + +| 来源 | 代码审计 M2 | +|------|-----------| +| **根因** | `HttpClientProvider.Shared` 设置 `Timeout = InfiniteTimeSpan` | + +**修复**:更新到 v10.4.6+(已设置为 5 分钟安全上网限) + +--- + +### H11. 更新成功后版本号未 WriteBack + +| 来源 | #467, #475 | +|------|-----------| +| **根因** | manifest.json 未更新,下次启动时版本号还是旧的 | + +**修复**:更新到 v10.4.6+(已实现 WriteBack)。旧版本手动处理: +```csharp +/// v10.4.6 稳定版无 IUpdateHooks 接口。 +/// 如需在更新后回写版本号,可在服务端处理。 +``` + +--- + +## 🟡 三级:中等 / 需要关注 + +### M1. 增量更新报错:patch 应用失败 + +| 来源 | #II75WI, #I8T0QX | +|------|-----------------| +| **根因** | ① 旧 patch 临时文件残留 ② 文件已被修改导致 hash 不匹配 | + +**修复**: +```csharp +// 每次更新前手动清理临时目录,避免残留文件 +if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); +``` + +### M2. 同名文件在不同目录时封包出错 + +| 来源 | #II77NS | +|------|---------| +| **根因** | `DefaultCleanMatcher.Match` 未使用相对路径匹配 | + +**修复**:使用自定义 CleanMatcher: +```csharp +new DiffPipelineBuilder() + .UseCleanMatcher(new CustomRelativePathCleanMatcher()) + .Build(); +``` + +### M3. 多级文件夹结构更新后文件位置错乱 + +| 来源 | #I59QRI | +|------|---------| +| **根因** | 子目录文件被错误更新到根目录 | + +**修复**:更新到最新版;确保差分包路径包含完整相对路径 + +### M4. 中文文件名乱码 + +| 来源 | #I502QQ | +|------|---------| +| **根因** | ZIP 解压未指定编码 | + +**修复**:确保构建 ZIP 时使用 UTF-8 编码(编码由内部处理,无需在代码中设置) + +### M5. 版本号出现异常字符 + +| 来源 | #I8TNPE | +|------|---------| +| **根因** | 版本链计算缺陷,中间版本号被污染 | + +**修复**:更新到 v10.4.6+(已修复) + +### M6. 文件被占用 / "file in use" + +| 来源 | #479, #ID3UDN | +|------|-------------| +| **根因** | 进程退出后文件句柄未完全释放 | + +**修复**: +```csharp +// 增加重试次数 +var config = new Configinfo +{ + // ... +}; +// 更新到 v10.4.6+ 内置重试逻辑 +``` + +### M7. Linux 下 Environment.GetEnvironmentVariable("ProcessInfo") 为空 + +| 来源 | #ID4ZF5 | +|------|---------| +| **根因** | Linux 环境变量作用域问题 | + +**修复**:使用最新版(已改用加密文件 IPC)或 NamedPipe IPC + +### M8. Linux / macOS 更新后文件无执行权限 + +| 来源 | #ID5049 | +|------|---------| +| **根因** | 新文件缺少 Unix 可执行权限 | + +**修复**: +```csharp +bootstrap.Hooks(); +``` + +> ⚠️ v10.4.6 稳定版无 IUpdateHooks 接口。Linux 权限问题需手动处理 chmod +x。 + +### M9. IPC 加密文件被防病毒软件隔离 + +| 来源 | 代码审计 #1, #2 | +|------|----------------| +| **根因** | IPC 路径固定为 `%TEMP%/GeneralUpdate/ipc/process_info.enc` | + +**修复**:使用 NamedPipe IPC 替代 + +### M10. 版本比较错误:"1.0" 与 "1.0.0.0" 不等 + +| 来源 | #475, 服务端 #26 | +|------|----------------| +| **根因** | `System.Version` 将 "1.0" 解析为 `1.0.-1.-1`,`< "1.0.0.0"` | + +**修复**:服务端和客户端版本号统一为 4 段式 + +### M11. Assembly.GetExecutingAssembly 获取版本号不正确 + +| 来源 | #I5O4KV | +|------|---------| +| **根因** | 应使用 `Assembly.GetEntryAssembly()`,而非 `GetExecutingAssembly()` | + +**修复**:在 manifest.json 中显式填写 `ClientVersion` + +### M12. SignalR 推送后无反应(ObjectDisposedException) + +| 来源 | #402, 代码审计 #5 | +|------|-----------------| +| **根因** | `UpgradeHubService.DisposeAsync` 不置 null,重连时崩溃 | + +**修复**:使用 `SafeHubConnection` 包装类(见 PushStrategy.cs) + +### M13. Bowl 没有生成 dump 文件 + +| 来源 | #492 | +|------|------| +| **根因** | Bowl IPC 文件每次读取后自动删除,多进程竞争 | + +**修复**:更新到 v10.4.6+(已修复 Bowl IPC 架构);手动下载 procdump + +### M14. 默认备份保留最多 3 个版本 + +| 来源 | 默认行为 | +|------|---------| +| **根因** | `StorageManager.CleanBackup` 只保留最近 3 个备份 | + +**修复**:如需更多保留,在 StorageManager 中配置: +```csharp +// 注意:Option.BackupConfig 为不存在常量,需直接使用 StorageManager.BackupConfig +// GeneralUpdate 默认只保留最近 3 个备份 +``` + +### M15. DefaultCleanMatcher 每次调用创建新 StorageManager 实例(并发不安全) + +| 来源 | 代码审计 #17 | +|------|------------| +| **根因** | 实例级别持有 `_fileCount` 和 `ComparisonResult`,但被并行调用 | + +**修复**:更新到 v10.4.6+ 或在 `DiffPipeline.CleanAsync` 中添加锁 + +### M16. HttpDownloadExecutor 不校验 Content-Length + +| 来源 | 代码审计 #22 | +|------|------------| +| **根因** | `StreamDownloadAsync` 不验证下载字节数 | + +**修复**:更新到 v10.4.6+(已添加校验) + +### M17. OssStrategy.StartAppAsync 返回 Task.CompletedTask + +| 来源 | 代码审计 #30 | +|------|------------| +| **根因** | `appName` 为空时直接返回,调用方无法区分"已启动"和"跳过" | + +**修复**:显式检查空值并抛异常 + +### M18. EventManager 单例 — Dispose 后仍可访问 + +| 来源 | 代码审计 #11 | +|------|------------| +| **根因** | `Lazy` 单例,Dispose 后 `_lazy.Value` 返回已释放实例 | + +**修复**:自行管理生命周期,在 Bootstrap 结束时调用 Clear + +### M19. GeneralTracer.Dispose 清空全局 Trace.Listeners + +| 来源 | 代码审计 #13 | +|------|------------| +| **根因** | `Dispose()` 调用 `Trace.Listeners.Clear()`,影响同一进程其他库的日志输出 | + +**建议**:更新到 v10.4.6+(已改为只移除自己的 Listener) + +### M20. GeneralTracer 日志只按天轮转,永不过期 + +| 来源 | 代码审计 #28 | +|------|------------| +| **根因** | `generalupdate-trace {yyyy-MM-dd}.log` 永不过期 | + +**修复**:手动配置日志保留策略,或定期清理 `Logs/` 目录 + +--- + +## 🔵 四级:低优先 / 代码气味 / 已知行为 + +### L1. DefaultRetryPolicy 用字符串包含判断 HTTP 状态码 + +| 来源 | 代码审计 #10 | +|------|------------| +| **根因** | `s.Contains("500")` 可能误匹配 URL 或响应正文中的 "500" | +| **建议** | 使用 `HttpRequestException.StatusCode` 属性 | + +### L2. OssDownloadSource 不区分 Main/Upgrade + +| 来源 | 代码审计 #27 | +|------|------------| +| **根因** | 将 `HasMainUpdate` 和 `HasUpgradeUpdate` 都设为 `assets.Count > 0` | +| **建议** | OSS 模式接受此行为,或自行实现 IDownloadSource | + +### L3. ProcessContract 构造函数空检查顺序错误 + +| 来源 | 代码审计 #9 | +|------|------------| +| **根因** | 先检查 `Directory.Exists(installPath)`,然后才 `?? throw` | +| **建议** | 小问题,不影响功能 | + +### L4. ConfigurationMapper.MapToUpdateContext 静默接受 null + +| 来源 | 代码审计 #20 | +|------|------------| +| **根因** | `source == null` 返回空的 `UpdateContext` | +| **建议** | 检查配置是否正确加载 | + +### L5. StorageManager 跳过目录使用 string.Contains 匹配 + +| 来源 | 代码审计 #21 | +|------|------------| +| **根因** | `dirName.Contains("backup-")`,目录名 `backup-custom` 因包含 "backup-" 也被跳过 | +| **建议** | 影响小,如需精确控制使用自定义跳过策略 | + +### L6. FileTreeComparer FAT32 时间精度 2 秒漏判 + +| 来源 | 代码审计 #18 | +|------|------------| +| **根因** | FAT32 文件系统时间戳精度 2 秒 | +| **建议** | 对 FAT32 卷添加哈希比对兜底 | + +### L7. DiffPipeline.CopyUnknownFiles 用 Replace 截取相对路径 + +| 来源 | 代码审计 #31 | +|------|------------| +| **根因** | `file.FullName.Replace(targetPath, "")` 当 targetPath 出现在路径中间时出错 | +| **建议** | 使用 `StartsWith + Substring` | + +### L8. StreamingHdiffDiffer 文件超限时截断 + +| 来源 | 代码审计 #32 | +|------|------------| +| **根因** | 超过 `MaxWindowSize` (默认 128MB) 时截断读取前 128MB | +| **建议** | 大文件使用全量更新替代差分 | + +### L9. Bowl StorageHelper.Restore 无条件执行 + +| 来源 | 代码审计 #33 | +|------|------------| +| **根因** | `AutoRestore=true` 时无验证恢复结果 | +| **建议** | 回滚后增加校验 | + +### L10. OssStrategy 版本比较可能抛异常 + +| 来源 | 代码审计 #23 | +|------|------------| +| **根因** | `new Version("")` 抛 ArgumentException | +| **建议** | 使用 `ParseVersion` 安全解析 | + +### L11. 静默模式更新完自动启动应用 + +| 来源 | #IJQ0Q5 | +|------|---------| +| **建议** | v10.4.6 无 SilentAutoRestart 选项 | + +### L12. OSS 模式下传的 ZIP 包编码无法解压 + +| 来源 | #I59Q5W, #I502QQ | +|------|----------------| +| **建议** | 构建 ZIP 时指定 UTF-8,上传前验证解压 | + +--- + +## 📋 通用诊断流程 + +当用户报告的问题未在以上清单中找到时,执行系统性诊断: + +### 步骤 1:版本检查 +``` +□ Client 和 Upgrade 使用相同 NuGet 版本号? +□ 使用最新稳定版(v5.0+ 推荐)? +``` + +### 步骤 2:配置文件检查 +``` +□ generalupdate.manifest.json 是否存在? +□ 格式是否正确(JSON 语法校验)? +□ ClientVersion 已填写(非空字符串)? +□ MainAppName 包含 .exe 扩展名? +□ UpdateAppName 指向存在的文件? +□ InstallPath 路径可访问? +``` + +### 步骤 3:双进程检查 +``` +□ UpgradeApp.exe 存在于发布目录? +□ Client 和 Upgrade 使用相同 AppSecretKey? +□ %TEMP%/GeneralUpdate/ipc/ 目录可写入? +□ 防病毒软件未隔离该目录? +``` + +### 步骤 4:策略配置检查 +``` +标准模式: + □ UpdateUrl 可访问(HTTP 200)? + □ /Upgrade/Verification 接口返回正确格式? + □ AppSecretKey 与服务端一致? + +OSS 模式: + □ versions.json URL 可下载? + □ versions.json 格式正确? + □ 版本号比较正常? + +静默模式: + □ ProcessExit 能触发(非 FailFast 场景)? + □ 应用关闭时显式调用了 TryLaunchUpgrade()? + □ manifest 字段全部正确填写? +``` + +### 步骤 5:日志检查 +``` +□ 查看 generalupdate-trace {yyyy-MM-dd}.log(位于 {BaseDir}/Logs/) +□ EventManager 是否触发了 Exception 事件? +□ AddListenerException 是否收到异常? +``` + +### 步骤 6:平台特定检查 +``` +Windows: + □ 防病毒软件是否拦截 IPC 文件或临时目录? + □ 管理员权限是否必要? + +Linux/macOS: + □ 文件可执行权限是否设置? + □ 环境变量作用域是否正确? + □ Mono 或 .NET 运行时版本兼容? + +AOT: + □ SignalR 使用 JSON 协议 + JsonSerializerContext? + □ 反射调用被 preserve? +``` + +--- + +## 快速诊断命令 + +```bash +# 1. 检查 manifest 文件 +if [ -f generalupdate.manifest.json ]; then cat generalupdate.manifest.json | python3 -m json.tool; fi + +# 2. 检查升级程序是否存在 +ls -la update/UpgradeApp.exe 2>/dev/null || echo "UpgradeApp.exe not found" + +# 3. 检查 IPC 文件(Windows) +ls -la /tmp/GeneralUpdate/ipc/ 2>/dev/null || echo "No IPC directory (expected before first update)" + +# 4. 检查更新日志 +if [ -d Logs ]; then cat Logs/generalupdate-trace*.log 2>/dev/null | tail -100; fi + +# 5. 验证服务端 API +curl -s -X POST https://your-server.com/Upgrade/Verification \ + -H "Content-Type: application/json" \ + -d '{"appKey":"test","appType":1,"clientVersion":"1.0.0.0","productId":"test"}' | head -20 + +--- + +## Issue 索引(快速跳转) + +| 范围 | 内容 | GitHub | Gitee | +|------|------|--------|-------| +| v5 重构 | 策略/配置/Bootstrap 重写 | #308–#361 | — | +| 扩展点修复 | 扩展点注入不消费 | #455, #457, #373 | — | +| 静默修复 | ProcessExit/PatchMiddleware | #471, #484 | #IJQ0Q5 | +| 场景判断 | Both 误判 / 无限循环 | #465, #475 | — | +| 差分问题 | 同名文件/残留 | — | #II77NS, #I8T0QX | +| 中文乱码 | ZIP 编码 | — | #I502QQ | +| 多级文件夹 | 文件位置错乱 | — | #I59QRI | +| Linux 权限 | 可执行权限 | — | #ID5049 | +| Linux IPC | 环境变量为空 | — | #ID4ZF5 | +| NuGet 版本 | Method not found | — | #I7MCA5 | +| 备份嵌套 | PathTooLongException | #501 | — | +| Bowl IPC | 文件读写冲突 | #492 | — | +| 推送 | SignalR 重连崩溃 | #402 | — | +| OSS | 不区分场景、SSL 覆盖 | #485, #487 | — | +| IPC 加密 | 固定密钥、固定路径 | 代码审计 #1, #2 | — | +| BSDIFF | 整数溢出、OOM | 代码审计 #3, #4 | — | +| 路径穿越 | ZIP 解压 | 代码审计 #7 | — | +| 跨租户 | 11 处服务端漏洞 | 代码审计 #15 | — | diff --git a/cli/assets/skills/generalupdate-troubleshoot/scripts/core.py b/cli/assets/skills/generalupdate-troubleshoot/scripts/core.py new file mode 100644 index 0000000..8b7140d --- /dev/null +++ b/cli/assets/skills/generalupdate-troubleshoot/scripts/core.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GeneralUpdate Search Core - BM25 search engine for troubleshooting issues and strategies. +""" +import csv +import re +import os +from pathlib import Path +from math import log +from collections import defaultdict + +# Allow DATA_DIR override via environment variable (for testing) +_default_data_dir = Path(__file__).resolve().parent.parent / "data" +DATA_DIR = Path(os.environ.get("GENERALUPDATE_DATA_DIR", str(_default_data_dir))) +MAX_RESULTS = 3 + +CSV_CONFIG = { + "issue": { + "file": "issues.csv", + "search_cols": ["symptom_en", "symptom_zh", "cause", "keywords"], + "output_cols": ["id", "severity", "symptom_en", "symptom_zh", "cause", "solution", "code_ref", "workaround"], + }, + "strategy": { + "file": "strategies.csv", + "search_cols": ["name", "description", "best_for", "keywords"], + "output_cols": ["id", "name", "description", "server_required", "best_for", "pros", "cons", "keywords"], + }, +} + +class BM25: + """BM25 ranking algorithm for text search""" + def __init__(self, k1=1.5, b=0.75): + self.k1 = k1 + self.b = b + self.corpus = [] + self.doc_lengths = [] + self.avgdl = 0 + self.idf = {} + self.doc_freqs = defaultdict(int) + self.N = 0 + + def tokenize(self, text): + """Tokenize text: split CJK into character unigrams + bigrams, keep English words.""" + text = str(text).lower() + result = [] + + # Split into CJK and non-CJK segments + segments = re.split(r'([一-鿿㐀-䶿]+)', text) + for seg in segments: + if not seg: + continue + if re.match(r'^[一-鿿㐀-䶿]+$', seg): + # CJK: char unigrams + bigrams + chars = list(seg) + # Unigrams + result.extend(chars) + # Bigrams + for i in range(len(chars) - 1): + result.append(chars[i] + chars[i+1]) + else: + # English/alphanumeric: clean punctuation, keep words + cleaned = re.sub(r'[^\w\s]', ' ', seg) + for w in cleaned.split(): + w = w.strip() + if len(w) > 1 and not w.isdigit(): + result.append(w) + + return result + + def fit(self, corpus): + self.corpus = corpus + self.doc_lengths = [len(doc) for doc in corpus] + self.avgdl = sum(self.doc_lengths) / len(corpus) if corpus else 0 + self.N = len(corpus) + + for doc in corpus: + seen = set() + for term in doc: + if term not in seen: + self.doc_freqs[term] += 1 + seen.add(term) + + for term, df in self.doc_freqs.items(): + self.idf[term] = log((self.N - df + 0.5) / (df + 0.5) + 1.0) + + def score(self, query_terms, doc_idx): + doc = self.corpus[doc_idx] + doc_len = self.doc_lengths[doc_idx] + score = 0.0 + for term in query_terms: + if term in self.idf: + tf = doc.count(term) + score += self.idf[term] * (tf * (self.k1 + 1)) / (tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)) + return score + + def search(self, query, top_n=MAX_RESULTS): + query_terms = self.tokenize(query) + if not query_terms or not self.corpus: + return [] + scores = [(i, self.score(query_terms, i)) for i in range(len(self.corpus))] + scores.sort(key=lambda x: x[1], reverse=True) + return [(idx, score) for idx, score in scores[:top_n] if score > 0] + + +def load_csv(filepath): + """Load CSV file and return list of dicts.""" + full_path = DATA_DIR / filepath + if not full_path.exists(): + return [] + with open(full_path, 'r', encoding='utf-8') as f: + return list(csv.DictReader(f)) + + +def search(query, domain="issue", max_results=MAX_RESULTS): + """Search across issues or strategies domain.""" + if domain not in CSV_CONFIG: + return {"error": f"Unknown domain: {domain}. Available: {list(CSV_CONFIG.keys())}"} + + config = CSV_CONFIG[domain] + data = load_csv(config["file"]) + + if not data: + return {"error": f"No data found for domain: {domain}"} + + # Build corpus + corpus = [] + for row in data: + doc_text = " ".join(str(row.get(col, "")) for col in config["search_cols"]) + corpus.append(BM25().tokenize(doc_text)) + + bm25 = BM25() + bm25.fit(corpus) + results = bm25.search(query, max_results) + + output = [] + for idx, score in results: + row = data[idx] + row["_score"] = round(score, 2) + output.append(row) + + return { + "domain": domain, + "query": query, + "file": config["file"], + "count": len(output), + "results": output, + } diff --git a/cli/assets/skills/generalupdate-troubleshoot/scripts/search.py b/cli/assets/skills/generalupdate-troubleshoot/scripts/search.py new file mode 100644 index 0000000..5a790a7 --- /dev/null +++ b/cli/assets/skills/generalupdate-troubleshoot/scripts/search.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GeneralUpdate Search - BM25 search engine for troubleshooting issues and strategies. +Usage: python3 scripts/search.py "" [--domain ] [--max-results 3] + +Domains: issue (default), strategy +""" +import argparse +import sys +import io +from core import CSV_CONFIG, MAX_RESULTS, search + +if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8': + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def format_output(result): + if "error" in result: + return f"Error: {result['error']}" + + output = [] + output.append(f"## GeneralUpdate Search Results") + output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}") + output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n") + + for i, row in enumerate(result['results'], 1): + severity = row.get('severity', '') + sev_icon = {'C': '🔴', 'H': '🟠', 'M': '🟡', 'L': '🔵'}.get(severity, '') + output.append(f"### {sev_icon} Result {i} (Score: {row.get('_score', 'N/A')})") + for key, value in row.items(): + if key.startswith('_'): + continue + value_str = str(value) + if len(value_str) > 500: + value_str = value_str[:500] + "..." + output.append(f"- **{key}:** {value_str}") + output.append("") + + return "\n".join(output) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="GeneralUpdate Search") + parser.add_argument("query", help="Search query") + parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), default="issue", help="Search domain") + parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)") + parser.add_argument("--json", action="store_true", help="Output as JSON") + + args = parser.parse_args() + result = search(args.query, args.domain, args.max_results) + + if args.json: + import json + print(json.dumps(result, ensure_ascii=False, indent=2)) + else: + print(format_output(result)) diff --git a/cli/assets/skills/generalupdate-troubleshoot/scripts/tests/test_search.py b/cli/assets/skills/generalupdate-troubleshoot/scripts/tests/test_search.py new file mode 100644 index 0000000..bebb065 --- /dev/null +++ b/cli/assets/skills/generalupdate-troubleshoot/scripts/tests/test_search.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Unit tests for GeneralUpdate BM25 search engine.""" +import sys +import os + +# Set DATA_DIR before importing core — test runs from scripts/ but data is at ../data +os.environ["GENERALUPDATE_DATA_DIR"] = os.path.normpath( + os.path.join(os.path.dirname(__file__), '..', '..', 'data') +) + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +import json +from core import search, CSV_CONFIG, DATA_DIR + + +def test_csv_files_exist(): + """All configured CSV data files must exist.""" + for domain, config in CSV_CONFIG.items(): + filepath = DATA_DIR / config["file"] + assert filepath.exists(), f"Missing CSV: {filepath}" + print(f" ✓ {config['file']} exists") + + +def test_issues_csv_has_all_severities(): + """Issues CSV must contain entries for all severity levels: C, H, M, L.""" + # Use a broad query that matches many issues + result = search("update error fail crash bug", "issue", 100) + assert "error" not in result, f"Search error: {result.get('error')}" + severities = {row["severity"] for row in result["results"]} + for sev in ["C", "H", "M", "L"]: + assert sev in severities, f"Missing severity level: {sev} (found: {severities})" + print(f" ✓ All severities (C/H/M/L) present: found {len(result['results'])} matching entries") + + +def test_issues_csv_required_columns(): + """Verify required columns exist by reading raw CSV.""" + import csv + filepath = DATA_DIR / CSV_CONFIG["issue"]["file"] + with open(filepath, 'r', encoding='utf-8') as f: + rows = list(csv.DictReader(f)) + required = ["id", "severity", "symptom_en", "symptom_zh", "cause", "solution"] + for row in rows: + for col in required: + assert col in row and row[col], f"Missing column '{col}' in issue {row.get('id')}" + print(f" ✓ All required columns present in {len(rows)} issues") + + +def test_strategies_csv_has_all_6(): + """Must have exactly 6 strategy entries.""" + import csv + filepath = DATA_DIR / CSV_CONFIG["strategy"]["file"] + with open(filepath, 'r', encoding='utf-8') as f: + rows = list(csv.DictReader(f)) + assert len(rows) == 6, f"Expected 6 strategies, got {len(rows)}" + ids = {r["id"] for r in rows} + for i in range(1, 7): + sid = f"S{i:02d}" + assert sid in ids, f"Missing strategy: {sid}" + print(f" ✓ All 6 strategies present") + + +def test_chinese_search_upgrade_not_start(): + """"升级后启动不了" should match C1 (top result).""" + result = search("升级后启动不了", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C1", f"Expected C1, got {top['id']}" + assert top["_score"] > 0, f"Zero score for C1" + print(f" ✓ '升级后启动不了' → C1 (score={top['_score']})") + + +def test_chinese_search_garbled(): + """"中文乱码" should match H1 (top result).""" + result = search("中文乱码", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "H1", f"Expected H1, got {top['id']}" + print(f" ✓ '中文乱码' → H1 (score={top['_score']})") + + +def test_english_search_method_not_found(): + """"method not found" should match C2.""" + result = search("method not found", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C2", f"Expected C2, got {top['id']}" + print(f" ✓ 'method not found' → C2 (score={top['_score']})") + + +def test_english_search_zip_slip(): + """"zip slip path traversal" should match C6.""" + result = search("zip slip path traversal", "issue", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "C6", f"Expected C6, got {top['id']}" + print(f" ✓ 'zip slip path traversal' → C6 (score={top['_score']})") + + +def test_strategy_search_oss(): + """"OSS no backend" should match OSS strategy.""" + result = search("OSS no backend server", "strategy", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "S02", f"Expected S02 (OSS), got {top['id']}" + print(f" ✓ 'OSS no backend' → S02 (score={top['_score']})") + + +def test_strategy_search_signalr(): + """"pus" should match SignalR push strategy.""" + result = search("push real-time connection", "strategy", 3) + assert "error" not in result + assert len(result["results"]) > 0 + top = result["results"][0] + assert top["id"] == "S06", f"Expected S06 (SignalR), got {top['id']}" + print(f" ✓ 'push real-time' → S06 (score={top['_score']})") + + +def test_search_json_output(): + """Search output should have correct JSON structure.""" + result = search("升级后启动不了", "issue", 3) + assert "error" not in result + assert result["domain"] == "issue" + assert result["query"] == "升级后启动不了" + assert result["file"] == "issues.csv" + assert result["count"] >= 1 + assert len(result["results"]) >= 1 + # Each result should have _score + for row in result["results"]: + assert "_score" in row + print(f" ✓ JSON structure correct") + + +def test_search_invalid_domain(): + """Invalid domain should return error.""" + result = search("test", "invalid_domain") + assert "error" in result + print(f" ✓ Invalid domain returns error") + + +def test_search_no_results(): + """Search with gibberish should return 0 results.""" + result = search("zzzzzzzxxxxxxyyyyyyy", "issue", 3) + assert "error" not in result + assert result.get("count", 0) == 0 + print(f" ✓ Gibberish search returns 0 results") + + +def test_bm25_scoring_differentiation(): + """Different queries should produce different top results.""" + r1 = search("garbled encoding chinese", "issue", 1) + r2 = search("zip slip path traversal", "issue", 1) + top1_id = r1["results"][0]["id"] + top2_id = r2["results"][0]["id"] + assert top1_id != top2_id, f"Two different queries returned same top result: {top1_id}" + print(f" ✓ BM25 differentiates queries: {top1_id} vs {top2_id}") + + +def test_all_strategies_searchable(): + """Each of the 6 strategies should be findable by keyword.""" + queries = ["standard client-server", "oss", "silent background", "differential delta", "cross version", "signalr push"] + for i, q in enumerate(queries): + r = search(q, "strategy", 1) + assert r["count"] >= 1, f"Strategy {i+1} not found by query: {q}" + print(f" ✓ All 6 strategies searchable") + + +if __name__ == "__main__": + print(f"\n🧪 GeneralUpdate Search Engine Tests\n") + tests = [fn for fn in dir() if fn.startswith("test_")] + passed = 0 + failed = 0 + for name in tests: + try: + globals()[name]() + print(f" PASS {name}") + passed += 1 + except Exception as e: + print(f" FAIL {name}: {e}") + failed += 1 + print(f"\n{'='*40}") + print(f" Total: {passed + failed} | ✅ {passed} | ❌ {failed}") + if failed: + sys.exit(1) diff --git a/cli/assets/skills/generalupdate-ui/SKILL.md b/cli/assets/skills/generalupdate-ui/SKILL.md new file mode 100644 index 0000000..8edc936 --- /dev/null +++ b/cli/assets/skills/generalupdate-ui/SKILL.md @@ -0,0 +1,234 @@ +--- +name: generalupdate-ui +description: | + Generate a complete update UI window for ANY .NET UI framework — no UI coding required. + Automatically detects WPF (LayUI.Wpf, WPFDevelopers, native), WinForms (AntdUI, native), + Avalonia (SemiUrsa), MAUI, or console apps. Generates fully wired update windows with + REAL GeneralUpdate.Core event bindings. + Triggers on: "update UI", "progress bar", "update window", "show progress", + "update界面", "进度显示", "更新窗口", "好看点", "UI样式", + "how to show update progress", "need a progress UI", "update form", + "beautiful update UI", "professional update appearance". + ALWAYS load this skill when the user asks for auto-update + UI together. + Pairs with generalupdate-init for complete integration. + Pairs with generalupdate-troubleshoot if UI states show wrong values. +when_to_use: | + - User wants a visual update progress interface (any framework) + - User asks about showing download progress, speed, remaining time + - User mentions their UI framework (WPF/WinForms/Avalonia/MAUI) in context of updates + - User wants a "beautiful" or "professional looking" update experience + - User already has basic update integration working and wants a UI for it +allowed-tools: "Read, Write, Edit, Glob, Grep" +--- + +# 🎨 GeneralUpdate 更新界面生成 — 全状态覆盖 + +自动检测开发者的 UI 框架类型,生成带真实 GeneralUpdate.Core 事件绑定的完整更新窗口代码。 + +> ⚠️ 针对 NuGet v10.4.6 稳定版。`RealDownloadService.cs` 为抽象桥接模板,需手动适配。 + +--- + +## 📋 用户需求提取(生成 UI 前必须确认) + +``` +### UI 框架(必需) +- 目标框架: ______(WPF/WinForms/Avalonia/MAUI/控制台/不确定) +- 偏好 UI 库: ______(默认推荐 / LayUI.Wpf / WPFDevelopers / AntdUI / SemiUrsa / 原生) +- 是否已有项目模板: ______(是/否,如果否,从 generalupdate-init 开始) + +### 更新场景(必需) +- 更新窗口角色: ______(Client 端/ Upgrade 端/ 两端都需要) +- 是否需要手动触发更新: ______(是/否,自动启动时检查) +- 是否支持暗黑模式: ______(是/否) + +### 高级 UI 需求(可选) +- 需要自定义品牌色/Logo: ______(是/否) +- 需要多语言支持: ______(是/否) +- 需要无障碍支持: ______(是/否) +``` + +--- + +## 工作流程 + +``` +1. 框架探测 + ├── 扫描 .csproj → PackageReference 识别 UI 库 + ├── 如果无法识别 → 询问用户 + └── 如果无 UI 框架 → 控制台进度条 + +2. 状态代码生成 + ├── IDownloadService 桥接接口 + ├── RealDownloadService 桥接代码(手动适配 GeneralUpdate.Core 事件) + ├── ViewModel(MVVM)或 Code-Behind + └── 窗口/页面 XAML + +3. 集成指导 + ├── 如何引入 GeneralUpdateBootstrap + └── Bootstrap 配置(与 generalupdate-init 配合) +``` + +--- + +## UI 状态机(所有模板覆盖以下状态) + +``` + ┌─────────────┐ + │ Idle │ ← 初始状态 + └──────┬──────┘ + │ 自动/手动触发 + ▼ + ┌─────────────┐ + ┌─────│ Checking │ ← "正在检查更新..." + │ └──────┬──────┘ + │ │ + │ ┌──────┴──────┐ + │ ▼ ▼ + │ ┌────────┐ ┌──────────┐ + │ │ Latest │ │ Found! │ ← 显示版本号/大小 + │ └────────┘ └────┬─────┘ + │ │ 用户点击"开始更新" + │ ▼ + │ ┌──────────────┐ + │ ┌─────│ Downloading │ ← 进度条/速度/剩余时间 + │ │ └──────┬───────┘ + │ │ │ + │ │ ┌──────┴──────┐ + │ │ ▼ ▼ + │ │ ┌────────┐ ┌──────────┐ + │ │ │ Paused │ │ Error │ ← 显示错误 + "重试" + │ │ └───┬────┘ └────┬─────┘ + │ │ │ 继续 │ 重试 + │ │ ▼ ▼ + │ │ ┌──────────────┐ + │ │ │ Downloading │ + │ │ └──────────────┘ + │ │ + │ │ ┌──────────────┐ + │ └────→│ Applying │ ← "正在安装更新..." + │ └──────┬───────┘ + │ │ + │ ┌──────┴──────┐ + │ ▼ ▼ + │ ┌─────────┐ ┌──────────┐ + │ │ Success │ │ Failed │ + │ └────┬────┘ └──────────┘ + │ │ + │ ▼ + │ ┌──────────┐ + │ │ Restart │ ← 重启应用 + │ └──────────┘ + │ + └── 回到 Idle +``` + +--- + +## 工作流程 + +``` +1. 框架探测 + ├── 扫描 .csproj → PackageReference 识别 UI 库 + ├── 如果无法识别 → 询问用户 + └── 如果无 UI 框架 → 控制台进度条 + +2. 状态代码生成 + ├── IDownloadService 桥接接口 + ├── RealDownloadService 桥接代码(手动适配 GeneralUpdate.Core 事件) + ├── ViewModel(MVVM)或 Code-Behind + └── 窗口/页面 XAML + +3. 集成指导 + ├── 如何引入 GeneralUpdateBootstrap + └── Bootstrap 配置(与 generalupdate-init 配合) +``` + +--- + +## 核心桥接:RealDownloadService + +所有 UI 模板共享这个桥接类,将 GeneralUpdate.Core 的事件映射到 `IDownloadService` 接口。 + +### 桥接逻辑(v10.4.6 稳定版) + +```csharp +// GeneralUpdate.Core 事件 → DownloadStatus 状态机映射: + +GeneralUpdateBootstrap.AddListenerMultiDownloadStatistics + → Downloading(更新 ProgressPercentage/Speed/Remaining) + +GeneralUpdateBootstrap.AddListenerMultiDownloadCompleted + → 文件处理中(解压/校验) + +GeneralUpdateBootstrap.AddListenerMultiAllDownloadCompleted + → Applying → Success + +GeneralUpdateBootstrap.AddListenerMultiDownloadError + → DownloadError(自动重试 N 次后) + +GeneralUpdateBootstrap.AddListenerException + → Failed(非致命异常不改变状态) +``` + +--- + +## UI 框架模板清单 + +| 模板文件 | 适用框架 | 包含特性 | +|---------|---------|---------| +| `SemiUrsaClientView.axaml` + `.cs` | Avalonia + SemiUrsa | 全状态机、暗黑切换、动画 | +| `SemiUrsaUpgradeView.axaml` + `.cs` | Avalonia + SemiUrsa (Upgrade) | 等待中 UI | +| `LayUIStyle.xaml` + `.cs` | WPF + LayUI.Wpf | 玻璃效果、进度条 | +| `WPFDevelopersStyle.xaml` + `.cs` | WPF + WPFDevelopers | 圆形进度、呼吸灯动画 | +| `AntdUIStyle.cs` | WinForms + AntdUI | 暗黑主题、波浪进度按钮 | +| `MauiUpdatePage.xaml` + `.cs` | MAUI | 深色模式、AppThemeBinding | +| `DownloadViewModels.cs` | 所有框架共用 | MVVM ViewModel | +| `RealDownloadService.cs` | 所有框架共用 | **核心桥接** | + +--- + +## ✅ 集成验证清单(交付前逐项检查) + +### 事件桥接 +- [ ] 所有 6 个事件都已绑定(UpdateInfo, MultiDownloadStatistics, MultiDownloadCompleted, MultiDownloadError, MultiAllDownloadCompleted, Exception) +- [ ] 桥接代码使用正确的 EventArgs 类型(检查命名空间 `GeneralUpdate.Common.Download`) +- [ ] `IsComplated` 注意拼写(v10.4.6 API 中的实际拼写,不是 `IsCompleted`) + +### 线程安全 +- [ ] UI 更新操作在正确的线程上执行(WPF/Avalonia 用 `Dispatcher`,WinForms 用 `Invoke`,MAUI 用 `MainThread`) +- [ ] `MultiDownloadStatistics` 事件中不执行耗时操作(仅更新 UI) +- [ ] 下载完成后的"正在应用"状态有超时保护(建议 > 30 秒显示进度提示) + +### 状态机覆盖 +- [ ] 所有 11 个状态都已实现(Idle → Checking → Latest/Found → Downloading → Paused → Error → Retrying → Applying → Success/Failed → Restart) +- [ ] 下载错误的自动重试次数有限制(不超过 3 次) +- [ ] 用户可取消更新操作 + +### 框架特定检查 +- [ ] **Avalonia**: ViewModel 实现 `INotifyPropertyChanged`,绑定使用 `{Binding}` +- [ ] **WPF**: 使用 `Dispatcher.Invoke` 更新绑定的属性 +- [ ] **WinForms AntdUI**: 使用 `Control.Invoke` 进行跨线程更新 +- [ ] **MAUI**: 检查 `Platform.CurrentActivity` 在 Android 上的生命周期 + +--- + +## ⚠️ 反模式清单 + +| # | 反模式 | 后果 | 正确做法 | +|---|--------|------|---------| +| 1 | **通用 ViewModel 直接用在不同框架** | 线程模型不兼容导致跨线程异常 | 按框架分别适配 Dispatcher/Invoke/MainThread | +| 2 | **在下载统计事件中做文件 IO 或网络请求** | 阻塞更新流程,UI 卡顿 | 仅更新 UI 绑定的属性 | +| 3 | **进度条绑定一次性更新到 100%** | 用户看不到中间过程,体验差 | 使用 `e.ProgressPercentage` 逐步更新 | +| 4 | **未处理 MultiDownloadError 事件** | 下载失败时用户无反馈,卡在等待状态 | 至少显示错误信息 + 重试按钮 | +| 5 | **未区分 Client 和 Upgrade 的 UI** | Upgrade 端显示不必要的"下载进度" | Upgrade 端只显示"正在安装,请稍候" | +| 6 | **直接使用 RealDownloadService.cs 不做适配** | 事件绑定不生效 | 必须根据项目结构调整 `IDownloadService` 实现 | +| 7 | **Avalonia/WPF 在 ViewModel 构造函数中启动更新** | UI 还未初始化完成,绑定不生效 | 在 Loaded 事件或 View 层触发检查更新 | + +--- + +## 相关技能 + +- `/generalupdate-init` — 如果还未配置 Bootstrap +- `/generalupdate-strategy` — 如果想要 Silent 模式不需要 UI +- `/generalupdate-troubleshoot` — 如果 UI 显示异常 diff --git a/cli/assets/skills/generalupdate-ui/templates/AntdUIStyle.cs b/cli/assets/skills/generalupdate-ui/templates/AntdUIStyle.cs new file mode 100644 index 0000000..e565328 --- /dev/null +++ b/cli/assets/skills/generalupdate-ui/templates/AntdUIStyle.cs @@ -0,0 +1,285 @@ +using AntdUI; +using GeneralUpdate.Core; +using GeneralUpdate.Common.Shared.Object; + +namespace Upgrade; + +/// +/// 【Skill 自动生成】WinForms + AntdUI 更新窗口 +/// +/// 基于 AntdUI 皮肤库,支持: +/// - 暗黑/明亮主题切换 +/// - AntdUI 本地化(中英文自适应) +/// - 波浪进度按钮 +/// - 真实 GeneralUpdate.Core 事件绑定 +/// +/// 使用方式: +/// 1. 安装 NuGet: AntdUI +/// 2. 在 Program.cs 中:Application.Run(new UpdateForm(url, secretKey)) +/// 3. 替换原有的 Mock Main.cs +/// +public partial class UpdateForm : AntdUI.Window +{ + private readonly string _updateUrl; + private readonly string _secretKey; + private CancellationTokenSource? _cts; + + // UI 控件 + private Button btn_download; + private Button btn_cancel; + private Button btn_mode; + private Label lbl_version; + private Label lbl_note; + + public UpdateForm(string updateUrl, string secretKey) + { + _updateUrl = updateUrl; + _secretKey = secretKey; + + InitializeComponent(); + SetTheme(); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + Text = AntdUI.Localization.Get("title", "软件更新"); + _ = CheckForUpdatesAsync(); + } + + private async Task CheckForUpdatesAsync() + { + Spin.Open(this, AntdUI.Localization.Get("checking", "正在检查更新...")); + + try + { + // 使用 GeneralUpdate.Core 进行版本验证 + // 实际集成中,这里调用 GeneralUpdateBootstrap 的版本检查 + // 此处为演示,真实集成时替换为 Bootstrap 调用 + await Task.Delay(500); + lbl_note.Text = AntdUI.Localization.Get("found", "发现新版本"); + lbl_version.Text = ""; // GeneralUpdate 会返回版本号 + } + finally + { + Spin.Close(this); + } + } + + private async void btn_download_Click(object? sender, EventArgs e) + { + if (btn_download.Type == TTypeMini.Success) + { + Close(); + return; + } + + btn_download.Enabled = false; + btn_download.Loading = true; + btn_cancel.Visible = true; + + _cts = new CancellationTokenSource(); + await StartUpdateAsync(_cts.Token); + } + + private async Task StartUpdateAsync(CancellationToken token) + { + try + { + var config = new Configinfo + { + UpdateUrl = _updateUrl, + AppSecretKey = _secretKey, + AppName = "MyApp.exe", + MainAppName = "MyApp.exe", + ClientVersion = "1.0.0.0", + ProductId = "my-product-001", + InstallPath = ".", + }; + + // v10.4.6 稳定版 API + await new GeneralUpdateBootstrap() + .SetConfig(config) + .AddListenerMultiDownloadStatistics((_, e) => + { + // 更新 UI 进度(在 UI 线程上) + Invoke(() => + { + btn_download.LoadingWaveValue = (float)(e.ProgressPercentage / 100.0); + btn_download.Text = $"{e.ProgressPercentage:F1}% " + + AntdUI.Localization.Get("downloading", "下载中"); + // 更新状态标签 + Text = $"{e.Speed} | " + + AntdUI.Localization.Get("remaining", "剩余") + + $" {e.Remaining:mm\\:ss}"; + }); + }) + .AddListenerMultiAllDownloadCompleted((_, e) => + { + Invoke(() => OnUpdateSuccess()); + }) + .AddListenerException((_, e) => + { + Invoke(() => OnUpdateError(e.Message)); + }) + .LaunchAsync(); + } + catch (Exception ex) + { + Invoke(() => OnUpdateError(ex.Message)); + } + } + + private void OnUpdateSuccess() + { + btn_download.Loading = false; + btn_download.LoadingWaveValue = 0; + btn_download.Type = TTypeMini.Success; + btn_download.Text = AntdUI.Localization.Get("completed", "更新完成"); + btn_download.Enabled = true; + btn_cancel.Visible = false; + } + + private void OnUpdateError(string error) + { + btn_download.Loading = false; + btn_download.LoadingWaveValue = 0; + btn_download.Type = TTypeMini.Error; + btn_download.Text = AntdUI.Localization.Get("failed", "更新失败"); + btn_download.Enabled = true; + btn_cancel.Visible = false; + + // 显示错误详情 + var localizedError = AntdUI.Localization.Get("error", "错误"); + MessageBox.Show($"{localizedError}: {error}", + AntdUI.Localization.Get("title", "软件更新"), + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + private void btn_cancel_Click(object? sender, EventArgs e) + { + _cts?.Cancel(); + btn_cancel.Visible = false; + btn_download.Loading = false; + btn_download.Text = AntdUI.Localization.Get("canceled", "已取消"); + btn_download.Enabled = true; + } + + private void btn_mode_Click(object? sender, EventArgs e) + { + AntdUI.Config.IsDark = !AntdUI.Config.IsDark; + SetTheme(); + } + + private void SetTheme() + { + Dark = AntdUI.Config.IsDark; + btn_mode.Toggle = Dark; + if (Dark) + { + BackColor = Color.Black; + ForeColor = Color.White; + } + else + { + BackColor = Color.White; + ForeColor = Color.Black; + } + } + + #region 控件初始化 + + private void InitializeComponent() + { + this.Text = "软件更新"; + this.Size = new Size(460, 360); + this.StartPosition = FormStartPosition.CenterScreen; + this.FormBorderStyle = FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + + var panel = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 5, + Padding = new Padding(32) + }; + + // 标题 + var lblTitle = new Label + { + Text = AntdUI.Localization.Get("update", "软件更新"), + Font = new Font("Microsoft YaHei UI", 20, FontStyle.Bold), + TextAlign = ContentAlignment.MiddleCenter, + Dock = DockStyle.Fill + }; + + // 版本号 + lbl_version = new Label + { + Text = "...", + TextAlign = ContentAlignment.MiddleCenter, + Font = new Font("Microsoft YaHei UI", 13), + ForeColor = Color.Gray, + Dock = DockStyle.Fill + }; + + // 更新说明 + lbl_note = new Label + { + Text = "", + TextAlign = ContentAlignment.MiddleLeft, + Font = new Font("Microsoft YaHei UI", 11), + Dock = DockStyle.Fill, + Padding = new Padding(16, 8, 16, 8) + }; + + // 按钮行 + var btnPanel = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection = FlowDirection.RightToLeft, + WrapContents = false + }; + + btn_download = new Button + { + Text = AntdUI.Localization.Get("download", "开始更新"), + Width = 130, + Height = 40, + Font = new Font("Microsoft YaHei UI", 12) + }; + btn_download.Click += btn_download_Click!; + + btn_cancel = new Button + { + Text = AntdUI.Localization.Get("cancel", "取消"), + Width = 80, + Height = 40, + Visible = false + }; + btn_cancel.Click += btn_cancel_Click!; + + btn_mode = new Button + { + Text = AntdUI.Localization.Get("theme", "主题"), + Width = 70, + Height = 40 + }; + btn_mode.Click += btn_mode_Click!; + + btnPanel.Controls.Add(btn_mode); + btnPanel.Controls.Add(btn_cancel); + btnPanel.Controls.Add(btn_download); + + panel.Controls.Add(lblTitle, 0, 0); + panel.Controls.Add(lbl_version, 0, 1); + panel.Controls.Add(lbl_note, 0, 2); + panel.Controls.Add(btnPanel, 0, 4); + + this.Controls.Add(panel); + } + + #endregion +} diff --git a/cli/assets/skills/generalupdate-ui/templates/DownloadViewModels.cs b/cli/assets/skills/generalupdate-ui/templates/DownloadViewModels.cs new file mode 100644 index 0000000..15aeb49 --- /dev/null +++ b/cli/assets/skills/generalupdate-ui/templates/DownloadViewModels.cs @@ -0,0 +1,208 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Common.Avalonia.Models; + +namespace Client.Avalonia.ViewModels; + +/// +/// 【Skill 自动生成】增强版下载 ViewModel — 覆盖全部 UI 状态 +/// +/// 适用于所有 UI 框架(Avalonia/WPF/WinForms/MAUI)。 +/// 包含完整的 MVVM 命令绑定和状态转换逻辑。 +/// +public partial class EnhancedDownloadViewModel : ObservableObject +{ + private readonly IDownloadService _downloadService; + + // ── 可观察属性 ── + + [ObservableProperty] + private DownloadStatistics _statistics; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(CheckCommand))] + [NotifyCanExecuteChangedFor(nameof(DownloadCommand))] + [NotifyCanExecuteChangedFor(nameof(PauseCommand))] + [NotifyCanExecuteChangedFor(nameof(RetryCommand))] + [NotifyCanExecuteChangedFor(nameof(CancelCommand))] + private DownloadStatus _status; + + [ObservableProperty] + private string _statusText = "准备就绪"; + + [ObservableProperty] + private string _versionText = ""; + + [ObservableProperty] + private string _speedText = ""; + + [ObservableProperty] + private string _errorMessage = ""; + + [ObservableProperty] + private bool _isIndeterminate; + + [ObservableProperty] + private bool _isErrorVisible; + + [ObservableProperty] + private bool _isProgressVisible; + + [ObservableProperty] + private bool _isUpdateFound; + + [ObservableProperty] + private bool _isCompleted; + + private string _lastError = ""; + + public EnhancedDownloadViewModel(IDownloadService downloadService) + { + _downloadService = downloadService; + + _downloadService.StatisticsChanged += OnStatisticsChanged; + _downloadService.StatusChanged += OnStatusChanged; + _downloadService.ErrorOccurred += OnError; + _downloadService.UpdateCompleted += OnCompleted; + + Statistics = _downloadService.CurrentStatistics; + Status = _downloadService.Status; + UpdateVisibility(); + } + + // ═══════════════════════════════════════════════ + // 命令 + // ═══════════════════════════════════════════════ + + private bool CanStart => _downloadService.CanStart; + + [RelayCommand(CanExecute = nameof(CanStart))] + private void Check() => _downloadService.CheckForUpdates(); + + private bool CanDownload => Status == DownloadStatus.FoundUpdate; + [RelayCommand(CanExecute = nameof(CanDownload))] + private void Download() => _downloadService.StartDownload(); + + private bool CanPauseDownload => _downloadService.CanPause; + [RelayCommand(CanExecute = nameof(CanPauseDownload))] + private void Pause() => _downloadService.Pause(); + + private bool CanRetryDownload => _downloadService.CanRetry; + [RelayCommand(CanExecute = nameof(CanRetryDownload))] + private void Retry() => _downloadService.Retry(); + + private bool CanCancelOp => Status is DownloadStatus.Downloading + or DownloadStatus.Paused + or DownloadStatus.DownloadError; + [RelayCommand(CanExecute = nameof(CanCancelOp))] + private void Cancel() => _downloadService.Cancel(); + + // ═══════════════════════════════════════════════ + // 事件处理 + // ═══════════════════════════════════════════════ + + private void OnStatisticsChanged(DownloadStatistics stats) + { + Dispatch(() => + { + Statistics = stats; + + // 版本信息同步 + if (stats.Version != null) + VersionText = $"版本: {stats.Version}"; + + // 速度信息:小于 0.01 MB/s 视为 0 + if (stats.Speed > 0.01) + SpeedText = $"{stats.Speed:F1} MB/s"; + else + SpeedText = ""; + }); + } + + private void OnStatusChanged(DownloadStatus status) + { + Dispatch(() => + { + Status = status; + UpdateVisibility(); + UpdateStatusText(); + + // 非下载状态清空速度 + if (status is not (DownloadStatus.Downloading or DownloadStatus.Applying)) + SpeedText = ""; + }); + } + + private void OnError(string error) + { + Dispatch(() => + { + _lastError = error; + ErrorMessage = error; + IsErrorVisible = true; + }); + } + + private void OnCompleted() + { + Dispatch(() => + { + IsCompleted = true; + StatusText = "更新完成!应用即将重启"; + }); + } + + // ═══════════════════════════════════════════════ + // UI 状态同步 + // ═══════════════════════════════════════════════ + + private void UpdateVisibility() + { + IsProgressVisible = Status is DownloadStatus.Downloading + or DownloadStatus.Applying + or DownloadStatus.UpgradeProgress; + + IsIndeterminate = Status is DownloadStatus.Checking + or DownloadStatus.Applying + or DownloadStatus.UpgradeProgress + or DownloadStatus.RollingBack; + + IsUpdateFound = Status == DownloadStatus.FoundUpdate; + IsErrorVisible = Status is DownloadStatus.DownloadError + or DownloadStatus.Failed; + IsCompleted = Status == DownloadStatus.Success; + } + + private void UpdateStatusText() + { + StatusText = Status switch + { + DownloadStatus.Idle => "准备就绪", + DownloadStatus.Checking => "正在检查更新...", + DownloadStatus.FoundUpdate => "发现新版本!", + DownloadStatus.AlreadyLatest => "已是最新版本 ✓", + DownloadStatus.Downloading => $"正在下载 ({Statistics.ProgressPercentage:F0}%)", + DownloadStatus.Paused => $"已暂停 ({Statistics.ProgressPercentage:F0}%)", + DownloadStatus.DownloadError => "下载出错", + DownloadStatus.Applying => "正在安装更新...", + DownloadStatus.UpgradeProgress => "正在完成更新...", + DownloadStatus.Success => "更新完成!", + DownloadStatus.Failed => $"更新失败: {_lastError}", + DownloadStatus.RollingBack => "正在回滚到上一个版本...", + _ => "" + }; + } + + /// + /// 将操作调度到 UI 线程(框架适配) + /// 在 WPF 中使用 Application.Current.Dispatcher + /// 在 Avalonia 中使用 Avalonia.Threading.Dispatcher.UIThread + /// 在 WinForms 中使用 Control.BeginInvoke + /// 在 MAUI 中使用 MainThread.BeginInvokeOnMainThread + /// + private static void Dispatch(Action action) + { + // 框架自动适配 — 在对应的 UI 项目中替换为正确的 Dispatcher + action(); + } +} diff --git a/cli/assets/skills/generalupdate-ui/templates/LayUIStyle.xaml b/cli/assets/skills/generalupdate-ui/templates/LayUIStyle.xaml new file mode 100644 index 0000000..a452afc --- /dev/null +++ b/cli/assets/skills/generalupdate-ui/templates/LayUIStyle.xaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cli/assets/skills/generalupdate-ui/templates/MauiUpdatePage.xaml b/cli/assets/skills/generalupdate-ui/templates/MauiUpdatePage.xaml new file mode 100644 index 0000000..4b52787 --- /dev/null +++ b/cli/assets/skills/generalupdate-ui/templates/MauiUpdatePage.xaml @@ -0,0 +1,91 @@ + + + + + + + + + + + + +