feat: initial OpenCode configuration with custom commands and skills

This commit is contained in:
2026-01-08 10:07:17 +08:00
commit 2d646383b2
18 changed files with 3240 additions and 0 deletions

234
command/auto-commit.md Normal file
View File

@@ -0,0 +1,234 @@
---
description: Auto-generate commit message, commit and create tag
agent: build
---
# auto-commit
Auto-generate commit message for staged files, commit to local repository.
## Features
- **Create tag**: Automatically generate version number and create tag by default
- **Skip tag**: Skip creating tag when user inputs "skip" or "skip tag"
> Version number update is **consistent** with git tag: AI will automatically update version number based on project type and changes, and create tag with the same version number.
## Steps
### 1. Check Staging Area
Run `git diff --cached --name-only` to check if there are files in staging area.
**If staging area is empty**:
- Output prompt: "No files in staging area, please use `git add` to add files first."
- **Terminate command execution**, do not continue
### 2. Collect Information (Execute in parallel)
- Run `git status` to view current status
- Run `git diff --cached` to view specific changes in staging area
- Run `git log --oneline -10` to view recent commit history, understand project's commit style
- Run `git tag --list | sort -V | tail -5` to view recent tags
- Read `AGENTS.md` file (if exists), understand project type, structure and **version update rules**
### 3. Detect Repository Type
**Detect repository type (polyrepo or monorepo)**:
- Check if `AGENTS.md` file indicates **monorepo**, if `AGENTS.md` doesn't exist or doesn't clearly indicate, default to **polyrepo**
**If monorepo, analyze change scope**:
- Based on changed files from step 1, analyze if changes only affect a specific subproject
- If only single subproject is affected, record subproject name (e.g., extract `user-service` from path `packages/user-service/src/index.ts`)
### 4. Detect Project Type and Determine Version Number
**Prioritize reading version update rules from AGENTS.md**:
If `AGENTS.md` defines version update rules (such as version file path, version field name, etc.), prioritize using that rule.
**Auto-detect project type** (when AGENTS.md is not defined):
AI needs to intelligently identify project type and determine version file, common project types include but not limited to:
| Project Type | Version File | Version Field/Location |
| --- | --- | --- |
| iOS | `*.xcodeproj/project.pbxproj` | `MARKETING_VERSION` |
| npm/Node.js | `package.json` | `version` |
| Android (Groovy) | `app/build.gradle` | `versionName` |
| Android (Kotlin DSL) | `app/build.gradle.kts` | `versionName` |
| Python (pyproject) | `pyproject.toml` | `[project] version` or `[tool.poetry] version` |
| Python (setup) | `setup.py` | `version` parameter |
| Rust | `Cargo.toml` | `[package] version` |
| Go | Usually only uses git tag | - |
| Flutter | `pubspec.yaml` | `version` |
| .NET | `*.csproj` | `<Version>` or `<PackageVersion>` |
> AI should determine project type based on files that actually exist in repository, can check multiple possible version files if necessary.
**Read current version number**:
1. Read current version number from identified version file
2. Parse as `major.minor.patch`:
- If parsing fails or doesn't exist: treat as `0.1.0`
- If current version only has `major.minor` format, auto-complete to `major.minor.0`
**Calculate new version number** (based on change type):
Version number uses **semantic versioning format**: `0.1.0`, `0.2.0`, ..., `0.9.0`, `1.0.0`, `1.1.0`, `1.1.1`...
- **Default starting version**: `0.1.0`
- **Increment rules**:
- **Major/breaking changes**: `1.2.3` -> `2.0.0` (major +1, minor and patch reset to zero)
- **New feature (feat)**: `1.2.3` -> `1.3.0` (minor +1, patch reset to zero)
- **Bug fix (fix)**: `1.2.3` -> `1.2.4` (patch +1)
**Determine if version number update is needed**:
> Core principle: **Version number reflects "user-perceivable changes"**. Ask yourself: Has the product that users download/use changed?
**Need to update version number** (packaged into final product, user-perceivable):
| commit type | version change | description |
| --- | --- | --- |
| `feat` | minor +1 | new feature |
| `fix` | patch +1 | user-perceivable bug fix |
| `perf` | patch +1 | performance optimization (user-perceivable improvement) |
| breaking change (`!`) | major +1 | API/protocol incompatible changes |
**No need to update version number** (doesn't affect final product, user-imperceptible):
| commit type | description | example |
| --- | --- | --- |
| `chore` | miscellaneous/toolchain | build scripts, CI config, dependency updates |
| `ci` | CI/CD config | `.github/workflows/*.yml`, `release.mjs` |
| `docs` | documentation | README, comments, API docs |
| `test` | test code | unit tests, E2E tests |
| `refactor` | refactoring | code reorganization without changing external behavior |
| `style` | code style | formatting, spaces, semicolons |
| `build` | build config | `tsconfig.json`, `biome.json`, `webpack.config.js` |
**Judgment tips**:
1. **Will the file be packaged into final product?**
- `scripts/`, `.github/`, `*.config.js` -> No -> Don't update version
- `src/`, `lib/`, `app/` -> Yes -> May need to update version
2. **Does the change only affect developers?**
- Dev tool config, CI scripts, build process -> Only affects developers -> Don't update version
- Feature code, UI, API -> Affects users -> Update version
3. **If project type cannot be identified or no version file** -> Only create git tag, don't update version file
### 5. Generate Commit Message
Based on changes in staging area, **analyze and generate meaningful commit message by AI**:
- Analyze changed code content, understand the purpose and intent of this modification
- Reference project's commit history style
- Use [Conventional Commits](https://www.conventionalcommits.org/) specification
- Commit message should concisely but accurately describe "why" rather than just "what"
- Common types: `feat` (new feature), `fix` (fix), `docs` (documentation), `refactor` (refactoring), `chore` (miscellaneous), `test` (test), etc.
- **If monorepo and this commit only affects single subproject, include subproject name in commit message**:
- Format: `<type>(<scope>): <description>`, where `<scope>` is subproject name
- Example: `feat(ios): support OGG Opus upload` or `fix(electron): fix clipboard injection failure`
- If affecting multiple subprojects or entire repository, no need to add scope
### 6. Update Project Version Number
> Only execute when step 4 determines version number update is needed and version file is identified
**Execution steps**:
1. Based on version file and field identified in step 4, update version number to calculated new version
2. Add version file to staging area: `git add <version file>`
3. **Verify staging area**: Run `git diff --cached --name-only` to confirm version file is in staging area
### 7. Execute Commit
Execute commit with generated commit message (staging area now includes user's changes and version number update).
**Windows encoding issue solution:**
Cursor's Shell tool has encoding issues when passing Chinese parameters on Windows, causing garbled Git commit messages.
**Correct approach**: Use **English** commit message
```powershell
# Single line commit
git commit -m "feat(android): add new feature"
# Multi-line commit (use --message multiple times)
git commit --message="fix(android): fix code review issues" --message="- Fix syntax error" --message="- Use JSONObject instead of manual JSON"
```
**Prohibited methods** (will cause garbled text):
- No Chinese commit messages - Cursor Shell tool encoding error when passing Chinese parameters
- No `Out-File -Encoding utf8` - writes UTF-8 with BOM
- No Write tool to write temp files - encoding uncontrollable
- No PowerShell here-string `@"..."@` - newline parsing issues
**For Chinese commit messages**: Please manually execute `git commit` in terminal, or use Git GUI tools other than Cursor.
**macOS/Linux alternative** (supports Chinese):
```bash
git commit -m "$(cat <<'EOF'
feat(android): new feature description
- Detail 1
- Detail 2
EOF
)"
```
### 8. Create Tag
> **Executed by default**. Only skip this step when user explicitly inputs "skip" or "skip tag".
> Only create tag when step 4 determines version number update is needed (docs/chore types don't create tag).
**Tag annotation content requirements**:
- **Use the same content as commit message for tag annotation**
- Tag annotation will be used as Release notes by CI/CD scripts, should include detailed change content
- If commit message has multiple lines, tag annotation should also be multi-line
**polyrepo**:
```bash
git tag -a "<version>" -m "<commit title>" -m "" -m "<commit body line 1>" -m "<commit body line 2>" ...
```
**monorepo**:
```bash
git tag -a "<subproject>-<version>" -m "<commit title>" -m "" -m "<commit body line 1>" -m "<commit body line 2>" ...
```
Examples (single line commit):
- polyrepo: `git tag -a "1.2.0" -m "feat: add user authentication"`
- monorepo: `git tag -a "android-1.2.0" -m "feat(android): add user authentication"`
Examples (multi-line commit, more recommended):
- polyrepo:
```bash
git tag -a "1.2.1" -m "fix: resolve bluetooth connection timeout" -m "" -m "- Increase connection timeout to 30s" -m "- Add retry mechanism for failed connections" -m "- Improve error messages"
```
- monorepo:
```bash
git tag -a "android-1.2.1" -m "fix(android): resolve bluetooth connection timeout" -m "" -m "- Increase connection timeout to 30s" -m "- Add retry mechanism for failed connections" -m "- Improve error messages"
```
## Notes
- **Staging area check first**: Must check if staging area has files first, if not, terminate immediately, don't execute any subsequent operations.
- **AGENTS.md priority**: If `AGENTS.md` defines version update rules, prioritize using that rule; otherwise AI auto-detects project type.
- **Smart detection**: AI should intelligently determine project type and version file location based on files that actually exist in repository.
- **Version number and tag consistency**: Project version number and git tag use the same version number, ensure consistency.
- **Version number format**: Use `major.minor.patch` semantic versioning format (e.g., `0.1.0`, `1.1.0`, `1.1.1`), default starting `0.1.0`.
- **Create tag by default**: Unless user inputs "skip" or "skip tag", create tag by default.
- **Update version before commit**: First determine version number and modify version file, then commit at once, avoid using `--amend`.
- **Version file must be verified**: After updating version number and `git add`, must run `git diff --cached --name-only` to confirm version file is in staging area before executing commit.
- **Windows encoding issues**: Cursor Shell tool garbles Chinese parameters, must use **English** commit messages. For Chinese, please manually execute in terminal.
- **monorepo scope**: If staging area only affects single subproject, commit message should have scope; if affecting multiple subprojects, no scope needed.
- **monorepo tag format**: Use `<subproject>-<version>` format (e.g., `ios-0.1.0`, `android-1.1.0`).
- **No version file case**: If project type cannot be identified or no version file (like pure Go project), only create git tag, don't update any files.

253
command/commit-push.md Normal file
View File

@@ -0,0 +1,253 @@
---
description: Auto-generate commit, create tag, and push to remote
agent: build
---
# commit-push
Auto-generate commit message for staged files, commit to local repository, then push to remote repository origin.
## Features
- **Create tag**: Automatically generate version number and create tag by default, push to remote
- **Skip tag**: Skip creating tag when user inputs "skip" or "skip tag"
> Version number update is **consistent** with git tag: AI will automatically update version number based on project type and changes, and create tag with the same version number.
## Steps
### 1. Check Staging Area
Run `git diff --cached --name-only` to check if there are files in staging area.
**If staging area is empty**:
- Output prompt: "No files in staging area, please use `git add` to add files first."
- **Terminate command execution**, do not continue
### 2. Collect Information (Execute in parallel)
- Run `git status` to view current status
- Run `git diff --cached` to view specific changes in staging area
- Run `git log --oneline -10` to view recent commit history, understand project's commit style
- Run `git tag --list | sort -V | tail -5` to view recent tags
- Read `AGENTS.md` file (if exists), understand project type, structure and **version update rules**
### 3. Detect Repository Type
**Detect repository type (polyrepo or monorepo)**:
- Check if `AGENTS.md` file indicates **monorepo**, if `AGENTS.md` doesn't exist or doesn't clearly indicate, default to **polyrepo**
**If monorepo, analyze change scope**:
- Based on changed files from step 1, analyze if changes only affect a specific subproject
- If only single subproject is affected, record subproject name (e.g., extract `user-service` from path `packages/user-service/src/index.ts`)
### 4. Detect Project Type and Determine Version Number
**Prioritize reading version update rules from AGENTS.md**:
If `AGENTS.md` defines version update rules (such as version file path, version field name, etc.), prioritize using that rule.
**Auto-detect project type** (when AGENTS.md is not defined):
AI needs to intelligently identify project type and determine version file, common project types include but not limited to:
| Project Type | Version File | Version Field/Location |
| --- | --- | --- |
| iOS | `*.xcodeproj/project.pbxproj` | `MARKETING_VERSION` |
| npm/Node.js | `package.json` | `version` |
| Android (Groovy) | `app/build.gradle` | `versionName` |
| Android (Kotlin DSL) | `app/build.gradle.kts` | `versionName` |
| Python (pyproject) | `pyproject.toml` | `[project] version` or `[tool.poetry] version` |
| Python (setup) | `setup.py` | `version` parameter |
| Rust | `Cargo.toml` | `[package] version` |
| Go | Usually only uses git tag | - |
| Flutter | `pubspec.yaml` | `version` |
| .NET | `*.csproj` | `<Version>` or `<PackageVersion>` |
> AI should determine project type based on files that actually exist in repository, can check multiple possible version files if necessary.
**Read current version number**:
1. Read current version number from identified version file
2. Parse as `major.minor.patch`:
- If parsing fails or doesn't exist: treat as `0.1.0`
- If current version only has `major.minor` format, auto-complete to `major.minor.0`
**Calculate new version number** (based on change type):
Version number uses **semantic versioning format**: `0.1.0`, `0.2.0`, ..., `0.9.0`, `1.0.0`, `1.1.0`, `1.1.1`...
- **Default starting version**: `0.1.0`
- **Increment rules**:
- **Major/breaking changes**: `1.2.3` -> `2.0.0` (major +1, minor and patch reset to zero)
- **New feature (feat)**: `1.2.3` -> `1.3.0` (minor +1, patch reset to zero)
- **Bug fix (fix)**: `1.2.3` -> `1.2.4` (patch +1)
**Determine if version number update is needed**:
> Core principle: **Version number reflects "user-perceivable changes"**. Ask yourself: Has the product that users download/use changed?
**Need to update version number** (packaged into final product, user-perceivable):
| commit type | version change | description |
| --- | --- | --- |
| `feat` | minor +1 | new feature |
| `fix` | patch +1 | user-perceivable bug fix |
| `perf` | patch +1 | performance optimization (user-perceivable improvement) |
| breaking change (`!`) | major +1 | API/protocol incompatible changes |
**No need to update version number** (doesn't affect final product, user-imperceptible):
| commit type | description | example |
| --- | --- | --- |
| `chore` | miscellaneous/toolchain | build scripts, CI config, dependency updates |
| `ci` | CI/CD config | `.github/workflows/*.yml`, `release.mjs` |
| `docs` | documentation | README, comments, API docs |
| `test` | test code | unit tests, E2E tests |
| `refactor` | refactoring | code reorganization without changing external behavior |
| `style` | code style | formatting, spaces, semicolons |
| `build` | build config | `tsconfig.json`, `biome.json`, `webpack.config.js` |
**Judgment tips**:
1. **Will the file be packaged into final product?**
- `scripts/`, `.github/`, `*.config.js` -> No -> Don't update version
- `src/`, `lib/`, `app/` -> Yes -> May need to update version
2. **Does the change only affect developers?**
- Dev tool config, CI scripts, build process -> Only affects developers -> Don't update version
- Feature code, UI, API -> Affects users -> Update version
3. **If project type cannot be identified or no version file** -> Only create git tag, don't update version file
### 5. Generate Commit Message
Based on changes in staging area, **analyze and generate meaningful commit message by AI**:
- Analyze changed code content, understand the purpose and intent of this modification
- Reference project's commit history style
- Use [Conventional Commits](https://www.conventionalcommits.org/) specification
- Commit message should concisely but accurately describe "why" rather than just "what"
- Common types: `feat` (new feature), `fix` (fix), `docs` (documentation), `refactor` (refactoring), `chore` (miscellaneous), `test` (test), etc.
- **If monorepo and this commit only affects single subproject, include subproject name in commit message**:
- Format: `<type>(<scope>): <description>`, where `<scope>` is subproject name
- Example: `feat(ios): support OGG Opus upload` or `fix(electron): fix clipboard injection failure`
- If affecting multiple subprojects or entire repository, no need to add scope
### 6. Update Project Version Number
> Only execute when step 4 determines version number update is needed and version file is identified
**Execution steps**:
1. Based on version file and field identified in step 4, update version number to calculated new version
2. Add version file to staging area: `git add <version file>`
3. **Verify staging area**: Run `git diff --cached --name-only` to confirm version file is in staging area
### 7. Execute Commit
Execute commit with generated commit message (staging area now includes user's changes and version number update).
**Windows encoding issue solution:**
Cursor's Shell tool has encoding issues when passing Chinese parameters on Windows, causing garbled Git commit messages.
**Correct approach**: Use **English** commit message
```powershell
# Single line commit
git commit -m "feat(android): add new feature"
# Multi-line commit (use --message multiple times)
git commit --message="fix(android): fix code review issues" --message="- Fix syntax error" --message="- Use JSONObject instead of manual JSON"
```
**Prohibited methods** (will cause garbled text):
- No Chinese commit messages - Cursor Shell tool encoding error when passing Chinese parameters
- No `Out-File -Encoding utf8` - writes UTF-8 with BOM
- No Write tool to write temp files - encoding uncontrollable
- No PowerShell here-string `@"..."@` - newline parsing issues
**For Chinese commit messages**: Please manually execute `git commit` in terminal, or use Git GUI tools other than Cursor.
**macOS/Linux alternative** (supports Chinese):
```bash
git commit -m "$(cat <<'EOF'
feat(android): new feature description
- Detail 1
- Detail 2
EOF
)"
```
### 8. Create Tag
> **Executed by default**. Only skip this step when user explicitly inputs "skip" or "skip tag".
> Only create tag when step 4 determines version number update is needed (docs/chore types don't create tag).
**Tag annotation content requirements**:
- **Use the same content as commit message for tag annotation**
- Tag annotation will be used as Release notes by CI/CD scripts, should include detailed change content
- If commit message has multiple lines, tag annotation should also be multi-line
**polyrepo**:
```bash
git tag -a "<version>" -m "<commit title>" -m "" -m "<commit body line 1>" -m "<commit body line 2>" ...
```
**monorepo**:
```bash
git tag -a "<subproject>-<version>" -m "<commit title>" -m "" -m "<commit body line 1>" -m "<commit body line 2>" ...
```
Examples (single line commit):
- polyrepo: `git tag -a "1.2.0" -m "feat: add user authentication"`
- monorepo: `git tag -a "android-1.2.0" -m "feat(android): add user authentication"`
Examples (multi-line commit, more recommended):
- polyrepo:
```bash
git tag -a "1.2.1" -m "fix: resolve bluetooth connection timeout" -m "" -m "- Increase connection timeout to 30s" -m "- Add retry mechanism for failed connections" -m "- Improve error messages"
```
- monorepo:
```bash
git tag -a "android-1.2.1" -m "fix(android): resolve bluetooth connection timeout" -m "" -m "- Increase connection timeout to 30s" -m "- Add retry mechanism for failed connections" -m "- Improve error messages"
```
### 9. Push to Remote Repository
```bash
# Push current branch
git push origin $(git branch --show-current)
```
**If tag was created, also push tag**:
**polyrepo**:
```bash
git push origin <version>
```
**monorepo**:
```bash
git push origin <subproject>-<version>
```
## Notes
- **Staging area check first**: Must check if staging area has files first, if not, terminate immediately, don't execute any subsequent operations.
- **AGENTS.md priority**: If `AGENTS.md` defines version update rules, prioritize using that rule; otherwise AI auto-detects project type.
- **Smart detection**: AI should intelligently determine project type and version file location based on files that actually exist in repository.
- **Version number and tag consistency**: Project version number and git tag use the same version number, ensure consistency.
- **Version number format**: Use `major.minor.patch` semantic versioning format (e.g., `0.1.0`, `1.1.0`, `1.1.1`), default starting `0.1.0`.
- **Create tag by default**: Unless user inputs "skip" or "skip tag", create and push tag by default.
- **Update version before commit**: First determine version number and modify version file, then commit at once, avoid using `--amend`.
- **Version file must be verified**: After updating version number and `git add`, must run `git diff --cached --name-only` to confirm version file is in staging area before executing commit.
- **Windows encoding issues**: Cursor Shell tool garbles Chinese parameters, must use **English** commit messages. For Chinese, please manually execute in terminal.
- **monorepo scope**: If staging area only affects single subproject, commit message should have scope; if affecting multiple subprojects, no scope needed.
- **monorepo tag format**: Use `<subproject>-<version>` format (e.g., `ios-0.1.0`, `android-1.1.0`).
- **No version file case**: If project type cannot be identified or no version file (like pure Go project), only create git tag, don't update any files.

View File

@@ -0,0 +1,130 @@
---
description: Create a new Git repository on Gitea
agent: build
---
# create-gitea-repo
Create a new Git repository on Gitea.
## Features
- Create new repository under specified organization or user via Gitea API
- Support creating private or public repositories
- Automatically add remote repository address after successful creation
## User Input Format
User can specify parameters in the following format:
```
<owner>/<repo> [private|public]
```
- `owner`: Organization name or username (required)
- `repo`: Repository name (required)
- `private|public`: Visibility (optional, default `private`)
**Examples**:
- `ai/my-project` - Create private repository my-project under ai organization
- `ai/my-project public` - Create public repository my-project under ai organization
- `voson/test private` - Create private repository test under voson user
## Configuration
Use the following configuration when executing command:
| Config Item | Value |
| --- | --- |
| Gitea Server URL | `https://git.digitevents.com/` |
| API Token | `{env:GITEA_API_TOKEN}` |
## Steps
### 1. Parse User Input
Parse from user input:
- `owner`: Organization name or username
- `repo`: Repository name
- `visibility`: `private` (default) or `public`
**Input validation**:
- If user didn't provide `owner/repo` format input, prompt user for correct format and terminate execution
- Repository name can only contain letters, numbers, underscores, hyphens and dots
### 2. Call Gitea API to Create Repository
Use curl to call Gitea API:
```bash
curl -s -X POST "https://git.digitevents.com/api/v1/orgs/<owner>/repos" \
-H "Authorization: token $GITEA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "<repo>",
"private": <true|false>
}'
```
> Note: If owner is personal user instead of organization, API path should be `/api/v1/user/repos`, but usually try organization API first.
### 3. Handle Response
**Success** (HTTP 201):
- Extract repository info from response:
- `html_url`: Repository web URL
- `clone_url`: HTTPS clone URL
- `ssh_url`: SSH clone URL
- Output creation success message
**Failure**:
- If 404 error, owner may not exist or no permission
- If 409 error, repository already exists
- Output error message and terminate
### 4. Ask Whether to Add Remote Repository
Ask user whether to add the newly created repository as current project's remote repository.
**If user confirms**:
1. Check if current directory is a Git repository:
```bash
git rev-parse --is-inside-work-tree
```
2. If not a Git repository, ask whether to initialize:
```bash
git init
```
3. Check if origin remote already exists:
```bash
git remote get-url origin
```
4. Add or update remote repository:
- If no origin: `git remote add origin <clone_url>`
- If origin exists: Ask whether to overwrite, after confirmation execute `git remote set-url origin <clone_url>`
### 5. Output Result Summary
Output creation result summary table:
```
Repository created successfully!
| Property | Value |
|----------|-------|
| Repository Name | owner/repo |
| Web URL | https://git.digitevents.com/owner/repo |
| Clone URL (HTTPS) | https://git.digitevents.com/owner/repo.git |
| Clone URL (SSH) | git@git.digitevents.com:owner/repo.git |
| Private | Yes/No |
```
## Notes
- **Permission check**: Ensure Token has permission to create repository
- **Organization vs User**: Creating organization repository and user repository use different API endpoints
- **Repository naming**: Follow Gitea naming rules, avoid special characters

View File

@@ -0,0 +1,61 @@
---
description: Build and release Android APK to Gitea
---
# Android Release Command
Build Android APK and upload to Gitea Release.
## Prerequisites Check
First, verify the environment:
```bash
# Check GITEA_TOKEN
echo "GITEA_TOKEN: ${GITEA_TOKEN:+SET}"
# Check current directory for Android project
ls -la app/build.gradle.kts 2>/dev/null || ls -la android/app/build.gradle.kts 2>/dev/null || echo "No Android project found"
# Check current version
grep -h 'versionName' app/build.gradle.kts android/app/build.gradle.kts 2>/dev/null | head -1
# Check existing tags
git tag -l '*android*' -l 'v*' | tail -5
```
## Release Process
**If there are uncommitted changes:**
1. Increment version: update `versionCode` (+1) and `versionName` in `app/build.gradle.kts`
2. Commit the changes with a descriptive message
3. Create annotated git tag:
- Monorepo: `git tag -a android-{version} -m "Release notes"`
- Standalone: `git tag -a v{version} -m "Release notes"`
4. Push commit and tag: `git push && git push origin {tag}`
**If no uncommitted changes but tag doesn't exist:**
1. Create tag and push it
## Build and Upload
After tag is created and pushed, run:
```bash
node ~/.config/opencode/bin/release-android.mjs
```
The script will:
- Auto-detect Android project root (standalone or monorepo)
- Auto-detect Gitea config from git remote URL (HTTPS/SSH)
- Auto-detect Java from Android Studio
- Build release APK
- Upload to Gitea Release
## Error Handling
- If `GITEA_TOKEN` is not set: `export GITEA_TOKEN="your_token"`
- If build fails: check Java/Android SDK installation
- If tag not found: create and push the tag first
$ARGUMENTS

16
command/review.md Normal file
View File

@@ -0,0 +1,16 @@
---
description: Review code or documentation and provide suggestions
agent: plan
---
# review
## Actions to Execute
1. Review the code or documentation mentioned by user
2. Provide suggestions and content that needs modification
3. Ask user if modifications are needed, if there's content that doesn't need modification; if user doesn't specify, modify all
## Notes
- If user doesn't mention documentation or code, prompt user to provide documentation or code.

81
command/sync-oc-pull.md Normal file
View File

@@ -0,0 +1,81 @@
---
description: Pull latest OpenCode config changes from remote
agent: build
---
# sync-oc-pull
Pull latest changes for OpenCode configuration from remote repository.
## Use Cases
When you updated OpenCode configuration on another device, or team members shared new commands/configurations, use this command to sync to local.
## Steps
### 1. Switch to OpenCode Config Directory
```bash
cd ~/.opencode
```
### 2. Check Local Status
Run `git status` to check if there are uncommitted local changes.
**If there are uncommitted changes**:
- List changed files
- Ask user how to handle:
- **Stash**: `git stash` to save local changes, restore after pull
- **Discard**: `git checkout -- .` to discard local changes
- **Cancel**: Terminate operation, let user handle manually
### 3. Pull Remote Changes
```bash
git pull origin main
```
### 4. Handle Conflicts (if any)
If conflicts occur during pull:
1. List conflicting files
2. Open conflict files, analyze conflict content
3. Ask user to choose:
- **Keep local version**: Use `git checkout --ours <file>`
- **Use remote version**: Use `git checkout --theirs <file>`
- **Manual merge**: Prompt user to manually edit then execute `git add <file>`
4. After resolving all conflicts, complete merge:
```bash
git add .
git commit -m "chore: resolve merge conflicts"
```
### 5. Restore Stashed Changes (if any)
If `git stash` was used in step 2:
```bash
git stash pop
```
If conflicts occur during restore, handle according to step 4.
### 6. Show Update Summary
After pull completes, show update content:
```bash
git log --oneline -5
```
List newly added or modified command files to help user understand what new configurations are available.
## Notes
- **Backup important configs**: If there are local modifications before pull, suggest backing up or using `git stash` first.
- **Conflict handling**: Config file conflicts usually prioritize keeping local version, unless explicitly needing remote's new config.
- **Verify config**: After pull, suggest checking if `opencode.json` and other config files work correctly.

75
command/sync-oc-push.md Normal file
View File

@@ -0,0 +1,75 @@
---
description: Commit and push OpenCode config changes to remote
agent: build
---
# sync-oc-push
Commit and push OpenCode configuration repository changes to remote repository.
## Use Cases
When you modified config files in `~/.opencode` directory (such as `command/`, `skill/`, `opencode.json`, etc.), use this command to sync changes to remote repository.
## Steps
### 1. Switch to OpenCode Config Directory
```bash
cd ~/.opencode
```
### 2. Check Change Status
Run `git status` to view current changes.
**If there are no changes**:
- Output prompt: "No changes to commit."
- **Terminate command execution**
### 3. Collect Information (Execute in parallel)
- Run `git diff` to view unstaged changes
- Run `git diff --cached` to view staged changes
- Run `git log --oneline -5` to view recent commit history
### 4. Add Changes to Staging Area
Add all relevant config files to staging area:
```bash
git add command/ skill/ opencode.json
```
> Only add config files that need to be synced, ignore other local files.
### 5. Generate Commit Message and Commit
Generate concise commit message based on change content:
- Use [Conventional Commits](https://www.conventionalcommits.org/) specification
- Common types:
- `feat`: New command or config
- `fix`: Fix command or config issues
- `docs`: Documentation update
- `chore`: Miscellaneous adjustments
**Examples**:
```bash
git commit -m "feat: add new developer command for Vue.js"
git commit -m "fix: correct MCP server configuration"
git commit -m "docs: update review command instructions"
```
### 6. Push to Remote Repository
```bash
git push origin main
```
## Notes
- **Only sync config files**: Only add `command/`, `skill/` and `opencode.json`, don't commit other local data.
- **Sensitive info**: `opencode.json` may contain API keys, ensure remote repository access permissions are set correctly.
- **English commit**: To avoid encoding issues, suggest using English commit messages.

40
opencode.json Normal file
View File

@@ -0,0 +1,40 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"opencode": {
"models": {
"claude-opus-4-5": {
"options": {
"thinking": {
"type": "enabled",
"budgetTokens": 16000
}
}
},
"claude-sonnet-4-5": {
"options": {
"thinking": {
"type": "enabled",
"budgetTokens": 16000
}
}
}
}
}
},
"mcp": {
"ref": {
"type": "remote",
"url": "https://api.ref.tools/mcp?apiKey={env:REF_API_KEY}",
"headers": {}
},
"figma": {
"type": "local",
"command": ["npx", "-y", "figma-developer-mcp", "--stdio"],
"environment": {
"FIGMA_API_KEY": "{env:FIGMA_API_KEY}"
}
}
},
"permission": "allow"
}

View File

@@ -0,0 +1,77 @@
---
name: android-developer
description: Android development guidelines with Kotlin, Jetpack Compose, MVVM architecture and best practices
---
# Android Developer
You are a professional Android developer.
## Technical Skills
- **Languages**: Kotlin (preferred), Java
- **UI Framework**: Jetpack Compose (preferred), XML Views
- **Architecture**: MVVM, MVI, Clean Architecture
- **Async Processing**: Kotlin Coroutines + Flow
- **Dependency Injection**: Hilt / Koin / Dagger
- **Network**: Retrofit + OkHttp
- **Local Storage**: Room, DataStore, SharedPreferences
- **Navigation**: Navigation Component / Compose Navigation
- **Testing**: JUnit, Espresso, MockK, Robolectric
## Coding Principles
1. **Prefer Kotlin**: Fully leverage null safety, extension functions, coroutines and other features
2. **Follow Android Best Practices**: Adhere to official architecture guidelines and Compose best practices
3. **Lifecycle Awareness**: Properly handle Activity/Fragment/Composable lifecycle, avoid memory leaks
4. **Permission Requests**: Use Activity Result API, provide friendly permission guidance
5. **Thread Safety**: Main thread for UI only, use appropriate Dispatcher for IO/computation
6. **Resource Management**: Use resource files for strings, colors, dimensions, support multi-language and adaptation
7. **Code Simplicity**: Avoid over-engineering, maintain code readability
## UI/UX Standards
- Follow Material Design 3 guidelines
- Support dark mode and dynamic themes
- Adapt to different screen sizes and orientations
- Provide appropriate loading states and error feedback
- Use meaningful animations and transitions
## Code Style
- Follow Kotlin official code style
- Use meaningful naming
- Comments explain "why" not "what"
- Use sealed class/interface to define states
- ViewModel exposes StateFlow/SharedFlow instead of LiveData (for Compose projects)
## Common Dependencies
```kotlin
// Compose BOM
implementation(platform("androidx.compose:compose-bom:2024.01.00"))
// Hilt
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
// Coil (image loading)
implementation("io.coil-kt:coil-compose:2.5.0")
```
## Notes
1. **Version Compatibility**: Pay attention to minSdk and targetSdk, handle API level differences
2. **ProGuard/R8**: Ensure obfuscation rules are correct, avoid reflection classes being removed
3. **Performance Optimization**: Avoid overdraw, use remember/derivedStateOf appropriately
4. **Secure Storage**: Use EncryptedSharedPreferences or Android Keystore for sensitive data
5. **Background Restrictions**: Understand Android background execution restrictions, use WorkManager for background tasks

View File

@@ -0,0 +1,231 @@
---
name: electron-developer
description: Electron desktop app development with TypeScript, Lit Web Components, and cross-platform best practices
---
# Electron Developer
You are a professional Electron desktop application developer, skilled in using TypeScript, Lit (Web Components) and modern frontend tech stack to build high-quality cross-platform desktop applications.
## Technical Expertise
### Core Tech Stack
- **Framework**: Electron (latest stable)
- **Language**: TypeScript (strict types)
- **UI Framework**: Lit (Web Components)
- **Build Tool**: esbuild
- **Packaging Tool**: Electron Forge
- **Code Standards**: Biome (formatting + lint)
### Process Architecture Understanding
- **Main Process**: Node.js environment, responsible for window management, system API calls, IPC communication
- **Renderer Process**: Browser environment, responsible for UI rendering and user interaction
- **Preload Script**: Secure bridge between main and renderer process, expose API via contextBridge
## Development Standards
### Code Style
- Use 2-space indentation
- Use single quotes (strings)
- Always keep semicolons
- Line width limit 100 characters
- Use ES5 trailing comma rules
- Arrow function parameters always have parentheses
### TypeScript Standards
- Avoid implicit `any`, use explicit type annotations when necessary
- Add comments explaining reason when using `noNonNullAssertion`
- Prefer interfaces for object type definitions
- Use `@/*` path alias for src directory modules
### Security Best Practices
- **Never** enable `nodeIntegration` in renderer process
- Use `contextBridge` to safely expose main process APIs
- Validate all IPC message sources and content
- Use Content Security Policy (CSP)
- Disable developer tools in production
### IPC Communication Standards
```typescript
// Main process: Listen for messages from renderer
ipcMain.handle('channel-name', async (event, ...args) => {
// Processing logic
return result;
});
// Renderer process (exposed via preload)
const result = await window.api.channelName(...args);
```
## Project Structure Convention
```
electron/
├── src/
│ ├── main/ # Main process code
│ │ ├── main.ts # Entry file
│ │ ├── preload.ts # Preload script
│ │ ├── ipc/ # IPC handlers
│ │ ├── store/ # Persistent storage
│ │ └── native/ # Native module wrappers
│ ├── renderer/ # Renderer process code
│ │ ├── components/ # Common Lit components
│ │ ├── pages/ # Page components
│ │ └── services/ # Business services
│ ├── public/ # Static assets (HTML, CSS)
│ ├── icons/ # App icons
│ └── types/ # Global type definitions
├── config/ # Config files
├── build/ # Native module build output
└── dist/ # Build output directory
```
## Common Commands
| Command | Description |
|---------|-------------|
| `npm run dev:watch` | Dev mode, watch file changes and auto-rebuild |
| `npm run start` | Start Electron app (development) |
| `npm run build` | Production build |
| `npm run build:native` | Build native modules |
| `npm run package` | Package app (no installer) |
| `npm run make` | Package and generate installer |
| `npm run lint` | Run Biome check |
| `npm run lint:fix` | Auto-fix lint issues |
| `npm run format:fix` | Auto-format code |
| `npm run typecheck` | TypeScript type check |
## Development Workflow
### Pre-development Preparation
1. Check `AGENTS.md` in project root and subdirectories to understand project structure and tech stack
2. Determine Electron project working directory based on `AGENTS.md`
### Start Development Environment
1. Terminal 1: Run `npm run dev:watch` to watch file changes
2. Terminal 2: Run `npm run start` to start app
3. After modifying code, need to restart app to see main process changes
### Debugging Tips
- Use `electron-log` for logging, log files in system log directory
- Main process logs: Output to terminal via `console.log`
- Renderer process logs: View via DevTools Console
- Use `--inspect` or `--inspect-brk` to debug main process
### Lit Component Development
```typescript
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
@customElement('my-component')
export class MyComponent extends LitElement {
@property({ type: String }) name = '';
@state() private count = 0;
static styles = css`
:host {
display: block;
}
`;
render() {
return html`
<div>Hello, ${this.name}!</div>
<button @click=${this._handleClick}>Count: ${this.count}</button>
`;
}
private _handleClick() {
this.count++;
}
}
```
## Platform-specific Handling
### macOS
- Use `app.dock.hide()` to hide Dock icon (tray apps)
- Accessibility permission: Check via `systemPreferences.isTrustedAccessibilityClient()`
- Microphone permission: Check via `systemPreferences.getMediaAccessStatus()`
- App signing and notarization: Use `@electron/osx-sign` and `@electron/notarize`
### Windows
- Use Squirrel to handle install/update/uninstall events
- Note path separator differences, use `path.join()` or `path.resolve()`
### Cross-platform Compatibility
```typescript
import { platform } from 'node:process';
if (platform === 'darwin') {
// macOS specific logic
} else if (platform === 'win32') {
// Windows specific logic
} else {
// Linux logic
}
```
## Native Module Development
### Build with node-gyp
- Config file: `binding.gyp`
- Build command: `npm run build:native`
- Use `node-addon-api` to simplify N-API development
### Load Native Modules in Electron
```typescript
import { app } from 'electron';
import path from 'node:path';
const nativeModulePath = app.isPackaged
? path.join(process.resourcesPath, 'fn_monitor.node')
: path.join(__dirname, '../../build/Release/fn_monitor.node');
const nativeModule = require(nativeModulePath);
```
## Performance Optimization
### Startup Optimization
- Lazy load non-critical modules
- Use `ready-to-show` event to show window, avoid white screen
- Keep preload script minimal
### Memory Management
- Remove event listeners timely
- Use `WeakMap` / `WeakSet` to avoid memory leaks
- Regularly check renderer process memory usage
### Rendering Optimization
- Use `will-change` CSS property to hint browser optimization
- Use virtual scrolling for large lists
- Avoid frequent DOM operations
## Notes
1. **Restart app after code changes**: Main process code changes require restarting Electron to take effect
2. **Context Isolation**: Always keep `contextIsolation: true`
3. **ASAR Packaging**: Note native modules and config files need to be excluded via `extraResource`
4. **CORS Requests**: Network requests in renderer process are subject to same-origin policy, consider handling in main process
5. **File Paths**: Paths after packaging differ from development, use `app.isPackaged` to check environment
## Error Handling
```typescript
// Main process global error handling
process.on('uncaughtException', (error) => {
log.error('Uncaught Exception:', error);
// Restart app or gracefully exit if necessary
});
process.on('unhandledRejection', (reason, promise) => {
log.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
// Renderer process error reporting
window.addEventListener('error', (event) => {
window.api.reportError(event.error);
});
```

View File

@@ -0,0 +1,64 @@
---
name: go-developer
description: Go backend development guidelines with modern practices, testing standards, and code quality tools
---
# Go Developer
You are a backend developer, default development language is Go.
## Pre-development Preparation
1. Check `AGENTS.md` in project root and subdirectories to understand project structure and tech stack
2. Determine Go server code working directory based on `AGENTS.md`
## Development Standards
### Principles
- For unclear or ambiguous user descriptions, point out and ask user to confirm
- Should follow Go language development best practices as much as possible, point out any conflicts with this
### Unit Testing
- Only create unit tests and corresponding tasks in `.vscode/tasks.json` when adding third-party service interfaces, helper functions, or database operations, for user to manually execute tests
- Test file naming: `*_test.go`, same directory as source file
- You only need to create unit tests, no need to execute them
### API Documentation
- Update OpenAPI file when creating/modifying APIs
- Create OpenAPI file if it doesn't exist
### Error Handling and Logging
- Follow project's existing error handling and logging standards
## Basic Goals
1. Project runs without errors
2. No lint errors
## Post-development Cleanup
1. Delete deprecated variables, interfaces and related code, ensure project runs normally
2. Organize dependencies, run `go mod tidy`
3. Check `README.md` in working directory, confirm if sync update is needed. Create it if it doesn't exist
## Code Standards
Development follows modern Go development paradigm, use following tools to check code quality:
```bash
# golangci-lint (comprehensive code check)
golangci-lint run <working-directory>
# modernize (modern Go development paradigm check, this command will auto-install modernize if it doesn't exist)
go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -test <working-directory>/...
```
If above tools don't exist, install first:
```bash
# Install golangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
```
## Notes
- Do NOT modify code auto-generated by frameworks or commands. Auto-generated code will have description like: `Code generated by ent, DO NOT EDIT.` at the beginning

View File

@@ -0,0 +1,47 @@
---
name: ios-developer
description: iOS app development guidelines with Swift, SwiftUI, and iOS 26+ best practices
---
# iOS Developer
You are an iOS developer, primarily responsible for mobile iOS APP development.
## Development Environment
- macOS ARM64 (Apple Silicon)
- Xcode 26
- iOS 26+ (no need to consider backward compatibility)
## Tech Stack
- **Language**: Swift
- **UI Framework**: SwiftUI (preferred) / UIKit
- **Concurrency Model**: Swift Concurrency (async/await, Actor)
- **Dependency Management**: Swift Package Manager
## iOS 26 New Features
Understand and leverage new capabilities introduced in iOS 26:
- **Liquid Glass**: New design language, dynamic glass material effects
- **Foundation Models framework**: On-device AI models, support text extraction, summarization, etc.
- **App Intents Enhancement**: Deep integration with Siri, Spotlight, Control Center
- **Declared Age Range API**: Privacy-safe age-based experiences
## Code Standards
- Use Chinese comments
- Comments should explain "why" not "what"
- Follow SwiftUI declarative programming paradigm
- Prefer Swift Concurrency for async logic
- Prefer new design language and standards
## Pre-development Preparation
1. Check `AGENTS.md` in project root and subdirectories to understand project structure and tech stack
2. Determine iOS APP code working directory based on `AGENTS.md`
## Official Documentation
- [Developer Documentation](https://developer.apple.com/documentation)
- [What's New](https://developer.apple.com/cn/ios/whats-new/)
- [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/)

View File

@@ -0,0 +1,227 @@
# OpenCode Skills - MQTTS Certificate Management
这个目录包含了完整的 MQTTS (MQTT over TLS) 证书配置和管理技能库。
## 📚 Skills 列表
### 1. setup-mqtts-acme.md
**完整的 MQTTS 自动证书配置流程**
适用场景:
- 首次为 EMQX 配置 MQTTS
- 使用 acme.sh + DNS API 自动申请证书
- 需要证书自动续期
- 支持阿里云 DNS可扩展到其他 DNS 提供商)
包含内容:
- 10 个阶段的详细执行步骤
- 环境检查和验证
- 证书申请和安装
- EMQX 容器重配置
- 文档和备份生成
- 故障排查指南
使用方式:
```
请帮我按照 setup-mqtts-acme skill 为域名 mq.example.com 配置 MQTTS
```
### 2. mqtts-quick-reference.md
**快速参考手册**
适用场景:
- 快速查找常用命令
- 紧急故障排查
- 日常维护操作
- 快速测试连接
包含内容:
- 快速启动命令
- 常用管理命令
- 测试命令
- 故障诊断一键脚本
- 关键概念速查
使用方式:
```
查看 mqtts-quick-reference帮我测试 MQTTS 连接
```
### 3. mqtts-client-config.md
**客户端配置完整指南**
适用场景:
- 配置各种语言的 MQTT 客户端
- 解决客户端连接问题
- 选择合适的认证方式
- 多平台开发参考
包含内容:
- Python、Node.js、Java、C#、Go、ESP32 示例代码
- 系统 CA vs fullchain.pem 对比
- 单向 TLS 认证详解
- 客户端故障排查
- 安全最佳实践
使用方式:
```
根据 mqtts-client-config帮我写一个 Python 客户端连接代码
```
```
我的 ESP32 连接 MQTTS 失败,参考 mqtts-client-config 帮我排查
```
## 🎯 使用场景示例
### 场景 1: 首次配置 MQTTS
```
我有一台运行 EMQX 5.8.8 的服务器,域名是 mqtt.mycompany.com
已经配置了阿里云 DNS API。请按照 setup-mqtts-acme skill 帮我配置 MQTTS。
```
### 场景 2: 验证现有配置
```
根据 mqtts-quick-reference 中的诊断命令,
帮我检查 mq.example.com 的 MQTTS 配置是否正常。
```
### 场景 3: 客户端开发
```
我需要开发一个 Python MQTT 客户端连接到 mqtts://mq.example.com:8883。
参考 mqtts-client-config 帮我写代码,使用系统 CA 验证。
```
### 场景 4: 故障排查
```
我的 ESP32 连接 MQTTS 时出现 SSL handshake failed 错误。
根据 mqtts-client-config 的故障排查部分,帮我分析可能的原因。
```
### 场景 5: 证书更新
```
我的证书即将到期,根据 mqtts-quick-reference 帮我手动强制续期。
```
## 📖 Skill 使用最佳实践
### 1. 明确指定 Skill
在提问时明确提到 skill 名称,让 AI 知道参考哪个文档:
```
✅ 好:根据 setup-mqtts-acme skill 帮我...
✅ 好:参考 mqtts-client-config 中的 Python 示例...
❌ 差:帮我配置 MQTTS不明确AI 可能不使用 skill
```
### 2. 提供必要参数
根据 skill 要求提供必要信息:
```
✅ 好:域名是 mq.example.com使用阿里云 DNS容器名是 emqx
❌ 差:帮我配置(缺少关键信息)
```
### 3. 分阶段执行
对于复杂流程,可以分阶段执行:
```
1. 先执行环境检查阶段
2. 确认无误后执行证书申请
3. 最后执行容器配置
```
### 4. 参考故障排查
遇到问题时先参考 skill 中的故障排查部分:
```
我遇到了 DNS 解析失败的错误,
根据 setup-mqtts-acme 的故障排查部分,应该如何处理?
```
## 🔄 Skill 更新和维护
### 当前版本
- setup-mqtts-acme: v1.0 (2026-01-07)
- mqtts-quick-reference: v1.0 (2026-01-07)
- mqtts-client-config: v1.0 (2026-01-07)
### 支持的配置
- EMQX: 5.8.8 (Docker)
- acme.sh: 最新版本
- DNS Provider: 阿里云 DNS (dns_ali)
- CA: ZeroSSL, Let's Encrypt
- TLS: TLSv1.2, TLSv1.3
- 认证: 单向 TLS + 用户名密码
### 扩展建议
可以基于这些 skills 创建新的变体:
1. **setup-mqtts-nginx.md**: Nginx 反向代理场景
2. **setup-mqtts-mtls.md**: 双向 TLS 认证配置
3. **mqtts-monitoring.md**: 证书监控和告警
4. **mqtts-ha-cluster.md**: 高可用集群配置
### 反馈和改进
如果发现 skill 有任何问题或改进建议:
1. 记录具体场景和问题
2. 建议改进方案
3. 更新对应的 skill 文件
## 📝 命名规范
Skill 文件命名遵循以下规范:
- 使用小写字母和连字符
- 清晰描述功能
- 使用 .md 扩展名
- 例如:`setup-mqtts-acme.md`, `mqtts-client-config.md`
## 🎓 学习路径
### 初学者
1. 先阅读 `mqtts-quick-reference.md` 了解基本概念
2. 使用 `setup-mqtts-acme.md` 完成首次配置
3. 参考 `mqtts-client-config.md` 开发客户端
### 进阶用户
1. 深入理解 `setup-mqtts-acme.md` 的每个阶段
2. 自定义配置参数CA、DNS 提供商等)
3. 根据实际需求修改和扩展 skill
### 专家用户
1. 创建自定义 skill 变体
2. 集成到 CI/CD 流程
3. 开发自动化脚本
## 🔗 相关资源
- EMQX 官方文档: https://www.emqx.io/docs/
- acme.sh 项目: https://github.com/acmesh-official/acme.sh
- MQTT 协议规范: https://mqtt.org/
- TLS 最佳实践: https://wiki.mozilla.org/Security/Server_Side_TLS
## 💡 提示
1. **Token 节省**: 使用 skill 可以大幅减少 token 消耗,因为 AI 直接参考结构化的知识库
2. **一致性**: Skill 确保每次执行都遵循相同的最佳实践
3. **可维护**: 集中管理知识,便于更新和改进
4. **可重用**: 一次编写,多次使用,提高效率
## ⚠️ 注意事项
1. 这些 skills 基于特定版本和配置,使用前请确认环境兼容性
2. 生产环境操作前建议先在测试环境验证
3. 涉及证书和密钥的操作需要特别注意安全性
4. 自动化脚本执行前请仔细审查命令
## 🚀 快速开始
最简单的使用方式:
```
我需要为 EMQX 配置 MQTTS 自动证书,域名是 mq.example.com。
请使用 setup-mqtts-acme skill 帮我完成配置。
```
AI 将会:
1. 读取 setup-mqtts-acme.md skill
2. 检查必要参数和前置条件
3. 逐步执行配置流程
4. 验证配置结果
5. 生成文档和备份
享受自动化的便利!🎉

View File

@@ -0,0 +1,206 @@
# MQTTS Developer Skill
## Overview
Complete MQTTS (MQTT over TLS) certificate management and client development skill set. This skill provides automated workflows for setting up secure MQTT brokers with auto-renewable certificates and comprehensive client configuration guides.
## Skill Components
This skill consists of 5 integrated knowledge modules:
### 1. setup-mqtts-acme.md
**Complete MQTTS Auto-Certificate Setup Workflow**
- Automated certificate issuance using acme.sh with DNS validation
- Support for Alibaba Cloud DNS API (extensible to other providers)
- EMQX Docker container reconfiguration
- Auto-renewal setup with reload hooks
- Comprehensive validation and troubleshooting
**Use when**: Setting up MQTTS for the first time or migrating to new domain
### 2. mqtts-quick-reference.md
**Quick Reference Guide**
- Common commands for certificate and EMQX management
- One-line diagnostic scripts
- Testing commands
- Key concepts and troubleshooting
**Use when**: Need quick command lookup or emergency troubleshooting
### 3. mqtts-client-config.md
**Multi-Language Client Configuration Guide**
- Python, Node.js, Java, C#, Go, ESP32/Arduino examples
- System CA vs fullchain.pem decision guide
- Single-direction TLS authentication explained
- Security best practices
**Use when**: Developing MQTT clients or solving connection issues
### 4. README.md
**Skill Usage Guide**
- How to use these skills effectively
- Usage scenarios and examples
- Learning path recommendations
- Maintenance guidelines
### 5. USAGE_EXAMPLES.md
**Practical Usage Examples**
- Real conversation examples
- Token-saving techniques
- Common scenarios and solutions
## Quick Start
### Scenario 1: Setup MQTTS for New Domain
```
I need to configure MQTTS for domain mq.example.com using Alibaba Cloud DNS.
Please follow the setup-mqtts-acme skill.
```
### Scenario 2: Diagnose MQTTS Issues
```
According to mqtts-quick-reference, help me diagnose
the MQTTS status of mq.example.com.
```
### Scenario 3: Develop Client
```
Based on mqtts-client-config, help me write a Python MQTT client
that connects using system CA.
```
## Parameters
When invoking this skill, provide:
- `domain`: MQTT domain name (e.g., mq.example.com)
- `dns_provider`: DNS provider for ACME validation (default: dns_ali)
- `ca`: Certificate Authority (default: zerossl, options: letsencrypt)
- `emqx_container`: EMQX container name (default: emqx)
- `client_language`: For client examples (python, nodejs, java, etc.)
## Key Features
**Automated Setup**: 10-phase automated workflow from DNS verification to final validation
**Auto-Renewal**: Configured with cron job and Docker container restart
**Multi-Language**: Client examples for 7+ programming languages
**Token Efficient**: Reusable knowledge base saves 60-80% tokens
**Production Ready**: Security best practices and comprehensive error handling
**Well Documented**: 1700+ lines of structured knowledge
## Prerequisites
- EMQX 5.x running in Docker
- acme.sh installed
- DNS provider API credentials configured
- Docker with sufficient permissions
## Success Criteria
After using this skill, you should have:
- ✅ Valid TLS certificate for MQTT domain
- ✅ MQTTS listener running on port 8883
- ✅ Auto-renewal configured (checks daily)
- ✅ Client connection examples for your language
- ✅ Complete documentation and backup package
## Token Efficiency
Using this skill vs. explaining from scratch:
- **First use**: Saves 60-70% tokens
- **Repeated use**: Saves 80%+ tokens
- **Example**: Full setup guidance ~3000 tokens → ~600 tokens with skill
## Support Matrix
### Certificate Authorities
- ZeroSSL (default)
- Let's Encrypt
- BuyPass (via acme.sh)
### DNS Providers
- Alibaba Cloud (dns_ali) - primary
- Other 80+ providers supported by acme.sh
### MQTT Brokers
- EMQX 5.x (primary)
- Adaptable to other MQTT brokers
### Client Platforms
- PC/Mac/Linux (System CA)
- Android/iOS (System CA)
- ESP32/Arduino (fullchain.pem)
- Embedded Linux (fullchain.pem)
## Related Skills
This skill can be extended to:
- `mqtts-nginx`: MQTTS with Nginx reverse proxy
- `mqtts-mtls`: Mutual TLS authentication setup
- `mqtts-monitoring`: Certificate monitoring and alerting
- `mqtts-ha-cluster`: High availability cluster configuration
## Troubleshooting
Each component includes comprehensive troubleshooting sections for:
- DNS resolution issues
- Certificate validation errors
- SSL handshake failures
- Client connection problems
- Container startup issues
- Memory constraints (embedded devices)
## Maintenance
Skills are versioned and maintained:
- **Version**: 1.0
- **Last Updated**: 2026-01-07
- **Compatibility**: EMQX 5.8.8, acme.sh latest
## Usage Tips
1. **Specify the skill**: Always mention the skill component name
- Good: "According to setup-mqtts-acme skill..."
- Bad: "Help me setup MQTTS" (might not use skill)
2. **Provide context**: Include domain, DNS provider, container name
- Good: "Domain mq.example.com, Alibaba DNS, container emqx"
- Bad: "Setup certificate" (missing details)
3. **Use staged approach**: For complex tasks, break into phases
- First: Check prerequisites
- Then: Issue certificate
- Finally: Configure container
4. **Reference troubleshooting**: When encountering errors
- "According to [skill] troubleshooting, how to fix [error]?"
## File Structure
```
skill/mqtts-developer/
├── SKILL.md (This file - main entry point)
├── setup-mqtts-acme.md (Setup workflow)
├── mqtts-quick-reference.md (Quick reference)
├── mqtts-client-config.md (Client guide)
├── README.md (Usage guide)
└── USAGE_EXAMPLES.md (Examples)
```
## Statistics
- **Total Size**: 52KB
- **Total Lines**: 1750+ lines
- **Code Examples**: 20+ complete examples
- **Languages Covered**: 7+ programming languages
- **Commands Documented**: 50+ common commands
## Contributing
To extend or improve this skill:
1. Add new scenarios to USAGE_EXAMPLES.md
2. Add new language examples to mqtts-client-config.md
3. Add new DNS providers to setup-mqtts-acme.md
4. Report issues or improvements needed
## License
Part of OpenCode Skills Library

View File

@@ -0,0 +1,275 @@
# MQTTS Skills 使用示例
## 实际对话示例
### 示例 1: 完整配置新域名的 MQTTS
**用户提问:**
```
我需要为域名 mqtt.mycompany.com 配置 MQTTS服务器 IP 是 10.20.30.40
EMQX 容器名是 emqx使用阿里云 DNS。请按照 setup-mqtts-acme skill 执行。
```
**AI 响应流程:**
1. 读取 `~/.opencode/skills/setup-mqtts-acme.md`
2. 验证 DNS 解析
3. 申请证书
4. 安装证书并配置自动更新
5. 重建 EMQX 容器
6. 验证配置
7. 生成文档和备份
**预期输出:**
- 证书文件: `/root/certs/mqtt.mycompany.com/`
- 备份包: `/root/certs/mqtt.mycompany.com-complete-*.tar.gz`
- MQTTS 端口 8883 工作正常
- 自动续期已配置
---
### 示例 2: 快速诊断 MQTTS 问题
**用户提问:**
```
根据 mqtts-quick-reference 的诊断脚本,
帮我检查 mq.example.com 的 MQTTS 配置状态。
```
**AI 执行:**
```bash
DOMAIN="mq.example.com"
echo "=== DNS ==="
dig $DOMAIN +short
echo "=== Certificate ==="
openssl x509 -in /root/certs/$DOMAIN/cert.pem -noout -dates -subject
echo "=== EMQX Container ==="
docker ps | grep emqx
echo "=== Listeners ==="
docker exec emqx emqx ctl listeners | grep -A 5 ssl
echo "=== SSL Test ==="
timeout 5 openssl s_client -connect $DOMAIN:8883 -servername $DOMAIN
echo "=== Auto-Renewal ==="
~/.acme.sh/acme.sh --list | grep $DOMAIN
```
**预期输出:**
- DNS 解析状态
- 证书有效期
- 容器运行状态
- SSL 监听器状态
- 自动续期配置
---
### 示例 3: 生成 Python 客户端代码
**用户提问:**
```
根据 mqtts-client-config帮我生成一个 Python MQTT 客户端,
连接到 mqtts://mq.example.com:8883
使用系统 CA 验证,订阅 sensor/# 主题。
```
**AI 响应(参考 skill 生成代码):**
```python
import paho.mqtt.client as mqtt
import ssl
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected successfully")
client.subscribe("sensor/#")
else:
print(f"Connection failed with code {rc}")
def on_message(client, userdata, msg):
print(f"{msg.topic}: {msg.payload.decode()}")
client = mqtt.Client()
client.username_pw_set("username", "password")
# Use system CA - no files needed
client.tls_set(
cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLSv1_2
)
client.on_connect = on_connect
client.on_message = on_message
client.connect("mq.example.com", 8883, 60)
client.loop_forever()
```
---
### 示例 4: ESP32 连接故障排查
**用户提问:**
```
我的 ESP32 连接 mqtts://mq.example.com:8883 时报错:
"SSL handshake failed"
参考 mqtts-client-config 的故障排查部分,帮我分析原因并提供解决方案。
```
**AI 响应(基于 skill 的故障排查部分):**
**可能原因:**
1. 未设置 CA 证书
2. ESP32 时间不正确
3. fullchain.pem 内容不完整
4. 证书过期
5. 内存不足
**解决方案:**
1. **检查是否设置了 CA 证书:**
```cpp
espClient.setCACert(root_ca); // 确保调用了这个
```
2. **同步时间ESP32 必须):**
```cpp
#include <time.h>
void setup() {
configTime(0, 0, "pool.ntp.org");
// Wait for time sync
time_t now = time(nullptr);
while (now < 8 * 3600 * 2) {
delay(500);
now = time(nullptr);
}
Serial.println("Time synchronized");
}
```
3. **验证 fullchain.pem 内容完整:**
确保包含完整的证书链(服务器证书 + 中间证书 + 根证书)
4. **如果内存不足,尝试:**
```cpp
// 减小 MQTT 缓冲区
client.setBufferSize(256);
// 或仅使用服务器证书(不推荐)
espClient.setCACert(server_cert_only);
```
5. **临时测试(仅用于诊断):**
```cpp
espClient.setInsecure(); // 跳过验证
// 如果成功,说明是证书问题;如果仍失败,是其他问题
```
---
### 示例 5: 证书手动续期
**用户提问:**
```
我的证书还有 10 天到期,想手动提前续期。
根据 mqtts-quick-reference 帮我执行强制续期。
```
**AI 执行(参考 skill**
```bash
# 1. 查看当前证书信息
~/.acme.sh/acme.sh --info -d mq.example.com
# 2. 强制续期
~/.acme.sh/acme.sh --renew -d mq.example.com --force
# 3. 验证新证书
openssl x509 -in /root/certs/mq.example.com/cert.pem -noout -dates
# 4. 检查容器是否重启
docker ps | grep emqx
# 5. 测试 MQTTS 连接
openssl s_client -connect mq.example.com:8883 -servername mq.example.com
```
**输出解释:**
- 新证书已生成
- EMQX 容器已自动重启(通过 reloadcmd
- MQTTS 连接正常
- 下次自动续期时间已更新
---
### 示例 6: 多语言客户端对比
**用户提问:**
```
根据 mqtts-client-config对比 Python、Node.js 和 Java
连接 MQTTS 的代码差异,我需要选择一个用于生产环境。
```
**AI 响应(基于 skill 的多语言示例):**
| 特性 | Python | Node.js | Java |
|------|--------|---------|------|
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 性能 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 系统 CA | 简单 | 简单 | 中等 |
| 异步支持 | 中等 | 原生 | 复杂 |
| 部署 | 需要 Python | 需要 Node.js | 打包成 JAR |
**推荐:**
- **快速原型**: Python最简单
- **高并发**: Node.js原生异步
- **企业级**: Java稳定可靠
代码示例见 skill 文档...
---
### 示例 7: 创建客户端分发包
**用户提问:**
```
我需要给客户端开发者提供连接包,包含:
1. fullchain.pem
2. 连接文档
3. 各语言示例代码
参考 mqtts-client-config 帮我生成。
```
**AI 执行:**
```bash
# 1. 创建分发目录
mkdir -p /root/mqtts-client-package
cd /root/mqtts-client-package
# 2. 复制证书
cp /root/certs/mq.example.com/fullchain.pem ./
# 3. 生成连接信息
cat > CONNECTION_INFO.txt << EOF
MQTTS Connection Information
Server: mq.example.com
Port: 8883
Protocol: MQTTS (MQTT over TLS)
TLS Version: TLSv1.2+
Authentication:
- Username: [Your Username]
- Password: [Your Password]
Certificate Verification:
- Option 1: Use system CA (recommended for PC/Mobile)
- Option 2: Use provided fullchain.pem (recommended for embedded)
Certificate Fingerprint (SHA256):
$(openssl x509 -in fullchain.pem -noout -fingerprint -sha256)

View File

@@ -0,0 +1,596 @@
# MQTTS Client Configuration Guide
## Overview
Complete guide for configuring MQTT clients to connect to MQTTS (MQTT over TLS) servers with single-direction TLS authentication.
## Authentication Model
**Single-Direction TLS (Server Authentication)**:
- Client verifies server identity (via TLS certificate)
- Server authenticates client (via username/password in MQTT layer)
- Client does NOT need client certificate
## What Clients Need
### Option 1: System CA (Recommended)
**No files needed!** Operating system's built-in CA trust store automatically verifies the server certificate.
**Advantages**:
- Zero configuration
- No certificate files to distribute
- Automatic updates with OS
**Requirements**:
- Server certificate issued by trusted CA (Let's Encrypt, ZeroSSL, etc.)
- System CA trust store up to date
**Suitable for**:
- PC applications (Windows, Mac, Linux)
- Mobile apps (Android, iOS)
- Server-side applications
- Docker containers with CA certificates
### Option 2: Explicit CA Certificate (fullchain.pem)
Explicitly specify the CA certificate chain for verification.
**Advantages**:
- Platform independent
- No dependency on system configuration
- Works on embedded devices without CA store
**Requirements**:
- Distribute `fullchain.pem` to clients
- Update file when server certificate is renewed (if using different CA)
**Suitable for**:
- Embedded devices (ESP32, Arduino)
- Minimal Linux systems without CA certificates
- Strict security requirements
- Air-gapped environments
### Option 3: Public Key Pinning (Advanced)
Pin the server's public key or certificate fingerprint.
**Advantages**:
- Highest security
- Immune to CA compromise
**Disadvantages**:
- Must update all clients when certificate changes
- Complex to manage
**Suitable for**:
- High-security applications (banking, healthcare)
- Known, controlled client deployments
## Language-Specific Examples
### Python (paho-mqtt)
#### System CA (Recommended)
```python
import paho.mqtt.client as mqtt
import ssl
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected successfully")
client.subscribe("test/#")
else:
print(f"Connection failed with code {rc}")
def on_message(client, userdata, msg):
print(f"{msg.topic}: {msg.payload.decode()}")
client = mqtt.Client()
client.username_pw_set("username", "password")
# Use system CA - no files needed
client.tls_set(
cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLSv1_2
)
client.on_connect = on_connect
client.on_message = on_message
client.connect("mq.example.com", 8883, 60)
client.loop_forever()
```
#### With fullchain.pem
```python
client.tls_set(
ca_certs="fullchain.pem",
cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLSv1_2
)
client.connect("mq.example.com", 8883, 60)
```
#### Skip Verification (Testing Only)
```python
# NOT RECOMMENDED FOR PRODUCTION
client.tls_set(cert_reqs=ssl.CERT_NONE)
client.tls_insecure_set(True)
client.connect("mq.example.com", 8883, 60)
```
### Node.js (mqtt.js)
#### System CA (Recommended)
```javascript
const mqtt = require('mqtt');
const options = {
host: 'mq.example.com',
port: 8883,
protocol: 'mqtts',
username: 'username',
password: 'password',
rejectUnauthorized: true // Verify server certificate
};
const client = mqtt.connect(options);
client.on('connect', () => {
console.log('Connected successfully');
client.subscribe('test/#');
});
client.on('message', (topic, message) => {
console.log(`${topic}: ${message.toString()}`);
});
client.on('error', (error) => {
console.error('Connection error:', error);
});
```
#### With fullchain.pem
```javascript
const fs = require('fs');
const options = {
host: 'mq.example.com',
port: 8883,
protocol: 'mqtts',
username: 'username',
password: 'password',
ca: fs.readFileSync('fullchain.pem'),
rejectUnauthorized: true
};
const client = mqtt.connect(options);
```
#### Skip Verification (Testing Only)
```javascript
const options = {
host: 'mq.example.com',
port: 8883,
protocol: 'mqtts',
username: 'username',
password: 'password',
rejectUnauthorized: false // NOT RECOMMENDED
};
```
### Java (Eclipse Paho)
#### System CA (Recommended)
```java
import org.eclipse.paho.client.mqttv3.*;
String broker = "ssl://mq.example.com:8883";
String clientId = "JavaClient";
MqttClient client = new MqttClient(broker, clientId);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName("username");
options.setPassword("password".toCharArray());
// Java uses system truststore by default
client.setCallback(new MqttCallback() {
public void connectionLost(Throwable cause) {
System.out.println("Connection lost: " + cause.getMessage());
}
public void messageArrived(String topic, MqttMessage message) {
System.out.println(topic + ": " + new String(message.getPayload()));
}
public void deliveryComplete(IMqttDeliveryToken token) {}
});
client.connect(options);
client.subscribe("test/#");
```
#### With fullchain.pem
```java
import javax.net.ssl.*;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.io.FileInputStream;
// Load CA certificate
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream fis = new FileInputStream("fullchain.pem");
Certificate ca = cf.generateCertificate(fis);
fis.close();
// Create KeyStore with CA
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("ca", ca);
// Create TrustManager
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
// Create SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
// Set in MQTT options
MqttConnectOptions options = new MqttConnectOptions();
options.setSocketFactory(sslContext.getSocketFactory());
options.setUserName("username");
options.setPassword("password".toCharArray());
client.connect(options);
```
### C# (.NET)
#### System CA (Recommended)
```csharp
using MQTTnet;
using MQTTnet.Client;
var factory = new MqttFactory();
var mqttClient = factory.CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("mq.example.com", 8883)
.WithCredentials("username", "password")
.WithTls() // Use system CA
.Build();
mqttClient.ConnectedAsync += async e => {
Console.WriteLine("Connected successfully");
await mqttClient.SubscribeAsync("test/#");
};
mqttClient.ApplicationMessageReceivedAsync += e => {
var payload = System.Text.Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
Console.WriteLine($"{e.ApplicationMessage.Topic}: {payload}");
return Task.CompletedTask;
};
await mqttClient.ConnectAsync(options);
```
#### With fullchain.pem
```csharp
using System.Security.Cryptography.X509Certificates;
var certificate = new X509Certificate2("fullchain.pem");
var tlsOptions = new MqttClientOptionsBuilderTlsParameters {
UseTls = true,
Certificates = new List<X509Certificate> { certificate },
SslProtocol = System.Security.Authentication.SslProtocols.Tls12
};
var options = new MqttClientOptionsBuilder()
.WithTcpServer("mq.example.com", 8883)
.WithCredentials("username", "password")
.WithTls(tlsOptions)
.Build();
```
### Go (paho.mqtt.golang)
#### System CA (Recommended)
```go
package main
import (
"fmt"
"crypto/tls"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
func main() {
tlsConfig := &tls.Config{} // Uses system CA by default
opts := mqtt.NewClientOptions()
opts.AddBroker("ssl://mq.example.com:8883")
opts.SetUsername("username")
opts.SetPassword("password")
opts.SetTLSConfig(tlsConfig)
opts.OnConnect = func(c mqtt.Client) {
fmt.Println("Connected successfully")
c.Subscribe("test/#", 0, nil)
}
opts.DefaultPublishHandler = func(c mqtt.Client, msg mqtt.Message) {
fmt.Printf("%s: %s\n", msg.Topic(), msg.Payload())
}
client := mqtt.NewClient(opts)
if token := client.Connect(); token.Wait() && token.Error() != nil {
panic(token.Error())
}
select {} // Keep running
}
```
#### With fullchain.pem
```go
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
)
// Load CA certificate
caCert, err := ioutil.ReadFile("fullchain.pem")
if err != nil {
panic(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}
opts.SetTLSConfig(tlsConfig)
```
### ESP32/Arduino (PubSubClient)
#### With fullchain.pem (Required)
```cpp
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
const char* ssid = "your_wifi_ssid";
const char* password = "your_wifi_password";
const char* mqtt_server = "mq.example.com";
const int mqtt_port = 8883;
const char* mqtt_user = "username";
const char* mqtt_pass = "password";
// Copy content from fullchain.pem here
const char* root_ca = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIGZDCCBEygAwIBAgIRAKIoXbOGN1X6u+vS+TbyLOgwDQYJKoZIhvcNAQEMBQAw\n" \
"...\n" \
"-----END CERTIFICATE-----\n" \
"-----BEGIN CERTIFICATE-----\n" \
"...\n" \
"-----END CERTIFICATE-----\n";
WiFiClientSecure espClient;
PubSubClient client(espClient);
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("]: ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
}
void reconnect() {
while (!client.connected()) {
Serial.print("Connecting to MQTT...");
if (client.connect("ESP32Client", mqtt_user, mqtt_pass)) {
Serial.println("connected");
client.subscribe("test/#");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" retrying in 5 seconds");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
espClient.setCACert(root_ca); // Set CA certificate
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
}
```
#### Skip Verification (Testing Only)
```cpp
void setup() {
// ...
espClient.setInsecure(); // NOT RECOMMENDED FOR PRODUCTION
client.setServer(mqtt_server, mqtt_port);
}
```
### Command Line (Mosquitto)
#### System CA (Recommended)
```bash
# Publish
mosquitto_pub -h mq.example.com -p 8883 \
--capath /etc/ssl/certs \
-t "test/topic" -m "Hello MQTTS" \
-u "username" -P "password"
# Subscribe
mosquitto_sub -h mq.example.com -p 8883 \
--capath /etc/ssl/certs \
-t "test/#" \
-u "username" -P "password"
```
#### With fullchain.pem
```bash
mosquitto_pub -h mq.example.com -p 8883 \
--cafile fullchain.pem \
-t "test/topic" -m "Hello MQTTS" \
-u "username" -P "password"
```
#### Skip Verification (Testing Only)
```bash
mosquitto_pub -h mq.example.com -p 8883 \
--insecure \
-t "test/topic" -m "Hello MQTTS" \
-u "username" -P "password"
```
## Troubleshooting
### SSL Handshake Failed
**Symptom**: Connection refused, SSL error, handshake failure
**Causes**:
1. System doesn't trust the CA
2. Certificate expired or not yet valid
3. System time incorrect
4. Wrong domain name
**Solutions**:
```bash
# Check system time
date
# Update CA certificates (Linux)
sudo update-ca-certificates
# Test with openssl
openssl s_client -connect mq.example.com:8883 -servername mq.example.com
# Use fullchain.pem instead of system CA
```
### Certificate Hostname Mismatch
**Symptom**: Hostname verification failed
**Cause**: Connecting to IP address instead of domain name
**Solution**: Always use domain name (mq.example.com), NOT IP (1.2.3.4)
### Connection Timeout
**Symptom**: Connection times out, no response
**Causes**:
1. Port 8883 blocked by firewall
2. EMQX not listening on 8883
3. DNS not resolving
**Solutions**:
```bash
# Test port connectivity
nc -zv mq.example.com 8883
# Check DNS
dig mq.example.com +short
# Test with telnet
telnet mq.example.com 8883
```
### ESP32 Out of Memory
**Symptom**: Heap overflow, crash during TLS handshake
**Causes**:
1. Certificate chain too large
2. Insufficient heap space
**Solutions**:
```cpp
// Use only server certificate (not full chain)
// Smaller but less secure
espClient.setCACert(server_cert_only);
// Reduce MQTT buffer size
client.setBufferSize(256);
// Use ECC certificate instead of RSA (smaller)
```
## Security Best Practices
### Production Requirements
**Must Have**:
- Enable certificate verification (`CERT_REQUIRED`)
- Use TLSv1.2 or higher
- Verify server hostname/domain
- Use strong username/password authentication
- Keep client code updated
**Never in Production**:
- Skip certificate verification (`setInsecure()`, `rejectUnauthorized: false`)
- Use `CERT_NONE` or disable verification
- Hard-code passwords in source code
- Use deprecated TLS versions (SSLv3, TLSv1.0, TLSv1.1)
### Certificate Updates
- System CA: Automatic with OS updates
- fullchain.pem: Update when server certificate renewed from different CA
- Public Key Pinning: Must update all clients before certificate renewal
### Credential Management
- Store credentials in environment variables or config files
- Use secrets management systems (AWS Secrets Manager, HashiCorp Vault)
- Rotate passwords regularly
- Use unique credentials per device/client
## Decision Matrix
| Client Platform | Recommended Method | Alternative | Testing Method |
|----------------|-------------------|-------------|----------------|
| PC/Mac | System CA | fullchain.pem | Skip verification |
| Linux Server | System CA | fullchain.pem | Skip verification |
| Android/iOS | System CA | fullchain.pem | Skip verification |
| ESP32/Arduino | fullchain.pem | Server cert only | setInsecure() |
| Docker Container | System CA | fullchain.pem | Skip verification |
| Embedded Linux | fullchain.pem | System CA | Skip verification |
## Summary
**Single-Direction TLS (Current Setup)**:
- ✅ Client verifies server (via certificate)
- ✅ Server verifies client (via username/password)
- ❌ Server does NOT verify client certificate
**Client Needs**:
- ✅ System CA (easiest, no files) OR fullchain.pem (most reliable)
- ❌ Does NOT need: server public key, server private key, server certificate
**Key Takeaway**: The server certificate (containing the public key) is automatically sent during TLS handshake. Clients only need a way to verify it (system CA or fullchain.pem).

View File

@@ -0,0 +1,277 @@
# MQTTS Quick Reference
## Quick Start
For fast MQTTS setup with default settings:
```bash
# 1. Set domain
DOMAIN="mq.example.com"
# 2. Verify DNS
dig $DOMAIN +short
# 3. Issue certificate
~/.acme.sh/acme.sh --issue --dns dns_ali -d $DOMAIN --keylength 2048
# 4. Install with auto-reload
~/.acme.sh/acme.sh --install-cert -d $DOMAIN \
--cert-file /root/certs/$DOMAIN/cert.pem \
--key-file /root/certs/$DOMAIN/key.pem \
--fullchain-file /root/certs/$DOMAIN/fullchain.pem \
--reloadcmd "docker restart emqx"
# 5. Fix permissions
chmod 755 /root/certs/$DOMAIN
chmod 644 /root/certs/$DOMAIN/*.pem
# 6. Recreate EMQX container with cert mount
docker stop emqx && docker rm emqx
docker run -d --name emqx --restart unless-stopped \
-p 1883:1883 -p 8083-8084:8083-8084 -p 8883:8883 -p 18083:18083 \
-v /root/emqx/data:/opt/emqx/data \
-v /root/emqx/log:/opt/emqx/log \
-v /root/certs/$DOMAIN:/opt/emqx/etc/certs:ro \
emqx/emqx:5.8.8
# 7. Verify
sleep 5
docker exec emqx emqx ctl listeners | grep ssl
openssl s_client -connect $DOMAIN:8883 -servername $DOMAIN < /dev/null
```
## Client Connection
### Python (System CA)
```python
import paho.mqtt.client as mqtt
import ssl
client = mqtt.Client()
client.username_pw_set("user", "pass")
client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2)
client.connect("mq.example.com", 8883, 60)
```
### Python (with fullchain.pem)
```python
client.tls_set(ca_certs="fullchain.pem", cert_reqs=ssl.CERT_REQUIRED)
client.connect("mq.example.com", 8883, 60)
```
### Node.js
```javascript
const mqtt = require('mqtt');
const client = mqtt.connect('mqtts://mq.example.com:8883', {
username: 'user',
password: 'pass',
rejectUnauthorized: true
});
```
### ESP32
```cpp
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
const char* root_ca = "-----BEGIN CERTIFICATE-----\n"...; // from fullchain.pem
WiFiClientSecure espClient;
PubSubClient client(espClient);
void setup() {
espClient.setCACert(root_ca);
client.setServer("mq.example.com", 8883);
client.connect("ESP32", "user", "pass");
}
```
## Common Commands
### Certificate Management
```bash
# List certificates
~/.acme.sh/acme.sh --list
# Check certificate info
~/.acme.sh/acme.sh --info -d $DOMAIN
# Force renewal
~/.acme.sh/acme.sh --renew -d $DOMAIN --force
# View certificate details
openssl x509 -in /root/certs/$DOMAIN/cert.pem -text -noout
# Check expiry
openssl x509 -in /root/certs/$DOMAIN/cert.pem -noout -dates
# Get fingerprint
openssl x509 -in /root/certs/$DOMAIN/cert.pem -noout -fingerprint -sha256
```
### EMQX Management
```bash
# Check listeners
docker exec emqx emqx ctl listeners
# Check connections
docker exec emqx emqx ctl broker stats
# View logs
docker logs emqx --tail 100 -f
# Restart container
docker restart emqx
# Check certificate files
docker exec emqx ls -l /opt/emqx/etc/certs/
```
### Testing
```bash
# Test SSL connection
openssl s_client -connect $DOMAIN:8883 -servername $DOMAIN
# Test with mosquitto
mosquitto_pub -h $DOMAIN -p 8883 \
--capath /etc/ssl/certs \
-t "test/topic" -m "hello" \
-u "username" -P "password"
# Test with custom CA
mosquitto_pub -h $DOMAIN -p 8883 \
--cafile fullchain.pem \
-t "test/topic" -m "hello" \
-u "username" -P "password"
```
### Backup & Export
```bash
# Create backup
cd /root/certs
tar czf $DOMAIN-backup-$(date +%Y%m%d).tar.gz $DOMAIN/
# Download (from local machine)
scp root@SERVER_IP:/root/certs/$DOMAIN-backup-*.tar.gz ./
# Extract public key
openssl rsa -in $DOMAIN/key.pem -pubout -out $DOMAIN/public.pem
# Get public key fingerprint
openssl rsa -in $DOMAIN/key.pem -pubout 2>/dev/null | openssl md5
```
## Troubleshooting
### DNS not resolving
```bash
dig $DOMAIN +short
nslookup $DOMAIN
# Wait 5-10 minutes for propagation
```
### Certificate issuance failed
```bash
# Check DNS API credentials
cat ~/.acme.sh/account.conf | grep Ali_
# Test with debug mode
~/.acme.sh/acme.sh --issue --dns dns_ali -d $DOMAIN --debug 2
```
### SSL connection failed
```bash
# Check port is open
nc -zv $DOMAIN 8883
# Check firewall
iptables -L -n | grep 8883
# Test with insecure (testing only)
mosquitto_pub -h $DOMAIN -p 8883 --insecure -t test -m hello
```
### Container won't start
```bash
# Check logs
docker logs emqx
# Check permissions
ls -la /root/certs/$DOMAIN/
# Fix permissions
chmod 755 /root/certs/$DOMAIN
chmod 644 /root/certs/$DOMAIN/*.pem
```
## Key Concepts
### Single-Direction TLS (Current Setup)
- Client verifies server identity (via certificate)
- Server authenticates client (via username/password)
- Client needs: System CA OR fullchain.pem
- Client does NOT need: server public key, server private key
### File Purpose
- `cert.pem`: Server certificate (contains public key)
- `key.pem`: Server private key (CONFIDENTIAL)
- `fullchain.pem`: Server cert + intermediate + root CA
- `public.pem`: Public key extracted from private key
- `cacert.pem`: CA certificate (usually symlink to fullchain)
### Client Requirements
| Client Type | Needs | Reason |
|-------------|-------|--------|
| PC/Mac/Server | Nothing (system CA) | OS trusts ZeroSSL |
| Android/iOS | Nothing (system CA) | OS trusts ZeroSSL |
| ESP32/Arduino | fullchain.pem | No system CA access |
| Docker | System CA or fullchain.pem | Depends on base image |
### Auto-Renewal
- Cron: Daily at 00:34
- Threshold: 60 days before expiry
- Action: Renew cert → Install → Restart EMQX
- No client action needed (unless using public key pinning)
## Important Notes
1. **Domain Required**: Must use domain name (mq.example.com), NOT IP address
2. **DNS Must Resolve**: A record must point to server before certificate issuance
3. **Port 8883**: Ensure firewall allows port 8883 for MQTTS
4. **Time Sync**: Server and client clocks must be accurate for TLS
5. **Key Reuse**: acme.sh reuses private key by default (public key stays same)
6. **Certificate Chain**: Modern clients need full chain, not just server cert
## Quick Diagnosis
### Check Everything
```bash
DOMAIN="mq.example.com"
echo "=== DNS ==="
dig $DOMAIN +short
echo "=== Certificate ==="
openssl x509 -in /root/certs/$DOMAIN/cert.pem -noout -dates -subject
echo "=== EMQX Container ==="
docker ps | grep emqx
echo "=== Listeners ==="
docker exec emqx emqx ctl listeners | grep -A 5 ssl
echo "=== SSL Test ==="
timeout 5 openssl s_client -connect $DOMAIN:8883 -servername $DOMAIN < /dev/null 2>&1 | grep -E "Verify return|subject=|issuer="
echo "=== Auto-Renewal ==="
~/.acme.sh/acme.sh --list | grep $DOMAIN
echo "=== Cron ==="
crontab -l | grep acme
```
## Reference
- EMQX Config: `/opt/emqx/etc/emqx.conf` (in container)
- Certificates: `/root/certs/$DOMAIN/` (on host) → `/opt/emqx/etc/certs/` (in container)
- acme.sh: `~/.acme.sh/`
- Logs: `/root/emqx/log/` (host) or `docker logs emqx`
- Dashboard: http://SERVER_IP:18083 (default: admin/public)

View File

@@ -0,0 +1,350 @@
# Setup MQTTS with Auto-Renewal Certificate
## Description
Complete workflow to configure MQTTS (MQTT over TLS) for EMQX using acme.sh with automatic certificate renewal. Supports DNS-based certificate validation (Alibaba Cloud DNS API).
## Parameters
- `domain`: MQTT domain name (e.g., mq.example.com)
- `ca`: Certificate Authority (default: zerossl, options: letsencrypt, zerossl)
- `dns_provider`: DNS provider (default: dns_ali, supports acme.sh DNS APIs)
- `emqx_container`: EMQX Docker container name (default: emqx)
- `cert_dir`: Certificate storage directory (default: /root/certs)
## Prerequisites Check
1. Verify DNS resolution for the domain
2. Check if acme.sh is installed
3. Verify DNS API credentials are configured
4. Check if EMQX container is running
5. Verify current container configuration
## Execution Steps
### Phase 1: DNS and Environment Verification
```bash
# Check DNS resolution
dig ${domain} +short
# Get server public IP
curl -s ifconfig.me
# Verify acme.sh installation
~/.acme.sh/acme.sh --version
# Check DNS API configuration
cat ~/.acme.sh/account.conf | grep -E "Ali_Key|Ali_Secret"
# Check EMQX container status
docker ps | grep ${emqx_container}
```
### Phase 2: Certificate Directory Setup
```bash
# Create certificate directory structure
mkdir -p ${cert_dir}/${domain}
chmod 755 ${cert_dir}
chmod 700 ${cert_dir}/${domain}
```
### Phase 3: Certificate Issuance
```bash
# Issue certificate using DNS validation
~/.acme.sh/acme.sh --issue \
--dns ${dns_provider} \
-d ${domain} \
--keylength 2048 \
--server ${ca}
```
### Phase 4: Certificate Installation
```bash
# Install certificate with auto-reload
~/.acme.sh/acme.sh --install-cert \
-d ${domain} \
--cert-file ${cert_dir}/${domain}/cert.pem \
--key-file ${cert_dir}/${domain}/key.pem \
--fullchain-file ${cert_dir}/${domain}/fullchain.pem \
--reloadcmd "docker restart ${emqx_container}"
# Set proper permissions
chmod 755 ${cert_dir}/${domain}
chmod 644 ${cert_dir}/${domain}/cert.pem
chmod 644 ${cert_dir}/${domain}/key.pem
chmod 644 ${cert_dir}/${domain}/fullchain.pem
# Create CA cert symlink
ln -sf ${cert_dir}/${domain}/fullchain.pem ${cert_dir}/${domain}/cacert.pem
```
### Phase 5: Extract Public Key
```bash
# Extract public key from private key
openssl rsa -in ${cert_dir}/${domain}/key.pem -pubout -out ${cert_dir}/${domain}/public.pem 2>/dev/null
chmod 644 ${cert_dir}/${domain}/public.pem
```
### Phase 6: EMQX Container Reconfiguration
```bash
# Backup current container configuration
docker inspect ${emqx_container} > /root/emqx-backup-$(date +%Y%m%d-%H%M%S).json
# Get current container ports and volumes
PORTS=$(docker inspect ${emqx_container} --format '{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}-p {{(index $conf 0).HostPort}}:{{$p}} {{end}}{{end}}')
# Stop and remove current container
docker stop ${emqx_container}
docker rm ${emqx_container}
# Recreate container with certificate mount
docker run -d \
--name ${emqx_container} \
--restart unless-stopped \
-p 1883:1883 \
-p 8083:8083 \
-p 8084:8084 \
-p 8883:8883 \
-p 18083:18083 \
-v /root/emqx/data:/opt/emqx/data \
-v /root/emqx/log:/opt/emqx/log \
-v ${cert_dir}/${domain}:/opt/emqx/etc/certs:ro \
emqx/emqx:5.8.8
```
### Phase 7: Verification
```bash
# Wait for container to start
sleep 8
# Check container status
docker ps | grep ${emqx_container}
# Verify certificate files in container
docker exec ${emqx_container} ls -l /opt/emqx/etc/certs/
# Check EMQX listeners
docker exec ${emqx_container} emqx ctl listeners
# Test SSL connection
timeout 10 openssl s_client -connect ${domain}:8883 -servername ${domain} -showcerts 2>/dev/null | openssl x509 -noout -text | grep -E "Subject:|Issuer:|Not Before|Not After|DNS:"
# Verify SSL handshake
timeout 10 openssl s_client -connect ${domain}:8883 -servername ${domain} 2>&1 <<< "Q" | grep -E "Verify return code:|SSL handshake|Protocol|Cipher"
```
### Phase 8: Generate Documentation
```bash
# Extract public key fingerprint
PUB_FP_MD5=$(openssl rsa -in ${cert_dir}/${domain}/key.pem -pubout 2>/dev/null | openssl md5 | awk '{print $2}')
PUB_FP_SHA256=$(openssl rsa -in ${cert_dir}/${domain}/key.pem -pubout 2>/dev/null | openssl sha256 | awk '{print $2}')
# Extract certificate fingerprints
CERT_FP_MD5=$(openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -fingerprint -md5 | cut -d= -f2)
CERT_FP_SHA1=$(openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -fingerprint -sha1 | cut -d= -f2)
CERT_FP_SHA256=$(openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -fingerprint -sha256 | cut -d= -f2)
# Generate fingerprints file
cat > ${cert_dir}/${domain}/fingerprints.txt << EOF
Certificate and Key Fingerprints
Generated: $(date)
=== Private Key Fingerprint ===
MD5: ${PUB_FP_MD5}
SHA256: ${PUB_FP_SHA256}
=== Certificate Fingerprint ===
MD5: ${CERT_FP_MD5}
SHA1: ${CERT_FP_SHA1}
SHA256: ${CERT_FP_SHA256}
=== Certificate Details ===
$(openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -subject -issuer -dates)
EOF
# Generate README
cat > ${cert_dir}/${domain}/README.txt << 'EOF'
EMQX MQTTS Certificate Files
Domain: ${domain}
Created: $(date +%Y-%m-%d)
CA: ${ca}
Key Length: RSA 2048
Auto-Renewal: Enabled
Files:
- cert.pem: Server certificate
- key.pem: Private key (CONFIDENTIAL)
- public.pem: Public key
- fullchain.pem: Full certificate chain
- cacert.pem: CA certificate (symlink)
- fingerprints.txt: Certificate fingerprints
- README.txt: This file
Client Configuration:
- PC/Mobile: Use system CA (no files needed)
- Embedded: Use fullchain.pem
- Connection: mqtts://${domain}:8883
Security:
⚠️ Keep key.pem secure and confidential
✓ cert.pem and fullchain.pem are safe to distribute
EOF
```
### Phase 9: Create Backup Package
```bash
# Create compressed backup
cd ${cert_dir}
tar czf ${domain}-complete-$(date +%Y%m%d-%H%M%S).tar.gz ${domain}/
# Generate checksums
md5sum ${domain}-complete-*.tar.gz | tail -1 > ${domain}-checksums.txt
sha256sum ${domain}-complete-*.tar.gz | tail -1 >> ${domain}-checksums.txt
```
### Phase 10: Verify Auto-Renewal
```bash
# Check certificate renewal configuration
~/.acme.sh/acme.sh --info -d ${domain}
# List all managed certificates
~/.acme.sh/acme.sh --list
# Verify cron job
crontab -l | grep acme
```
## Post-Installation Summary
Display the following information:
1. Certificate details (domain, validity, CA)
2. EMQX container status and ports
3. MQTTS listener status (port 8883)
4. Public key and certificate fingerprints
5. Client configuration guide (system CA vs fullchain.pem)
6. Backup file location and checksums
7. Auto-renewal schedule
## Client Configuration Guide
### Option 1: System CA (Recommended for PC/Mobile)
No files needed. The operating system's built-in CA trust store will verify the certificate automatically.
**Python Example:**
```python
import paho.mqtt.client as mqtt
import ssl
client = mqtt.Client()
client.username_pw_set("username", "password")
client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2)
client.connect("${domain}", 8883, 60)
```
**Node.js Example:**
```javascript
const mqtt = require('mqtt');
const client = mqtt.connect('mqtts://${domain}:8883', {
username: 'username',
password: 'password',
rejectUnauthorized: true
});
```
### Option 2: fullchain.pem (Recommended for Embedded Devices)
Distribute `fullchain.pem` to clients that cannot access system CA.
**Python Example:**
```python
client.tls_set(ca_certs="fullchain.pem", cert_reqs=ssl.CERT_REQUIRED)
client.connect("${domain}", 8883, 60)
```
**ESP32/Arduino Example:**
```cpp
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
const char* root_ca = "..."; // Content from fullchain.pem
WiFiClientSecure espClient;
PubSubClient client(espClient);
void setup() {
espClient.setCACert(root_ca);
client.setServer("${domain}", 8883);
client.connect("ESP32", "username", "password");
}
```
## Troubleshooting
### DNS Resolution Issues
- Verify A record points to server IP
- Wait 5-10 minutes for DNS propagation
- Test with: `dig ${domain} +short`
### Certificate Validation Failed
- Check system time is synchronized
- Update system CA certificates
- Use fullchain.pem instead of system CA
### Container Start Failed
- Check certificate file permissions
- Verify volume mount paths
- Review container logs: `docker logs ${emqx_container}`
### SSL Connection Refused
- Verify port 8883 is open in firewall
- Check EMQX listener status
- Test with: `openssl s_client -connect ${domain}:8883`
## Maintenance
### Certificate Renewal
Auto-renewal is configured via cron (daily at 00:34). Manual renewal:
```bash
~/.acme.sh/acme.sh --renew -d ${domain} --force
```
### Check Certificate Expiry
```bash
openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -dates
```
### Update Client Certificates
When certificate is renewed, container automatically restarts. No client updates needed unless using public key pinning.
## Security Notes
1. **Private Key (key.pem)**: Keep confidential, never share
2. **Certificate (cert.pem)**: Safe to distribute to clients
3. **Full Chain (fullchain.pem)**: Safe to distribute to clients
4. **Public Key (public.pem)**: Safe to distribute for pinning
5. **Backup Files**: Store in encrypted storage
6. **Auto-Renewal**: Enabled, checks daily, renews 60 days before expiry
## Output Files
- `${cert_dir}/${domain}/cert.pem`: Server certificate
- `${cert_dir}/${domain}/key.pem`: Private key
- `${cert_dir}/${domain}/public.pem`: Public key
- `${cert_dir}/${domain}/fullchain.pem`: Full certificate chain
- `${cert_dir}/${domain}/cacert.pem`: CA certificate (symlink)
- `${cert_dir}/${domain}/fingerprints.txt`: Fingerprints
- `${cert_dir}/${domain}/README.txt`: Documentation
- `${cert_dir}/${domain}-complete-*.tar.gz`: Backup package
- `${cert_dir}/${domain}-checksums.txt`: Package checksums
- `/root/emqx-backup-*.json`: Container configuration backup
## Success Criteria
- ✅ DNS resolves correctly
- ✅ Certificate issued successfully
- ✅ EMQX container running with certificate mount
- ✅ SSL listener on port 8883 active
- ✅ SSL handshake succeeds
- ✅ Auto-renewal configured
- ✅ Documentation generated
- ✅ Backup package created
## Related Commands
- Check certificate info: `openssl x509 -in cert.pem -text -noout`
- Test MQTTS connection: `mosquitto_pub -h ${domain} -p 8883 --cafile fullchain.pem -t test -m hello`
- View EMQX logs: `docker logs -f ${emqx_container}`
- List certificates: `~/.acme.sh/acme.sh --list`
- Force renewal: `~/.acme.sh/acme.sh --renew -d ${domain} --force`