|
| 1 | +# OpenBMCLAPI Kotlin Agent - Development Guide |
| 2 | + |
| 3 | +This document provides essential information for coding agents working on the OpenBMCLAPI Kotlin project. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +OpenBMCLAPI Kotlin Agent is a distributed file distribution network node for accelerating Minecraft resource downloads in China. It's a Kotlin/JVM project using Ktor framework, migrated from the original TypeScript/Node.js version. |
| 8 | + |
| 9 | +**Technology Stack:** |
| 10 | +- Language: Kotlin 2.2.20 (JVM target, Java 11+ required) |
| 11 | +- Web Framework: Ktor 3.3.1 with Netty server |
| 12 | +- Dependency Injection: Koin 4.1.1 with KSP annotations |
| 13 | +- Build System: Gradle with Kotlin DSL |
| 14 | +- Testing: Kotlin Test with JUnit, MockK for mocking |
| 15 | + |
| 16 | +## Build Commands |
| 17 | + |
| 18 | +### Core Commands |
| 19 | +```bash |
| 20 | +# Build the project |
| 21 | +./gradlew build |
| 22 | + |
| 23 | +# Run the application |
| 24 | +./gradlew run |
| 25 | + |
| 26 | +# Run all tests |
| 27 | +./gradlew test |
| 28 | + |
| 29 | +# Run a single test class |
| 30 | +./gradlew test --tests "ApplicationTest" |
| 31 | + |
| 32 | +# Run a specific test method |
| 33 | +./gradlew test --tests "ApplicationTest.主页返回OK" |
| 34 | + |
| 35 | +# Format code with ktlint |
| 36 | +./gradlew ktlintFormat |
| 37 | + |
| 38 | +# Check code style |
| 39 | +./gradlew ktlintCheck |
| 40 | + |
| 41 | +# Create executable JAR |
| 42 | +./gradlew buildFatJar |
| 43 | + |
| 44 | +# Clean build artifacts |
| 45 | +./gradlew clean |
| 46 | +``` |
| 47 | + |
| 48 | +### Development Commands |
| 49 | +```bash |
| 50 | +# Continuous build (watches for changes) |
| 51 | +./gradlew build --continuous |
| 52 | + |
| 53 | +# Run with specific profile |
| 54 | +./gradlew run --args="--config=application-dev.yaml" |
| 55 | + |
| 56 | +# Generate dependency report |
| 57 | +./gradlew dependencies |
| 58 | + |
| 59 | +# Check for dependency updates |
| 60 | +./gradlew dependencyUpdates |
| 61 | +``` |
| 62 | + |
| 63 | +## Code Style Guidelines |
| 64 | + |
| 65 | +### Formatting Rules (.editorconfig) |
| 66 | +- **Indentation:** 4 spaces (no tabs) |
| 67 | +- **Line Length:** 140 characters for Kotlin files, 120 for others |
| 68 | +- **Line Endings:** LF (Unix-style) |
| 69 | +- **Final Newline:** Required |
| 70 | +- **Trailing Whitespace:** Trimmed |
| 71 | + |
| 72 | +### Import Organization |
| 73 | +```kotlin |
| 74 | +// 1. Standard library imports |
| 75 | +import kotlinx.coroutines.runBlocking |
| 76 | +import kotlinx.serialization.json.Json |
| 77 | + |
| 78 | +// 2. Third-party library imports |
| 79 | +import io.ktor.server.application.Application |
| 80 | +import org.koin.core.annotation.Single |
| 81 | + |
| 82 | +// 3. Project imports (grouped by package) |
| 83 | +import com.bangbang93.openbmclapi.agent.config.ClusterConfig |
| 84 | +import com.bangbang93.openbmclapi.agent.service.BootstrapService |
| 85 | +``` |
| 86 | + |
| 87 | +### Naming Conventions |
| 88 | +- **Classes:** PascalCase (`BootstrapService`, `ClusterConfig`) |
| 89 | +- **Functions:** camelCase (`getToken()`, `setupCertificates()`) |
| 90 | +- **Properties:** camelCase (`clusterId`, `clusterSecret`) |
| 91 | +- **Constants:** SCREAMING_SNAKE_CASE (`AGENT_PROTOCOL_VERSION`) |
| 92 | +- **Packages:** lowercase with dots (`com.bangbang93.openbmclapi.agent`) |
| 93 | + |
| 94 | +### Type Annotations |
| 95 | +```kotlin |
| 96 | +// Explicit types for public APIs |
| 97 | +fun getConfig(env: ApplicationEnvironment): ClusterConfig |
| 98 | + |
| 99 | +// Type inference for local variables is acceptable |
| 100 | +val server = embeddedServer(Netty, env) |
| 101 | + |
| 102 | +// Nullable types explicitly declared |
| 103 | +var keystorePath: String? = null |
| 104 | + |
| 105 | +// Collection types with generics |
| 106 | +val storageOpts: Map<String, String> = emptyMap() |
| 107 | +``` |
| 108 | + |
| 109 | +### Function Structure |
| 110 | +```kotlin |
| 111 | +// Suspend functions for async operations |
| 112 | +suspend fun bootstrap() { |
| 113 | + logger.info { "Starting bootstrap process" } |
| 114 | + // Implementation |
| 115 | +} |
| 116 | + |
| 117 | +// Private functions use camelCase |
| 118 | +private fun scheduleFileCheck(lastModified: Long) { |
| 119 | + // Implementation |
| 120 | +} |
| 121 | + |
| 122 | +// Extension functions when appropriate |
| 123 | +suspend fun Application.module() { |
| 124 | + // Configuration |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +### Error Handling |
| 129 | +```kotlin |
| 130 | +// Use try-catch for expected exceptions |
| 131 | +try { |
| 132 | + val result = riskyOperation() |
| 133 | + logger.info { "Operation succeeded: $result" } |
| 134 | +} catch (e: Exception) { |
| 135 | + logger.error(e) { "Operation failed" } |
| 136 | + throw e // Re-throw if needed |
| 137 | +} |
| 138 | + |
| 139 | +// Use Result type for recoverable errors when appropriate |
| 140 | +fun parseConfig(input: String): Result<Config> { |
| 141 | + return try { |
| 142 | + Result.success(Json.decodeFromString(input)) |
| 143 | + } catch (e: Exception) { |
| 144 | + Result.failure(e) |
| 145 | + } |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +### Logging |
| 150 | +```kotlin |
| 151 | +// Use KotlinLogging with structured logging |
| 152 | +private val logger = KotlinLogging.logger {} |
| 153 | + |
| 154 | +// Log with lambda for lazy evaluation |
| 155 | +logger.info { "Processing ${files.size} files" } |
| 156 | +logger.error(exception) { "Failed to process request" } |
| 157 | + |
| 158 | +// Use appropriate log levels |
| 159 | +logger.debug { "Debug information" } |
| 160 | +logger.info { "General information" } |
| 161 | +logger.warn { "Warning message" } |
| 162 | +logger.error(exception) { "Error with exception" } |
| 163 | +``` |
| 164 | + |
| 165 | +### Dependency Injection (Koin) |
| 166 | +```kotlin |
| 167 | +// Service classes use @Single annotation |
| 168 | +@Single |
| 169 | +class BootstrapService( |
| 170 | + private val storage: IStorage, |
| 171 | + private val tokenManager: TokenManager, |
| 172 | +) { |
| 173 | + // Implementation |
| 174 | +} |
| 175 | + |
| 176 | +// Factory functions for configuration |
| 177 | +@Single |
| 178 | +fun getConfig(env: ApplicationEnvironment): ClusterConfig { |
| 179 | + // Configuration logic |
| 180 | +} |
| 181 | + |
| 182 | +// Inject dependencies in Ktor applications |
| 183 | +suspend fun Application.module() { |
| 184 | + val bootstrapService by inject<BootstrapService>() |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +## Project Structure |
| 189 | + |
| 190 | +``` |
| 191 | +src/main/kotlin/com/bangbang93/openbmclapi/agent/ |
| 192 | +├── Application.kt # Main entry point |
| 193 | +├── AppModule.kt # Koin DI configuration |
| 194 | +├── config/ # Configuration classes |
| 195 | +│ ├── Config.kt # Main configuration |
| 196 | +│ ├── Constants.kt # Application constants |
| 197 | +│ └── Version.kt # Version management |
| 198 | +├── model/ # Data models and DTOs |
| 199 | +├── nat/ # NAT/UPnP support |
| 200 | +├── routes/ # HTTP route handlers |
| 201 | +├── service/ # Business logic services |
| 202 | +├── storage/ # Storage backend implementations |
| 203 | +└── util/ # Utility functions |
| 204 | +``` |
| 205 | + |
| 206 | +## Testing Guidelines |
| 207 | + |
| 208 | +### Test Structure |
| 209 | +```kotlin |
| 210 | +class ServiceTest { |
| 211 | + @BeforeTest |
| 212 | + fun setup() { |
| 213 | + // Test setup |
| 214 | + } |
| 215 | + |
| 216 | + @AfterTest |
| 217 | + fun cleanup() { |
| 218 | + // Test cleanup |
| 219 | + } |
| 220 | + |
| 221 | + @Test |
| 222 | + fun `should handle normal case`() { |
| 223 | + // Arrange |
| 224 | + val input = createTestInput() |
| 225 | + |
| 226 | + // Act |
| 227 | + val result = serviceUnderTest.process(input) |
| 228 | + |
| 229 | + // Assert |
| 230 | + assertEquals(expected, result) |
| 231 | + } |
| 232 | +} |
| 233 | +``` |
| 234 | + |
| 235 | +### Mocking with MockK |
| 236 | +```kotlin |
| 237 | +val mockStorage = mockk<IStorage> { |
| 238 | + coEvery { check() } returns true |
| 239 | + coEvery { init() } returns Unit |
| 240 | +} |
| 241 | +``` |
| 242 | + |
| 243 | +### Integration Tests |
| 244 | +```kotlin |
| 245 | +@Test |
| 246 | +fun `integration test with test application`() = testApplication { |
| 247 | + application { |
| 248 | + runBlocking { this@application.appModule() } |
| 249 | + } |
| 250 | + |
| 251 | + client.get("/").apply { |
| 252 | + assertEquals(HttpStatusCode.OK, status) |
| 253 | + } |
| 254 | +} |
| 255 | +``` |
| 256 | + |
| 257 | +## Common Patterns |
| 258 | + |
| 259 | +### Coroutines Usage |
| 260 | +```kotlin |
| 261 | +// Use appropriate dispatchers |
| 262 | +CoroutineScope(Dispatchers.IO).launch { |
| 263 | + // I/O operations |
| 264 | +} |
| 265 | + |
| 266 | +// Structured concurrency |
| 267 | +suspend fun processFiles() = coroutineScope { |
| 268 | + val jobs = files.map { file -> |
| 269 | + async { processFile(file) } |
| 270 | + } |
| 271 | + jobs.awaitAll() |
| 272 | +} |
| 273 | +``` |
| 274 | + |
| 275 | +### Configuration Management |
| 276 | +- Use `application.yaml` for Ktor configuration |
| 277 | +- Use `.env` files for environment-specific settings (gitignored) |
| 278 | +- Support multiple configuration sources (Ktor config, dotenv, env vars, system props) |
| 279 | +- Provide sensible defaults for development |
| 280 | + |
| 281 | +### Storage Abstraction |
| 282 | +- Implement `IStorage` interface for new storage backends |
| 283 | +- Use factory pattern for storage creation |
| 284 | +- Support multiple storage types: file, WebDAV, MinIO, Aliyun OSS |
| 285 | + |
| 286 | +## Development Workflow |
| 287 | + |
| 288 | +1. **Before Making Changes:** |
| 289 | + - Run `./gradlew ktlintCheck` to verify code style |
| 290 | + - Run `./gradlew test` to ensure all tests pass |
| 291 | + |
| 292 | +2. **During Development:** |
| 293 | + - Use `./gradlew build --continuous` for automatic rebuilds |
| 294 | + - Write tests for new functionality |
| 295 | + - Follow existing patterns and conventions |
| 296 | + |
| 297 | +3. **Before Committing:** |
| 298 | + - Run `./gradlew ktlintFormat` to format code |
| 299 | + - Run `./gradlew build` to ensure everything compiles |
| 300 | + - Run `./gradlew test` to verify all tests pass |
| 301 | + |
| 302 | +## Key Dependencies |
| 303 | + |
| 304 | +- **Ktor:** Web framework and HTTP client |
| 305 | +- **Koin:** Dependency injection with KSP code generation |
| 306 | +- **Kotlinx Coroutines:** Async programming |
| 307 | +- **Socket.IO:** Cluster communication |
| 308 | +- **Logback:** Logging implementation |
| 309 | +- **BouncyCastle:** SSL/TLS certificate handling |
| 310 | +- **Various Storage:** MinIO, Aliyun OSS, WebDAV clients |
| 311 | + |
| 312 | +## Environment Setup |
| 313 | + |
| 314 | +Required environment variables (see `.env.example`): |
| 315 | +- `CLUSTER_ID`: Cluster identifier |
| 316 | +- `CLUSTER_SECRET`: Cluster authentication secret |
| 317 | +- `CLUSTER_PORT`: Server port (default: 4000) |
| 318 | +- `CLUSTER_STORAGE`: Storage backend type (default: "file") |
| 319 | + |
| 320 | +Optional variables: |
| 321 | +- `CLUSTER_IP`: Public IP address |
| 322 | +- `ENABLE_UPNP`: Enable UPnP NAT traversal |
| 323 | +- `SSL_KEY`/`SSL_CERT`: Custom SSL certificates (BYOC mode) |
0 commit comments