diff --git a/bun.lock b/bun.lock index 554356d..cefbd5b 100644 --- a/bun.lock +++ b/bun.lock @@ -9,21 +9,21 @@ }, "devDependencies": { "@kevisual/code-builder": "^0.0.6", - "@kevisual/context": "^0.0.6", + "@kevisual/context": "^0.0.8", "@kevisual/dts": "^0.0.4", "@kevisual/js-filter": "^0.0.5", "@kevisual/local-proxy": "^0.0.8", - "@kevisual/query": "^0.0.47", + "@kevisual/query": "^0.0.49", "@kevisual/use-config": "^1.0.30", - "@opencode-ai/plugin": "^1.2.6", + "@opencode-ai/plugin": "^1.2.10", "@types/bun": "^1.3.9", - "@types/node": "^25.2.3", + "@types/node": "^25.3.0", "@types/send": "^1.2.1", "@types/ws": "^8.18.1", "@types/xml2js": "^0.4.14", "eventemitter3": "^5.0.4", "fast-glob": "^3.3.3", - "hono": "^4.11.9", + "hono": "^4.12.2", "nanoid": "^5.1.6", "path-to-regexp": "^8.3.0", "send": "^1.2.1", @@ -43,7 +43,7 @@ "@kevisual/code-builder": ["@kevisual/code-builder@0.0.6", "", { "bin": { "code-builder": "bin/code.js", "builder": "bin/code.js" } }, "sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw=="], - "@kevisual/context": ["@kevisual/context@0.0.6", "", {}, "sha512-w7HBOuO3JH37n6xT6W3FD7ykqHTwtyxOQzTzfEcKDCbsvGB1wVreSxFm2bvoFnnFLuxT/5QMpKlnPrwvmcTGnw=="], + "@kevisual/context": ["@kevisual/context@0.0.8", "", {}, "sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA=="], "@kevisual/dts": ["@kevisual/dts@0.0.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", "rollup": "^4.57.1", "rollup-plugin-dts": "^6.3.0", "tslib": "^2.8.1" }, "bin": { "dts": "bin/dts.mjs" } }, "sha512-FVUaH/0nyhbHWpEVjFTGP54PLMm4Hf06aqWLdHOYHNPIgr1aK1C26kOH7iumklGFGk9w93IGxj8Zxe5fap5N2A=="], @@ -53,7 +53,7 @@ "@kevisual/local-proxy": ["@kevisual/local-proxy@0.0.8", "", {}, "sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q=="], - "@kevisual/query": ["@kevisual/query@0.0.47", "", {}, "sha512-ZR7WXeDDGUSzBtcGVU3J173sA0hCqrGTw5ybGbdNGlM0VyJV/XQIovCcSoZh1YpnciLRRqJvzXUgTnCkam+M3g=="], + "@kevisual/query": ["@kevisual/query@0.0.49", "", {}, "sha512-GrWW+QlBO5lkiqvb7PjOstNtpTQVSR74EHHWjm7YoL9UdT1wuPQXGUApZHmMBSh3NIWCf0AL2G1hPWZMC7YeOQ=="], "@kevisual/use-config": ["@kevisual/use-config@1.0.30", "", { "dependencies": { "@kevisual/load": "^0.0.6" }, "peerDependencies": { "dotenv": "^17" } }, "sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw=="], @@ -63,9 +63,9 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.6", "", { "dependencies": { "@opencode-ai/sdk": "1.2.6", "zod": "4.1.8" } }, "sha512-CJEp3k17yWsjyfivm3zQof8L42pdze3a7iTqMOyesHgJplSuLiBYAMndbBYMDuJkyAh0dHYjw8v10vVw7Kfl4Q=="], + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.15", "", { "dependencies": { "@opencode-ai/sdk": "1.2.15", "zod": "4.1.8" } }, "sha512-mh9S05W+CZZmo6q3uIEBubS66QVgiev7fRafX7vemrCfz+3pEIkSwipLjU/sxIewC9yLiDWLqS73DH/iEQzVDw=="], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.6", "", {}, "sha512-dWMF8Aku4h7fh8sw5tQ2FtbqRLbIFT8FcsukpxTird49ax7oUXP+gzqxM/VdxHjfksQvzLBjLZyMdDStc5g7xA=="], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.15", "", {}, "sha512-NUJNlyBCdZ4R0EBLjJziEQOp2XbRPJosaMcTcWSWO5XJPKGUpz0u8ql+5cR8K+v2RJ+hp2NobtNwpjEYfe6BRQ=="], "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" } }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="], @@ -129,7 +129,7 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, ""], - "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + "@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="], "@types/resolve": ["@types/resolve@1.20.2", "", {}, ""], @@ -185,7 +185,7 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""], - "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + "hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], @@ -261,7 +261,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.16.0", "", {}, ""], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "ws": ["@kevisual/ws@8.0.0", "", {}, "sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg=="], @@ -279,6 +279,16 @@ "@types/xml2js/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "bun-types/@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""], + + "@types/send/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""], + + "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""], + + "@types/xml2js/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""], + + "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""], } } diff --git a/system_design/https-server.md b/system_design/https-server.md new file mode 100644 index 0000000..ed27696 --- /dev/null +++ b/system_design/https-server.md @@ -0,0 +1,452 @@ +# HTTP Server 设计文档 + +## 概述 + +HTTP 服务器层负责接收外部 HTTP 请求,将其归一化后传递给 Router 层处理。所有请求统一入口为 `/api/router`。 + +## 请求入口 + +``` +POST /api/router?path=demo&key=01 +GET /api/router?path=demo&key=01 +``` + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| path | /api/router | 请求入口路径 | + +## 请求参数归一化 + +HTTP 请求的参数来源于两个部分,最终合并为一个 Message 对象: + +### 1. URL Query (searchParams) + +```typescript +// GET /api/router?path=demo&key=01&token=xxx +{ + path: "demo", + key: "01", + token: "xxx" +} +``` + +### 2. POST Body + +```typescript +// POST /api/router +// Body: { "name": "test", "value": 123 } +{ + name: "test", + value: 123 +} +``` + +### 3. 合并规则 + +最终的 Message = Query + Body(后者覆盖前者) + +```typescript +// GET /api/router?path=demo&key=01 +// POST Body: { "key": "02", "extra": "data" } +{ + path: "demo", + key: "02", // body 覆盖 query + extra: "data" +} +``` + +### 4. payload 参数 + +如果 query 或 body 中有 `payload` 字段且为 JSON 字符串,会自动解析为对象: + +```typescript +// GET /api/router?path=demo&key=01&payload={"id":123} +{ + path: "demo", + key: "01", + payload: { id: 123 } // 自动解析 +} +``` + +### 5. 认证信息 + +| 来源 | 字段名 | 说明 | +|------|--------|------| +| Authorization header | token | Bearer token | +| Cookie | token | cookie 中的 token | +| Cookie | cookies | 完整 cookie 对象 | + +## 路由匹配 + +### 方式一:path + key + +```bash +# 访问 path=demo, key=01 的路由 +POST /api/router?path=demo&key=01 +``` + +### 方式二:id + +```bash +# 直接通过路由 ID 访问 +POST /api/router?id=abc123 +``` + +## 内置路由 + +所有内置路由使用统一的访问方式: + +| 路由 | 访问方式 | 说明 | +|------|----------|------| +| router.list | POST /api/router?path=router&key=list | 获取路由列表 | + +### router.list + +获取当前应用所有路由列表。 + +**请求:** + +```bash +POST /api/router?path=router&key=list +``` + +**响应:** + +```json +{ + "code": 200, + "data": { + "list": [ + { + "id": "router$#$list", + "path": "router", + "key": "list", + "description": "列出当前应用下的所有的路由信息", + "middleware": [], + "metadata": {} + } + ], + "isUser": false + }, + "message": "success" +} +``` + +## Go 设计 + +```go +package server + +import ( + "encoding/json" + "net/http" +) + +// Message HTTP 请求归一化后的消息 +type Message struct { + Path string // 路由 path + Key string // 路由 key + ID string // 路由 ID (优先于 path+key) + Token string // 认证 token + Payload map[string]interface{} // payload 参数 + Query url.Values // 原始 query + Cookies map[string]string // cookie + // 其他参数 + Extra map[string]interface{} +} + +// HandleServer 解析 HTTP 请求 +func HandleServer(req *http.Request) (*Message, error) { + method := req.Method + if method != "GET" && method != "POST" { + return nil, fmt.Errorf("method not allowed") + } + + // 解析 query + query := req.URL.Query() + + // 获取 token + token := req.Header.Get("Authorization") + if token != "" { + token = strings.TrimPrefix(token, "Bearer ") + } + if token == "" { + if cookie, err := req.Cookie("token"); err == nil { + token = cookie.Value + } + } + + // 解析 body (POST) + var body map[string]interface{} + if method == "POST" { + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + body = make(map[string]interface{}) + } + } + + // 合并 query 和 body + msg := &Message{ + Token: token, + Query: query, + Cookies: parseCookies(req.Cookies()), + } + + // query 参数 + if v := query.Get("path"); v != "" { + msg.Path = v + } + if v := query.Get("key"); v != "" { + msg.Key = v + } + if v := query.Get("id"); v != "" { + msg.ID = v + } + if v := query.Get("payload"); v != "" { + if err := json.Unmarshal([]byte(v), &msg.Payload); err == nil { + // payload 解析成功 + } + } + + // body 参数覆盖 query + for k, v := range body { + msg.Extra[k] = v + switch k { + case "path": + msg.Path = v.(string) + case "key": + msg.Key = v.(string) + case "id": + msg.ID = v.(string) + case "payload": + msg.Payload = v.(map[string]interface{}) + } + } + + return msg, nil +} + +// RouterHandler 创建 HTTP 处理函数 +func RouterHandler(router *QueryRouter) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + // CORS + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST") + w.Header().Set("Content-Type", "application/json") + + if req.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + msg, err := HandleServer(req) + if err != nil { + json.NewEncoder(w).Encode(Result{Code: 400, Message: err.Error()}) + return + } + + // 调用 router.run + result, err := router.Run(*msg, nil) + if err != nil { + json.NewEncoder(w).Encode(Result{Code: 500, Message: err.Error()}) + return + } + + json.NewEncoder(w).Encode(result) + } +} + +// Server HTTP 服务器 +type Server struct { + Router *QueryRouter + Path string + Handlers []http.HandlerFunc +} + +func (s *Server) Listen(addr string) error { + mux := http.NewServeMux() + + // 自定义处理器 + for _, h := range s.Handlers { + mux.HandleFunc(s.Path, h) + } + + // 路由处理器 + mux.HandleFunc(s.Path, RouterHandler(s.Router)) + + return http.ListenAndServe(addr, mux) +} +``` + +## Rust 设计 + +```rust +use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Message HTTP 请求归一化后的消息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub path: Option, + pub key: Option, + pub id: Option, + pub token: Option, + pub payload: Option>, + #[serde(flatten)] + pub extra: HashMap, +} + +// Result 调用结果 +#[derive(Debug, Serialize)] +pub struct Result { + pub code: i32, + pub data: Option, + pub message: Option, +} + +// 解析 HTTP 请求 +pub async fn handle_server(req: HttpRequest, body: web::Bytes) -> Result { + let method = req.method().as_str(); + if method != "GET" && method != "POST" { + return Err("method not allowed".to_string()); + } + + // 获取 query 参数 + let query: web::Query> = web::Query::clone(&req); + + // 获取 token + let mut token = None; + if let Some(auth) = req.headers().get("authorization") { + if let Ok(s) = auth.to_str() { + token = Some(s.trim_start_matches("Bearer ").to_string()); + } + } + if token.is_none() { + if let Some(cookie) = req.cookie("token") { + token = Some(cookie.value().to_string()); + } + } + + // 解析 body (POST) + let mut body_map = HashMap::new(); + if method == "POST" { + if let Ok(v) = serde_json::from_slice::(&body) { + if let Some(obj) = v.as_object() { + for (k, val) in obj { + body_map.insert(k.clone(), val.clone()); + } + } + } + } + + // 构建 Message + let mut msg = Message { + path: query.get("path").cloned(), + key: query.get("key").cloned(), + id: query.get("id").cloned(), + token, + payload: None, + extra: HashMap::new(), + }; + + // 处理 payload + if let Some(p) = query.get("payload") { + if let Ok(v) = serde_json::from_str::(p) { + msg.payload = v.as_object().map(|m| m.clone()); + } + } + + // body 覆盖 query + for (k, v) in body_map { + match k.as_str() { + "path" => msg.path = v.as_str().map(|s| s.to_string()), + "key" => msg.key = v.as_str().map(|s| s.to_string()), + "id" => msg.id = v.as_str().map(|s| s.to_string()), + "payload" => msg.payload = v.as_object().map(|m| m.clone()), + _ => msg.extra.insert(k, v), + } + } + + Ok(msg) +} + +// HTTP 处理函数 +async fn router_handler( + req: HttpRequest, + body: web::Bytes, + router: web::Data, +) -> impl Responder { + let msg = match handle_server(req, body).await { + Ok(m) => m, + Err(e) => return HttpResponse::BadRequest().json(Result { + code: 400, + data: None, + message: Some(e), + }), + }; + + // 调用 router.run + match router.run(msg, None).await { + Ok(result) => HttpResponse::Ok().json(result), + Err(e) => HttpResponse::InternalServerError().json(Result { + code: 500, + data: None, + message: Some(e.to_string()), + }), + } +} + +// Server HTTP 服务器 +pub struct Server { + pub router: QueryRouter, + pub path: String, +} + +impl Server { + pub async fn listen(self, addr: &str) -> std::io::Result<()> { + let router = web::Data::new(self.router); + + HttpServer::new(move || { + App::new() + .app_data(router.clone()) + .route(&self.path, web::post().to(router_handler)) + .route(&self.path, web::get().to(router_handler)) + }) + .bind(addr)? + .run() + .await + } +} +``` + +## 请求示例 + +### 基础请求 + +```bash +# 访问 demo/01 路由 +curl -X POST "http://localhost:4002/api/router?path=demo&key=01" + +# 带 body +curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \ + -H "Content-Type: application/json" \ + -d '{"name":"test","value":123}' +``` + +### 获取路由列表 + +```bash +curl -X POST "http://localhost:4002/api/router?path=router&key=list" +``` + +### 带认证 + +```bash +# 通过 Header +curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \ + -H "Authorization: Bearer your-token" + +# 通过 Cookie +curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \ + -b "token=your-token" +``` diff --git a/system_design/router.md b/system_design/router.md new file mode 100644 index 0000000..e14d12b --- /dev/null +++ b/system_design/router.md @@ -0,0 +1,378 @@ +# Router 系统设计文档 + +## 概述 + +轻量级路由框架,支持链式路由、中间件模式、统一上下文。适用于构建 API 服务,支持跨语言实现(Go、Rust 等)。 + +## 核心组件 + +### Route + +| 字段 | 类型 | 说明 | +|------|------|------| +| path | string | 一级路径 | +| key | string | 二级路径 | +| id | string | 唯一标识 | +| run | Handler | 业务处理函数 | +| nextRoute | NextRoute? | 下一个路由 | +| middleware | string[] | 中间件 ID 列表 | +| metadata | T | 元数据/参数 schema | +| type | string | 类型:route / middleware | +| isDebug | bool | 是否开启调试 | + +### NextRoute + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | string? | 路由 ID | +| path | string? | 一级路径 | +| key | string? | 二级路径 | + +### RouteContext + +| 字段 | 类型 | 说明 | +|------|------|------| +| appId | string? | 应用 ID | +| query | object | URL 参数和 payload 合并结果 | +| args | object | 同 query | +| body | any | 响应 body | +| code | number | 响应状态码 | +| message | string | 响应消息 | +| state | object | 状态传递 | +| currentId | string? | 当前路由 ID | +| currentPath | string? | 当前路径 | +| currentKey | string? | 当前 key | +| currentRoute | Route? | 当前路由对象 | +| progress | [string, string][] | 路由执行路径 | +| nextQuery | object | 传递给下一个路由的参数 | +| end | boolean | 是否提前结束 | +| app | QueryRouter? | 路由实例引用 | +| error | any | 错误信息 | +| call | function | 调用其他路由(返回完整上下文) | +| run | function | 调用其他路由(返回简化结果) | +| throw | function | 抛出错误 | +| needSerialize | boolean | 是否需要序列化 | + +### QueryRouter + +| 方法 | 说明 | +|------|------| +| add(route, opts?) | 添加路由 | +| remove(route) | 按 path/key 移除路由 | +| removeById(id) | 按 ID 移除路由 | +| runRoute(path, key, ctx) | 执行单个路由 | +| parse(message, ctx) | 入口解析,返回完整上下文 | +| call(message, ctx) | 调用路由,返回完整上下文 | +| run(message, ctx) | 调用路由,返回简化结果 {code, data, message} | +| getHandle() | 获取 HTTP 处理函数 | +| setContext(ctx) | 设置默认上下文 | +| getList(filter?) | 获取路由列表 | +| hasRoute(path, key) | 检查路由是否存在 | +| findRoute(opts) | 查找路由 | +| exportRoutes() | 导出所有路由 | +| importRoutes(routes) | 批量导入路由 | +| createRouteList(opts) | 创建内置的路由列表功能 | + +### QueryRouterServer + +继承 QueryRouter,新增: + +| 字段 | 类型 | 说明 | +|------|------|------| +| appId | string | 应用 ID | +| handle | function | HTTP 处理函数 | + +| 方法 | 说明 | +|------|------| +| setHandle(wrapperFn, ctx) | 设置处理函数 | +| route(path, key?, opts?) | 工厂方法创建路由 | + +## Go 设计 + +```go +package router + +// Route 路由单元 +type Route struct { + Path string + Key string + ID string + Run func(ctx *RouteContext) (*RouteContext, error) + NextRoute *NextRoute + Middleware []string + Metadata map[string]interface{} + Type string + IsDebug bool +} + +// NextRoute 下一个路由 +type NextRoute struct { + ID string + Path string + Key string +} + +// RouteContext 请求上下文 +type RouteContext struct { + AppID string + Query map[string]interface{} + Args map[string]interface{} + Body interface{} + Code int + Message string + State map[string]interface{} + CurrentID string + CurrentPath string + CurrentKey string + CurrentRoute *Route + Progress [][2]string + NextQuery map[string]interface{} + End bool + App *QueryRouter + Error error + NeedSerialize bool + // Methods + Call func(msg interface{}, ctx *RouteContext) (*RouteContext, error) + Run func(msg interface{}, ctx *RouteContext) (interface{}, error) + Throw func(err interface{}) +} + +// Message 调用消息 +type Message struct { + ID string + Path string + Key string + Payload map[string]interface{} +} + +// Result 调用结果 +type Result struct { + Code int + Data interface{} + Message string +} + +// AddOpts 添加选项 +type AddOpts struct { + Overwrite bool +} + +// QueryRouter 路由管理器 +type QueryRouter struct { + Routes []*Route + MaxNextRoute int + Context *RouteContext +} + +func NewQueryRouter() *QueryRouter + +func (r *QueryRouter) Add(route *Route, opts *AddOpts) +func (r *QueryRouter) Remove(path, key string) +func (r *QueryRouter) RemoveByID(id string) +func (r *QueryRouter) RunRoute(path, key string, ctx *RouteContext) (*RouteContext, error) +func (r *QueryRouter) Parse(msg Message, ctx *RouteContext) (*RouteContext, error) +func (r *QueryRouter) Call(msg Message, ctx *RouteContext) (*RouteContext, error) +func (r *QueryRouter) Run(msg Message, ctx *RouteContext) (Result, error) +func (r *QueryRouter) GetHandle() func(msg interface{}) Result +func (r *QueryRouter) SetContext(ctx *RouteContext) +func (r *QueryRouter) GetList() []Route +func (r *QueryRouter) HasRoute(path, key string) bool +func (r *QueryRouter) FindRoute(opts FindOpts) *Route + +// QueryRouterServer 服务端 +type QueryRouterServer struct { + QueryRouter + AppID string + Handle func(msg interface{}) Result +} + +type ServerOpts struct { + HandleFn func(msg interface{}, ctx interface{}) Result + Context *RouteContext + AppID string +} + +func NewQueryRouterServer(opts *ServerOpts) *QueryRouterServer + +func (s *QueryRouterServer) SetHandle(wrapperFn func(msg interface{}, ctx interface{}) Result, ctx *RouteContext) +func (s *QueryRouterServer) Route(path string, key ...string) *Route +``` + +## Rust 设计 + +```rust +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; + +// Route 路由单元 +pub struct Route { + pub path: String, + pub key: String, + pub id: String, + pub run: Option Pin>>> + Send>>, + pub next_route: Option, + pub middleware: Vec, + pub metadata: M, + pub route_type: String, + pub is_debug: bool, +} + +// NextRoute 下一个路由 +#[derive(Clone)] +pub struct NextRoute { + pub id: Option, + pub path: Option, + pub key: Option, +} + +// RouteContext 请求上下文 +#[derive(Clone)] +pub struct RouteContext { + pub app_id: Option, + pub query: HashMap, + pub args: HashMap, + pub body: Option, + pub code: Option, + pub message: Option, + pub state: HashMap, + pub current_id: Option, + pub current_path: Option, + pub current_key: Option, + pub current_route: Option>, + pub progress: Vec<(String, String)>, + pub next_query: HashMap, + pub end: bool, + pub app: Option>, + pub error: Option, + pub need_serialize: bool, +} + +// Message 调用消息 +#[derive(Clone)] +pub struct Message { + pub id: Option, + pub path: Option, + pub key: Option, + pub payload: HashMap, +} + +// Result 调用结果 +pub struct Result { + pub code: i32, + pub data: Option, + pub message: Option, +} + +// AddOpts 添加选项 +pub struct AddOpts { + pub overwrite: bool, +} + +// FindOpts 查找选项 +pub struct FindOpts { + pub path: Option, + pub key: Option, + pub id: Option, +} + +// QueryRouter 路由管理器 +pub struct QueryRouter { + pub routes: Vec, + pub max_next_route: usize, + pub context: RouteContext, +} + +impl QueryRouter { + pub fn new() -> Self + pub fn add(&mut self, route: Route, opts: Option) + pub fn remove(&mut self, path: &str, key: &str) + pub fn remove_by_id(&mut self, id: &str) + pub async fn run_route(&self, path: &str, key: &str, ctx: RouteContext) -> Result + pub async fn parse(&self, msg: Message, ctx: Option) -> Result + pub async fn call(&self, msg: Message, ctx: Option) -> Result + pub async fn run(&self, msg: Message, ctx: Option) -> Result + pub fn get_handle(&self) -> impl Fn(Message) -> Result + '_ + pub fn set_context(&mut self, ctx: RouteContext) + pub fn get_list(&self) -> Vec + pub fn has_route(&self, path: &str, key: &str) -> bool + pub fn find_route(&self, opts: FindOpts) -> Option<&Route> +} + +// ServerOpts 服务端选项 +pub struct ServerOpts { + pub handle_fn: Option) -> Result + Send>>, + pub context: Option, + pub app_id: Option, +} + +// QueryRouterServer 服务端 +pub struct QueryRouterServer { + pub router: QueryRouter, + pub app_id: String, + pub handle: Option Result + Send>>, +} + +impl QueryRouterServer { + pub fn new(opts: Option) -> Self + pub fn set_handle(&mut self, wrapperFn: Box Result + Send>) + pub fn route(&self, path: &str, key: Option<&str>) -> Route +} +``` + +## 执行流程 + +``` +Message → parse() → runRoute() → [middleware] → run() → [nextRoute] → ... + ↓ + RouteContext (层层传递) +``` + +1. `parse()` 接收消息,初始化上下文(query、args、state) +2. `runRoute()` 查找路由,先执行 middleware,再执行 run +3. middleware 执行出错立即返回错误 +4. 如有 nextRoute,递归执行下一个路由(最多 40 层) +5. 返回最终 RouteContext + +## 特性说明 + +- **双层路径**: path + key 构成唯一路由 +- **链式路由**: nextRoute 支持路由链式执行 +- **中间件**: 每个 Route 可挂载多个 middleware +- **统一上下文**: RouteContext 贯穿整个请求生命周期 + +## 内置路由 + +框架内置以下路由,通过 HTTP 访问时使用 `path` 和 `key` 参数: + +| 路由 path | 路由 key | 说明 | +|-----------|----------|------| +| router | list | 获取当前应用所有路由列表 | + +### router/list + +获取当前应用所有路由列表。 + +**访问方式:** `POST /api/router?path=router&key=list` + +**响应:** + +```json +{ + "code": 200, + "data": { + "list": [ + { + "id": "router$#$list", + "path": "router", + "key": "list", + "description": "列出当前应用下的所有的路由信息", + "middleware": [], + "metadata": {} + } + ], + "isUser": false + }, + "message": "success" +} +``` diff --git a/system_design/ws-server.md b/system_design/ws-server.md new file mode 100644 index 0000000..6b631f0 --- /dev/null +++ b/system_design/ws-server.md @@ -0,0 +1,421 @@ +# WebSocket Server 设计文档 + +## 概述 + +WebSocket 服务器支持实时双向通信,可与 HTTP 服务器共享同一端口。所有 WebSocket 连接统一入口为 `/api/router`,通过 `type` 参数区分业务类型。 + +## 连接入口 + +``` +ws://host:port/api/router?token=xxx&id=client-id +``` + +| 参数 | 说明 | +|------|------| +| token | 认证 token | +| id | 客户端标识 | + +## 消息协议 + +### 请求消息格式 + +```json +{ + "type": "router", + "path": "demo", + "key": "01", + "payload": { "name": "test" } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| type | string | 消息类型:router | +| path | string | 路由 path | +| key | string | 路由 key | +| payload | object | 请求参数 | + +### 响应消息格式 + +```json +{ + "type": "router", + "code": 200, + "data": { "result": "success" }, + "message": "success" +} +``` + +### 内置消息类型 + +| 消息 | 说明 | +|------|------| +| ping | 服务端返回 pong | +| close | 关闭连接 | +| connected | 连接成功通知 | + +## 自定义 WebSocket 监听器 + +可以注册自定义的 WebSocket 监听器来处理特定路径的 WebSocket 连接。 + +### 注册监听器 + +```typescript +app.on([ + { + path: '/ws/chat', + io: true, + func: async (req, res) => { + const { data, token, pathname, id, ws, emitter } = req; + // 处理聊天消息 + res.end({ message: 'received' }); + } + } +]); +``` + +### Listener 配置 + +| 字段 | 类型 | 说明 | +|------|------|------| +| path | string | WebSocket 路径 | +| io | boolean | 是否为 WebSocket 监听器 | +| func | function | 处理函数 | +| json | boolean | 是否自动解析 JSON | + +## 执行流程 + +``` +WebSocket 连接 → 查找 listener → 自定义处理 / 默认 router 处理 → 响应 +``` + +1. WebSocket 连接建立时,解析 URL 获取 pathname、token、id +2. 查找是否有匹配的自定义 listener +3. 如有自定义 listener,调用对应处理函数 +4. 如无自定义 listener,使用默认处理: + - type = 'router' 时,调用 handle 处理路由 + - 支持 ping/pong/close 内置命令 +5. 响应消息通过 ws.send 返回 + +## Go 设计 + +```go +package server + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/websocket" +) + +// WebSocketMessage WebSocket 消息 +type WebSocketMessage struct { + Type string `json:"type"` + Path string `json:"path,omitempty"` + Key string `json:"key,omitempty"` + Payload map[string]interface{} `json:"payload,omitempty"` + Data interface{} `json:"data,omitempty"` + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +// WebSocketUpgrader WebSocket 升级配置 +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// Listener WebSocket 监听器 +type Listener struct { + Path string + IO bool + Func func(msg WebSocketMessage, ws *websocket.Conn) error + JSON bool +} + +// WsServer WebSocket 服务器 +type WsServer struct { + Router *QueryRouter + Path string + Listeners []Listener + Listen func() error +} + +// HandleWebSocket 处理 WebSocket 连接 +func (s *WsServer) HandleWebSocket(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer ws.Close() + + // 解析 token 和 id + token := r.URL.Query().Get("token") + id := r.URL.Query().Get("id") + + for { + // 读取消息 + _, msgBytes, err := ws.ReadMessage() + if err != nil { + break + } + + var msg WebSocketMessage + if err := json.Unmarshal(msgBytes, &msg); err != nil { + ws.WriteJSON(WebSocketMessage{ + Code: 400, + Message: "invalid message", + }) + continue + } + + // 查找自定义 listener + pathname := r.URL.Path + var handled bool + for _, listener := range s.Listeners { + if listener.IO && listener.Path == pathname { + if err := listener.Func(msg, ws); err != nil { + ws.WriteJSON(WebSocketMessage{ + Code: 500, + Message: err.Error(), + }) + } + handled = true + break + } + } + + if handled { + continue + } + + // 默认处理:router 类型 + if msg.Type == "router" { + result, err := s.Router.Run(Message{ + Path: msg.Path, + Key: msg.Key, + Payload: msg.Payload, + }, nil) + if err != nil { + ws.WriteJSON(WebSocketMessage{ + Code: 500, + Message: err.Error(), + }) + continue + } + ws.WriteJSON(WebSocketMessage{ + Type: "router", + Code: result.Code, + Data: result.Data, + Message: result.Message, + }) + continue + } + + // 内置命令 + if string(msgBytes) == "ping" { + ws.WriteMessage(websocket.TextMessage, []byte("pong")) + continue + } + + if string(msgBytes) == "close" { + break + } + + ws.WriteJSON(WebSocketMessage{ + Code: 400, + Message: "unknown type", + }) + } +} + +// Start 启动服务器 +func (s *WsServer) Start(addr string) error { + http.HandleFunc(s.Path, s.HandleWebSocket) + return http.ListenAndServe(addr, nil) +} +``` + +## Rust 设计 + +```rust +use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder}; +use actix_ws::{Message, Session, Aggregate}; +use std::sync::Arc; + +// WebSocket 消息 +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct WsMessage { + pub r#type: Option, + pub path: Option, + pub key: Option, + pub payload: Option, + pub data: Option, + pub code: Option, + pub message: Option, +} + +// Listener 配置 +pub struct Listener { + pub path: String, + pub io: bool, + pub func: Option Result<(), Box> + Send>>, + pub json: bool, +} + +// WsServer +pub struct WsServer { + pub router: QueryRouter, + pub path: String, + pub listeners: Vec, +} + +impl WsServer { + pub async fn start(&self, addr: &str) -> std::io::Result<()> { + let router = web::Data::new(self.router.clone()); + let path = self.path.clone(); + + HttpServer::new(move || { + App::new() + .app_data(router.clone()) + .service( + web::resource(&path) + .route(web::get().to(ws_handler)) + ) + }) + .bind(addr)? + .run() + .await + } +} + +async fn ws_handler( + req: HttpRequest, + stream: web::Payload, + router: web::Data, +) -> impl Responder { + let (resp, session, msg) = actix_ws::handle(&req, stream)?; + + let mut session = session; + + // 处理消息 + while let Some(result) = msg.next().await { + match result { + Ok(Message::Text(text)) => { + let ws_msg: WsMessage = match serde_json::from_str(&text) { + Ok(m) => m, + Err(_) => { + let _ = session.text(r#"{"code":400,"message":"invalid message"}"#).await; + continue; + } + }; + + // 调用 router + if ws_msg.r#type.as_deref() == Some("router") { + let msg = Message { + path: ws_msg.path.clone(), + key: ws_msg.key.clone(), + payload: ws_msg.payload.clone(), + }; + + match router.run(msg, None).await { + Ok(result) => { + let resp = WsMessage { + r Some("router".to_string()), + code: Some(result.code), + data: result.data, + message: result.message, + }; + let _ = session.text(serde_json::to_string(&resp).unwrap()).await; + } + Err(e) => { + let _ = session.text(format!(r#"{{"code":500,"message":"{}"}}"#, e)).await; + } + } + } else if text == "ping" { + let _ = session.text("pong").await; + } else if text == "close" { + break; + } + } + Ok(Message::Close(_)) => break, + Err(e) => { + let _ = session.text(format!(r#"{{"code":500,"message":"{}"}}"#, e)).await; + break; + } + _ => {} + } + } + + let _ = session.close(None).await; + + Ok(resp) +} +``` + +## 客户端示例 + +### TypeScript 客户端 + +```typescript +import { ReconnectingWebSocket } from '@kevisual/router'; + +const ws = new ReconnectingWebSocket('ws://localhost:4002/api/router', { + onOpen: () => { + console.log('connected'); + }, + onMessage: (data) => { + console.log('received:', data); + } +}); + +// 调用路由 +ws.send({ + type: 'router', + path: 'demo', + key: '01', + payload: { name: 'test' } +}); +``` + +### 原生 WebSocket + +```javascript +const ws = new WebSocket('ws://localhost:4002/api/router?token=xxx&id=client1'); + +ws.onopen = () => { + // 调用路由 + ws.send(JSON.stringify({ + type: 'router', + path: 'demo', + key: '01', + payload: { name: 'test' } + })); +}; + +ws.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log('received:', data); +}; + +// 发送 ping +ws.send('ping'); + +// 关闭连接 +ws.send('close'); +``` + +## 连接示例 + +```bash +# 基础连接 +wscat -c ws://localhost:4002/api/router + +# 带 token 和 id +wscat -c "ws://localhost:4002/api/router?token=xxx&id=client1" + +# 发送路由请求 +{"type":"router","path":"demo","key":"01"} +```