refactor: move server rs workspace entries into crates
This commit is contained in:
20
server-rs/crates/api-server/Cargo.toml
Normal file
20
server-rs/crates/api-server/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "api-server"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
|
||||
time = { version = "0.3", features = ["formatting"] }
|
||||
tower-http = { version = "0.6", features = ["trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
http-body-util = "0.1"
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
96
server-rs/crates/api-server/README.md
Normal file
96
server-rs/crates/api-server/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# api-server 主工程 crate 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. crate 职责
|
||||
|
||||
`api-server` 是新后端的 Axum 主工程 crate,后续负责:
|
||||
|
||||
1. `main.rs` 启动入口
|
||||
2. `Router` 装配
|
||||
3. `with_state` 共享状态注入
|
||||
4. 中间件挂载
|
||||
5. `/healthz`、`/api/*`、SSE 与静态资源兼容层装配
|
||||
6. 由 `../../scripts/dev.ps1` 与 `../../scripts/dev.sh` 驱动的本地开发启动链路
|
||||
7. 由 `../../scripts/test.ps1` 与 `../../scripts/test.sh` 驱动的本地测试链路
|
||||
8. 由 `../../scripts/check.ps1` 与 `../../scripts/check.sh` 驱动的本地统一检查链路
|
||||
9. 由 `../../scripts/smoke.ps1` 与 `../../scripts/smoke.sh` 驱动的本地启动与协议冒烟链路
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前目录已经完成以下基础骨架:
|
||||
|
||||
1. 目录占位
|
||||
2. `Cargo.toml`
|
||||
3. `src/main.rs`
|
||||
4. `src/app.rs`
|
||||
5. `src/state.rs`
|
||||
6. `src/config.rs`
|
||||
7. `src/logging.rs`
|
||||
8. 基础 `TraceLayer` 挂载与 `tracing subscriber` 初始化
|
||||
|
||||
后续与本 crate 直接相关的任务包括:
|
||||
|
||||
1. [x] 接入统一日志与 tracing
|
||||
2. [x] 接入 `request_id`
|
||||
3. [x] 接入统一错误处理中间件
|
||||
4. [x] 接入 response envelope
|
||||
5. [x] 接入 `/healthz`
|
||||
|
||||
当前 tracing 约定:
|
||||
|
||||
1. 进程启动时统一初始化 `tracing subscriber`。
|
||||
2. 默认日志过滤器来自 `GENARRATIVE_API_LOG`,未提供时回落到 `info,tower_http=info`。
|
||||
3. HTTP 访问日志统一通过 Axum 路由层的 `TraceLayer` 输出,后续 `request_id`、响应头与错误中间件继续在同一层扩展。
|
||||
|
||||
当前 request context 约定:
|
||||
|
||||
1. 中间件优先读取来访 `x-request-id`,未提供时生成新的 UUID。
|
||||
2. `request_id` 会统一写入请求 `extensions` 与请求头,供 tracing、错误处理中间件和响应头层复用。
|
||||
3. 最终响应会回写同一个 `x-request-id`,保证调用方、日志链路和后续 envelope `meta.requestId` 可对齐。
|
||||
|
||||
当前错误处理中间件约定:
|
||||
|
||||
1. 对 Axum 默认产生的空 `4xx / 5xx` 响应,统一归一化为 legacy 兼容 JSON 错误体:`{ error: { code, message, details? } }`。
|
||||
2. 已经带 `content-type` 的业务错误响应不会被覆盖,避免抢走后续 response envelope 的职责。
|
||||
3. 统一错误日志会复用当前请求的 `request_id`,便于后续和响应头、envelope 元信息串联。
|
||||
|
||||
当前 response envelope 约定:
|
||||
|
||||
1. `RequestContext` 已记录 `request_id`、请求开始时间、默认 `operation` 与 envelope 协商结果。
|
||||
2. `json_success_body(...)` / `json_error_body(...)` 会根据 `x-genarrative-response-envelope` 自动在“裸数据 / 标准 envelope / legacy error + meta”之间切换。
|
||||
3. `meta.apiVersion`、`meta.requestId`、`meta.routeVersion`、`meta.operation`、`meta.latencyMs`、`meta.timestamp` 已按当前前端契约生成,响应头回写仍留给后续独立任务。
|
||||
|
||||
当前基础响应头约定:
|
||||
|
||||
1. 所有响应都会回写 `x-request-id`。
|
||||
2. 所有响应都会回写固定的 `x-api-version`,当前值与 body `meta.apiVersion` 保持一致。
|
||||
3. 所有响应都会回写 `x-route-version`,当前阶段默认与 `x-api-version` 保持一致,后续再按路由粒度细分。
|
||||
4. 所有响应都会回写 `x-response-time-ms`,值来源于 `RequestContext` 内记录的请求开始时间。
|
||||
|
||||
当前 `/healthz` 约定:
|
||||
|
||||
1. 路径固定为 `/healthz`。
|
||||
2. 裸响应继续返回 `{ ok: true, service: "genarrative-node-server" }`,保持与当前 Node 工程兼容。
|
||||
3. 当请求携带 `x-genarrative-response-envelope` 时,`/healthz` 会返回标准 success envelope。
|
||||
4. `x-request-id`、`x-api-version`、`x-route-version`、`x-response-time-ms` 会在 `/healthz` 响应中一并回写。
|
||||
|
||||
当前本地检查链路约定:
|
||||
|
||||
1. `../../scripts/check.ps1` 与 `../../scripts/check.sh` 统一串联 `cargo fmt --all --check`、`cargo clippy`、`cargo check`、`cargo test`。
|
||||
2. 默认检查整个 `server-rs` workspace,确保后续多 crate 扩容时仍然保持统一口径。
|
||||
3. 当只需聚焦单个 crate 时,可通过 `-Package` 或 `SERVER_RS_CHECK_PACKAGE` 收窄 `clippy / check / test` 目标。
|
||||
4. `cargo fmt --all --check` 仍固定覆盖整个 workspace,避免多 crate 下格式基线漂移。
|
||||
|
||||
当前本地 smoke 链路约定:
|
||||
|
||||
1. `../../scripts/smoke.ps1` 与 `../../scripts/smoke.sh` 会先构建 `api-server`,再拉起临时本地进程完成冒烟验证。
|
||||
2. smoke 当前固定校验 `/healthz` 的 raw 响应、envelope 响应以及 `x-request-id`、`x-api-version`、`x-route-version`、`x-response-time-ms` 头。
|
||||
3. smoke 通过后,可作为“Axum 服务可独立启动且基础 contract 可联通”的本地自动化证据。
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `api-server` 负责 HTTP、SSE、Cookie、Header、路由与协议装配。
|
||||
2. 业务逻辑优先通过独立模块 crate 暴露能力,再由主工程组合。
|
||||
3. 外部副作用通过 `platform-auth`、`platform-oss`、`platform-llm` 与各模块 crate 的应用层完成。
|
||||
4. 不把领域规则直接堆在 handler 中。
|
||||
141
server-rs/crates/api-server/src/api_response.rs
Normal file
141
server-rs/crates/api-server/src/api_response.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
|
||||
use crate::{http_error::ApiErrorPayload, request_context::RequestContext};
|
||||
|
||||
pub const API_VERSION: &str = "2026-04-08";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ApiResponseMeta {
|
||||
#[serde(rename = "apiVersion")]
|
||||
api_version: &'static str,
|
||||
#[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
|
||||
request_id: Option<String>,
|
||||
#[serde(rename = "routeVersion")]
|
||||
route_version: &'static str,
|
||||
operation: Option<String>,
|
||||
#[serde(rename = "latencyMs")]
|
||||
latency_ms: u64,
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
// 当前阶段先把成功响应 envelope helper 准备好,后续 `/healthz` 与业务 handler 会直接复用这里的输出逻辑。
|
||||
#[allow(dead_code)]
|
||||
pub fn json_success_body<T>(request_context: Option<&RequestContext>, data: T) -> Json<Value>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
if let Some(context) = request_context {
|
||||
if context.wants_envelope() {
|
||||
return Json(json!({
|
||||
"ok": true,
|
||||
"data": data,
|
||||
"error": null,
|
||||
"meta": build_api_response_meta(Some(context)),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Json(serde_json::to_value(data).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
pub fn json_error_body(
|
||||
request_context: Option<&RequestContext>,
|
||||
error: &ApiErrorPayload,
|
||||
) -> Json<Value> {
|
||||
let meta = build_api_response_meta(request_context);
|
||||
|
||||
if request_context.is_some_and(RequestContext::wants_envelope) {
|
||||
return Json(json!({
|
||||
"ok": false,
|
||||
"data": null,
|
||||
"error": error,
|
||||
"meta": meta,
|
||||
}));
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
"error": error,
|
||||
"meta": meta,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_api_response_meta(request_context: Option<&RequestContext>) -> ApiResponseMeta {
|
||||
ApiResponseMeta {
|
||||
api_version: API_VERSION,
|
||||
request_id: request_context.map(|context| context.request_id().to_string()),
|
||||
route_version: API_VERSION,
|
||||
operation: request_context.map(|context| context.operation().to_string()),
|
||||
latency_ms: request_context
|
||||
.map(RequestContext::elapsed)
|
||||
.unwrap_or_default(),
|
||||
timestamp: OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::request_context::RequestContext;
|
||||
use std::time::Duration;
|
||||
|
||||
fn build_request_context(wants_envelope: bool) -> RequestContext {
|
||||
RequestContext::new(
|
||||
"req-test".to_string(),
|
||||
"GET /test".to_string(),
|
||||
Duration::from_millis(12),
|
||||
wants_envelope,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_body_returns_standard_envelope_when_requested() {
|
||||
let request_context = build_request_context(true);
|
||||
let body = json_success_body(Some(&request_context), json!({ "ok": "value" })).0;
|
||||
|
||||
assert_eq!(body["ok"], Value::Bool(true));
|
||||
assert_eq!(body["data"]["ok"], Value::String("value".to_string()));
|
||||
assert_eq!(
|
||||
body["meta"]["requestId"],
|
||||
Value::String("req-test".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
body["meta"]["routeVersion"],
|
||||
Value::String(API_VERSION.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_body_returns_raw_payload_when_envelope_not_requested() {
|
||||
let request_context = build_request_context(false);
|
||||
let body = json_success_body(Some(&request_context), json!({ "ok": "value" })).0;
|
||||
|
||||
assert_eq!(body["ok"], Value::String("value".to_string()));
|
||||
assert!(body.get("meta").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_body_returns_legacy_shape_without_envelope_header() {
|
||||
let request_context = build_request_context(false);
|
||||
let error = ApiErrorPayload {
|
||||
code: "NOT_FOUND",
|
||||
message: "资源不存在",
|
||||
details: None,
|
||||
};
|
||||
let body = json_error_body(Some(&request_context), &error).0;
|
||||
|
||||
assert_eq!(
|
||||
body["error"]["code"],
|
||||
Value::String("NOT_FOUND".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
body["meta"]["requestId"],
|
||||
Value::String("req-test".to_string())
|
||||
);
|
||||
assert!(body.get("ok").is_none());
|
||||
}
|
||||
}
|
||||
155
server-rs/crates/api-server/src/app.rs
Normal file
155
server-rs/crates/api-server/src/app.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use axum::{body::Body, extract::Extension, http::Request, middleware, routing::get, Router};
|
||||
use tower_http::trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer};
|
||||
use tracing::{info_span, Level};
|
||||
|
||||
use crate::{
|
||||
error_middleware::normalize_error_response,
|
||||
health::health_check,
|
||||
request_context::{attach_request_context, resolve_request_id},
|
||||
response_headers::propagate_request_id_header,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
|
||||
pub fn build_router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/healthz",
|
||||
get(|Extension(request_context): Extension<_>| async move {
|
||||
health_check(Extension(request_context)).await
|
||||
}),
|
||||
)
|
||||
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
|
||||
.layer(middleware::from_fn(normalize_error_response))
|
||||
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
|
||||
.layer(middleware::from_fn(propagate_request_id_header))
|
||||
// 当前阶段先统一挂接 HTTP tracing,后续 request_id、响应头与错误中间件继续在这里扩展。
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &Request<Body>| {
|
||||
let request_id =
|
||||
resolve_request_id(request).unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
info_span!(
|
||||
"http.request",
|
||||
method = %request.method(),
|
||||
uri = %request.uri(),
|
||||
request_id = %request_id,
|
||||
)
|
||||
})
|
||||
.on_request(DefaultOnRequest::new().level(Level::INFO))
|
||||
.on_response(DefaultOnResponse::new().level(Level::INFO))
|
||||
.on_failure(DefaultOnFailure::new().level(Level::ERROR)),
|
||||
)
|
||||
// request_id 中间件先进入请求链,确保后续 tracing、错误处理和响应头层都能复用同一份请求标识。
|
||||
.layer(middleware::from_fn(attach_request_context))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
use serde_json::Value;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use crate::{config::AppConfig, state::AppState};
|
||||
|
||||
use super::build_router;
|
||||
|
||||
#[tokio::test]
|
||||
async fn healthz_returns_legacy_compatible_payload_and_headers() {
|
||||
let app = build_router(AppState::new(AppConfig::default()));
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/healthz")
|
||||
.header("x-request-id", "req-health-legacy")
|
||||
.body(Body::empty())
|
||||
.expect("healthz request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("healthz request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-request-id")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("req-health-legacy")
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-api-version")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("2026-04-08")
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-route-version")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("2026-04-08")
|
||||
);
|
||||
assert!(response.headers().contains_key("x-response-time-ms"));
|
||||
|
||||
let body = response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("healthz body should collect")
|
||||
.to_bytes();
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("healthz body should be valid json");
|
||||
|
||||
assert_eq!(payload["ok"], Value::Bool(true));
|
||||
assert_eq!(
|
||||
payload["service"],
|
||||
Value::String("genarrative-node-server".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn healthz_returns_standard_envelope_when_requested() {
|
||||
let app = build_router(AppState::new(AppConfig::default()));
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/healthz")
|
||||
.header("x-request-id", "req-health-envelope")
|
||||
.header("x-genarrative-response-envelope", "v1")
|
||||
.body(Body::empty())
|
||||
.expect("healthz request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("healthz request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("healthz body should collect")
|
||||
.to_bytes();
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("healthz body should be valid json");
|
||||
|
||||
assert_eq!(payload["ok"], Value::Bool(true));
|
||||
assert_eq!(
|
||||
payload["data"]["service"],
|
||||
Value::String("genarrative-node-server".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["meta"]["requestId"],
|
||||
Value::String("req-health-envelope".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
52
server-rs/crates/api-server/src/config.rs
Normal file
52
server-rs/crates/api-server/src/config.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::{env, net::SocketAddr};
|
||||
|
||||
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppConfig {
|
||||
pub bind_host: String,
|
||||
pub bind_port: u16,
|
||||
pub log_filter: String,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bind_host: "127.0.0.1".to_string(),
|
||||
bind_port: 3000,
|
||||
log_filter: "info,tower_http=info".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn from_env() -> Self {
|
||||
let mut config = Self::default();
|
||||
|
||||
if let Ok(bind_host) = env::var("GENARRATIVE_API_HOST") {
|
||||
if !bind_host.trim().is_empty() {
|
||||
config.bind_host = bind_host;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(bind_port) = env::var("GENARRATIVE_API_PORT") {
|
||||
if let Ok(parsed_port) = bind_port.parse::<u16>() {
|
||||
config.bind_port = parsed_port;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(log_filter) = env::var("GENARRATIVE_API_LOG") {
|
||||
if !log_filter.trim().is_empty() {
|
||||
config.log_filter = log_filter;
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
pub fn bind_socket_addr(&self) -> SocketAddr {
|
||||
let address = format!("{}:{}", self.bind_host, self.bind_port);
|
||||
address
|
||||
.parse()
|
||||
.unwrap_or_else(|_| SocketAddr::from(([127, 0, 0, 1], 3000)))
|
||||
}
|
||||
}
|
||||
50
server-rs/crates/api-server/src/error_middleware.rs
Normal file
50
server-rs/crates/api-server/src/error_middleware.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use axum::{extract::Request, http::header::CONTENT_TYPE, middleware::Next, response::Response};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::{
|
||||
http_error::AppError,
|
||||
request_context::{resolve_request_id, RequestContext},
|
||||
};
|
||||
|
||||
pub async fn normalize_error_response(request: Request, next: Next) -> Response {
|
||||
let request_id = resolve_request_id(&request);
|
||||
let request_context = request.extensions().get::<RequestContext>().cloned();
|
||||
let response = next.run(request).await;
|
||||
|
||||
if !should_normalize_error_response(&response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let app_error = AppError::from_status(response.status());
|
||||
let status = response.status();
|
||||
let request_id = request_id.as_deref().unwrap_or("unknown");
|
||||
|
||||
if status.is_server_error() {
|
||||
error!(
|
||||
%request_id,
|
||||
status = status.as_u16(),
|
||||
error_code = app_error.code(),
|
||||
"request failed"
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
%request_id,
|
||||
status = status.as_u16(),
|
||||
error_code = app_error.code(),
|
||||
"request failed"
|
||||
);
|
||||
}
|
||||
|
||||
app_error.into_response_with_context(request_context.as_ref())
|
||||
}
|
||||
|
||||
fn should_normalize_error_response(response: &Response) -> bool {
|
||||
let status = response.status();
|
||||
|
||||
if !(status.is_client_error() || status.is_server_error()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 当前阶段只兜底框架默认的空错误响应或非 JSON 错误响应,避免覆盖后续业务 handler 主动构造的错误 body。
|
||||
!response.headers().contains_key(CONTENT_TYPE)
|
||||
}
|
||||
14
server-rs/crates/api-server/src/health.rs
Normal file
14
server-rs/crates/api-server/src/health.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use axum::{extract::Extension, Json};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{api_response::json_success_body, request_context::RequestContext};
|
||||
|
||||
pub async fn health_check(Extension(request_context): Extension<RequestContext>) -> Json<Value> {
|
||||
json_success_body(
|
||||
Some(&request_context),
|
||||
json!({
|
||||
"ok": true,
|
||||
"service": "genarrative-node-server",
|
||||
}),
|
||||
)
|
||||
}
|
||||
76
server-rs/crates/api-server/src/http_error.rs
Normal file
76
server-rs/crates/api-server/src/http_error.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{api_response::json_error_body, request_context::RequestContext};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppError {
|
||||
status_code: StatusCode,
|
||||
code: &'static str,
|
||||
message: &'static str,
|
||||
details: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ApiErrorPayload {
|
||||
pub code: &'static str,
|
||||
pub message: &'static str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub details: Option<Value>,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn from_status(status_code: StatusCode) -> Self {
|
||||
let (code, message) = resolve_http_error(status_code);
|
||||
|
||||
Self {
|
||||
status_code,
|
||||
code,
|
||||
message,
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code(&self) -> &'static str {
|
||||
self.code
|
||||
}
|
||||
|
||||
pub fn into_response_with_context(self, request_context: Option<&RequestContext>) -> Response {
|
||||
let status_code = self.status_code;
|
||||
let payload = self.to_payload();
|
||||
|
||||
(status_code, json_error_body(request_context, &payload)).into_response()
|
||||
}
|
||||
|
||||
fn to_payload(&self) -> ApiErrorPayload {
|
||||
ApiErrorPayload {
|
||||
code: self.code,
|
||||
message: self.message,
|
||||
details: self.details.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
self.into_response_with_context(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_http_error(status_code: StatusCode) -> (&'static str, &'static str) {
|
||||
match status_code {
|
||||
StatusCode::BAD_REQUEST => ("BAD_REQUEST", "请求参数不合法"),
|
||||
StatusCode::UNAUTHORIZED => ("UNAUTHORIZED", "未授权访问"),
|
||||
StatusCode::FORBIDDEN => ("FORBIDDEN", "禁止访问"),
|
||||
StatusCode::NOT_FOUND => ("NOT_FOUND", "资源不存在"),
|
||||
StatusCode::CONFLICT => ("CONFLICT", "请求冲突"),
|
||||
StatusCode::TOO_MANY_REQUESTS => ("TOO_MANY_REQUESTS", "请求过于频繁"),
|
||||
StatusCode::BAD_GATEWAY => ("UPSTREAM_ERROR", "上游服务请求失败"),
|
||||
_ if status_code.is_client_error() => ("BAD_REQUEST", "请求参数不合法"),
|
||||
_ => ("INTERNAL_SERVER_ERROR", "服务器内部错误"),
|
||||
}
|
||||
}
|
||||
19
server-rs/crates/api-server/src/logging.rs
Normal file
19
server-rs/crates/api-server/src/logging.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use std::io;
|
||||
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
|
||||
// 统一在独立模块初始化 tracing,避免入口层和后续测试入口重复散落 subscriber 配置。
|
||||
pub fn init_tracing(config: &AppConfig) -> Result<(), io::Error> {
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new(config.log_filter.as_str()))
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
fmt()
|
||||
.with_env_filter(env_filter)
|
||||
.with_target(true)
|
||||
.compact()
|
||||
.try_init()
|
||||
.map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}")))
|
||||
}
|
||||
32
server-rs/crates/api-server/src/main.rs
Normal file
32
server-rs/crates/api-server/src/main.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
mod api_response;
|
||||
mod app;
|
||||
mod config;
|
||||
mod error_middleware;
|
||||
mod health;
|
||||
mod http_error;
|
||||
mod logging;
|
||||
mod request_context;
|
||||
mod response_headers;
|
||||
mod state;
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{app::build_router, config::AppConfig, logging::init_tracing, state::AppState};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
// 统一先从配置对象读取监听地址,避免后续把环境变量读取散落到入口和路由层。
|
||||
let config = AppConfig::from_env();
|
||||
init_tracing(&config)?;
|
||||
|
||||
let bind_address = config.bind_socket_addr();
|
||||
let listener = TcpListener::bind(bind_address).await?;
|
||||
|
||||
let state = AppState::new(config);
|
||||
let router = build_router(state);
|
||||
|
||||
info!(%bind_address, "api-server 已完成 tracing 初始化并开始监听");
|
||||
|
||||
axum::serve(listener, router).await
|
||||
}
|
||||
113
server-rs/crates/api-server/src/request_context.rs
Normal file
113
server-rs/crates/api-server/src/request_context.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use axum::{
|
||||
extract::Request,
|
||||
http::{header::HeaderName, HeaderValue, Request as HttpRequest},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const API_RESPONSE_ENVELOPE_HEADER: &str = "x-genarrative-response-envelope";
|
||||
pub const X_REQUEST_ID_HEADER: &str = "x-request-id";
|
||||
|
||||
// 当前阶段先把请求级元信息统一挂到 extensions,后续响应头、envelope 与错误处理中间件继续复用。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequestContext {
|
||||
request_id: String,
|
||||
operation: String,
|
||||
request_started_at: Instant,
|
||||
wants_envelope: bool,
|
||||
}
|
||||
|
||||
impl RequestContext {
|
||||
pub fn new(
|
||||
request_id: String,
|
||||
operation: String,
|
||||
elapsed_seed: Duration,
|
||||
wants_envelope: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
request_id,
|
||||
operation,
|
||||
request_started_at: Instant::now()
|
||||
.checked_sub(elapsed_seed)
|
||||
.unwrap_or_else(Instant::now),
|
||||
wants_envelope,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_id(&self) -> &str {
|
||||
&self.request_id
|
||||
}
|
||||
|
||||
pub fn operation(&self) -> &str {
|
||||
&self.operation
|
||||
}
|
||||
|
||||
pub fn wants_envelope(&self) -> bool {
|
||||
self.wants_envelope
|
||||
}
|
||||
|
||||
pub fn elapsed(&self) -> u64 {
|
||||
self.request_started_at
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
.min(u64::MAX as u128) as u64
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn attach_request_context(mut request: Request, next: Next) -> Response {
|
||||
let wants_envelope = wants_api_envelope(&request);
|
||||
let request_id = request
|
||||
.headers()
|
||||
.get(X_REQUEST_ID_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
let operation = format!("{} {}", request.method(), request.uri());
|
||||
|
||||
request.extensions_mut().insert(RequestContext::new(
|
||||
request_id.clone(),
|
||||
operation,
|
||||
Duration::ZERO,
|
||||
wants_envelope,
|
||||
));
|
||||
|
||||
// 统一把 request_id 写回请求头,方便后续 tracing、响应头与 envelope 层读取同一来源。
|
||||
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(HeaderName::from_static(X_REQUEST_ID_HEADER), header_value);
|
||||
}
|
||||
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
pub fn resolve_request_id<B>(request: &HttpRequest<B>) -> Option<String> {
|
||||
request
|
||||
.extensions()
|
||||
.get::<RequestContext>()
|
||||
.map(|context| context.request_id().to_string())
|
||||
.or_else(|| {
|
||||
request
|
||||
.headers()
|
||||
.get(X_REQUEST_ID_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
}
|
||||
|
||||
fn wants_api_envelope<B>(request: &HttpRequest<B>) -> bool {
|
||||
request
|
||||
.headers()
|
||||
.get(API_RESPONSE_ENVELOPE_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::trim)
|
||||
.map(str::to_lowercase)
|
||||
.is_some_and(|value| matches!(value.as_str(), "1" | "true" | "v1" | "envelope"))
|
||||
}
|
||||
49
server-rs/crates/api-server/src/response_headers.rs
Normal file
49
server-rs/crates/api-server/src/response_headers.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use axum::{
|
||||
extract::Request,
|
||||
http::{header::HeaderName, HeaderValue},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api_response::API_VERSION,
|
||||
request_context::{resolve_request_id, RequestContext, X_REQUEST_ID_HEADER},
|
||||
};
|
||||
|
||||
pub const API_VERSION_HEADER: &str = "x-api-version";
|
||||
pub const RESPONSE_TIME_HEADER: &str = "x-response-time-ms";
|
||||
pub const ROUTE_VERSION_HEADER: &str = "x-route-version";
|
||||
|
||||
pub async fn propagate_request_id_header(request: Request, next: Next) -> Response {
|
||||
let request_id = resolve_request_id(&request);
|
||||
let request_context = request.extensions().get::<RequestContext>().cloned();
|
||||
let mut response = next.run(request).await;
|
||||
|
||||
if let Some(request_id) = request_id {
|
||||
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(HeaderName::from_static(X_REQUEST_ID_HEADER), header_value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(header_value) = HeaderValue::from_str(API_VERSION) {
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static(API_VERSION_HEADER),
|
||||
header_value.clone(),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(HeaderName::from_static(ROUTE_VERSION_HEADER), header_value);
|
||||
}
|
||||
|
||||
if let Some(request_context) = request_context {
|
||||
if let Ok(header_value) = HeaderValue::from_str(&request_context.elapsed().to_string()) {
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(HeaderName::from_static(RESPONSE_TIME_HEADER), header_value);
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
15
server-rs/crates/api-server/src/state.rs
Normal file
15
server-rs/crates/api-server/src/state.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::config::AppConfig;
|
||||
|
||||
// 当前阶段先保留最小共享状态壳,后续逐步接入配置、客户端与平台适配。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppState {
|
||||
// 配置会在后续中间件、路由和平台适配接入时逐步消费。
|
||||
#[allow(dead_code)]
|
||||
pub config: AppConfig,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(config: AppConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
29
server-rs/crates/module-ai/README.md
Normal file
29
server-rs/crates/module-ai/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# module-ai 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-ai` 是 AI 编排模块 package,后续负责:
|
||||
|
||||
1. 剧情、聊天、自定义世界、运行时物品等生成型流程的模块级编排
|
||||
2. prompt 组织、阶段状态、结果引用与模块间协同
|
||||
3. 与 `apps/api-server` 的流式输出与兼容接口对接
|
||||
4. 与 `apps/spacetime-module` 的任务状态、结果引用聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入模型调用、流式编排与结果回写实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计多模型编排与任务状态抽象
|
||||
2. 对齐剧情、聊天、自定义世界等生成链路
|
||||
3. 对齐流式输出、阶段事件与兼容响应结构
|
||||
4. 接入模块级结果回写与任务引用绑定
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-ai` 负责生成型流程的模块级编排,不把供应商 SDK 直接散落到各业务模块里。
|
||||
2. 实际模型接入通过 `packages/platform-llm` 完成,状态与结果引用最终回写到 `apps/spacetime-module` 聚合的状态模型中。
|
||||
3. 前端兼容 REST 与 SSE 由 `apps/api-server` 暴露,但 AI 编排过程不能再次退回单个大 orchestrator 的黑盒写法。
|
||||
30
server-rs/crates/module-assets/README.md
Normal file
30
server-rs/crates/module-assets/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# module-assets 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-assets` 是资产任务与对象绑定模块 package,后续负责:
|
||||
|
||||
1. `asset_job`、`asset_object`、`asset_manifest` 等资产状态模型
|
||||
2. 角色形象、动作、Qwen 精灵表、场景图、封面图等资产任务编排
|
||||
3. 业务实体与 OSS 对象的绑定关系
|
||||
4. 与 `apps/api-server` 的 assets 兼容接口对接
|
||||
5. 与 `apps/spacetime-module` 的资产表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入生成链路、对象确认与兼容接口实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `asset_job`、`asset_object`、`asset_manifest`
|
||||
2. 设计角色、动作、场景、精灵表相关资产表
|
||||
3. 对齐资产生成、发布、对象确认与兼容接口链路
|
||||
4. 接入 OSS 对象写入与绑定编排
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-assets` 负责资产任务状态、对象引用关系与模块级编排,不把二进制对象本身放回本地持久化目录真相中。
|
||||
2. OSS 上传、签名、对象读写等副作用通过平台适配完成,状态最终回写到 `apps/spacetime-module` 聚合的状态模型中。
|
||||
3. 前端兼容接口由 `apps/api-server` 暴露,但资产任务状态与对象绑定关系不能再次散落到本地文件判断逻辑里。
|
||||
38
server-rs/crates/module-auth/README.md
Normal file
38
server-rs/crates/module-auth/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# module-auth 独立模块 crate 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. crate 职责
|
||||
|
||||
`module-auth` 是鉴权与会话模块 crate,后续负责:
|
||||
|
||||
1. 用户身份、会话、风控、审计相关领域模型
|
||||
2. 手机验证码、微信登录、密码登录的模块内用例编排
|
||||
3. 与 `crates/api-server` 的鉴权接口装配对接
|
||||
4. 与 `crates/spacetime-module` 的身份表、会话表聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前阶段已冻结前六张鉴权基础表设计,剩余 `wechat_auth_state` 与 token 细节仍按顺序继续展开。
|
||||
|
||||
后续与本 crate 直接相关的任务包括:
|
||||
|
||||
1. 设计 `user_account`、`auth_identity`、`refresh_session`
|
||||
2. 设计 `auth_audit_log`、`auth_risk_block`
|
||||
3. 设计 `sms_auth_event`、`wechat_auth_state`
|
||||
4. 落地 JWT claims、refresh cookie 与旧接口兼容
|
||||
|
||||
当前已冻结文档:
|
||||
|
||||
1. [../../../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)
|
||||
2. [../../../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md)
|
||||
3. [../../../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)
|
||||
4. [../../../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md)
|
||||
5. [../../../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md)
|
||||
6. [../../../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md)
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-auth` 负责鉴权领域规则与模块级编排,不直接把供应商 SDK 逻辑写进主工程。
|
||||
2. 短信、微信、JWT、Cookie 等平台适配优先通过 `crates/platform-auth` 承接。
|
||||
3. 身份与会话状态最终由 `crates/spacetime-module` 聚合,前端接口由 `crates/api-server` 暴露。
|
||||
29
server-rs/crates/module-combat/README.md
Normal file
29
server-rs/crates/module-combat/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# module-combat 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-combat` 是战斗规则模块 package,后续负责:
|
||||
|
||||
1. `battle_state` 等战斗状态模型
|
||||
2. 战斗指令、伤害结算、战斗阶段推进规则
|
||||
3. 与 story action 主循环的战斗联动
|
||||
4. 与 `apps/spacetime-module` 的战斗表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入具体战斗规则与数值实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `battle_state`
|
||||
2. 设计 `resolve_combat_action`
|
||||
3. 对齐 battle 结果与兼容响应结构
|
||||
4. 接入 story 主循环的战斗型 action 结算
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-combat` 保持纯规则、纯状态计算,不直接承接 HTTP、LLM、OSS 或其他外部副作用。
|
||||
2. 战斗联动通过明确 reducer 与模块边界协作,不回到散落在多个 service 的过程式写法。
|
||||
3. 前端兼容输出由 `apps/api-server` 暴露,战斗真相由 `apps/spacetime-module` 聚合。
|
||||
30
server-rs/crates/module-custom-world/README.md
Normal file
30
server-rs/crates/module-custom-world/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# module-custom-world 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-custom-world` 是自定义世界与 agent 模块 package,后续负责:
|
||||
|
||||
1. `custom_world_profile`、`custom_world_session` 等世界状态模型
|
||||
2. `custom_world_agent_session`、消息、操作、草稿卡等 agent 状态模型
|
||||
3. 传统问答流、library、gallery、agent 会话的模块级编排
|
||||
4. 与 `apps/api-server` 的 custom world 兼容接口与 SSE 对接
|
||||
5. 与 `apps/spacetime-module` 的 custom world 表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入问答流、agent 流、世界编译与资产绑定实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `custom_world_profile`、`custom_world_session`
|
||||
2. 设计 `custom_world_agent_session`、消息、操作、卡片相关表
|
||||
3. 对齐 traditional custom world、library、gallery、agent 兼容链路
|
||||
4. 接入世界编译、场景图、封面图与角色资产的模块级编排
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-custom-world` 负责世界状态真相、agent 状态与模块级编排,不把整个会话重新塞回单大 JSON 体。
|
||||
2. 外部 LLM、图片生成、OSS 写入等副作用通过平台适配和应用层完成,状态最终回写到 `apps/spacetime-module` 聚合的状态模型中。
|
||||
3. 前端兼容 REST 与 SSE 由 `apps/api-server` 暴露,但自定义世界主链状态不能再次分散到本地 session store 或前端临时状态中。
|
||||
29
server-rs/crates/module-inventory/README.md
Normal file
29
server-rs/crates/module-inventory/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# module-inventory 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-inventory` 是背包与物品变更模块 package,后续负责:
|
||||
|
||||
1. `inventory_slot` 等背包状态模型
|
||||
2. 物品获得、消耗、赠礼、背包变更规则
|
||||
3. 与 story action、runtime item、NPC 交互的背包联动
|
||||
4. 与 `apps/spacetime-module` 的背包表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入具体背包规则与读模型实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `inventory_slot`
|
||||
2. 设计 `apply_inventory_mutation`
|
||||
3. 对齐背包 patch、奖励结果与兼容响应结构
|
||||
4. 接入 story action 主循环的背包联动
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-inventory` 负责物品状态真相与背包规则,不把外部 AI、OSS 或 HTTP 协议塞进模块内部。
|
||||
2. 与 `module-story`、`module-runtime-item`、`module-npc` 的协作通过明确 reducer 或投影边界完成。
|
||||
3. 前端兼容输出由 `apps/api-server` 暴露,背包状态真相由 `apps/spacetime-module` 聚合。
|
||||
30
server-rs/crates/module-npc/README.md
Normal file
30
server-rs/crates/module-npc/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# module-npc 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-npc` 是 NPC 状态与互动模块 package,后续负责:
|
||||
|
||||
1. `npc_state` 等 NPC 关系与状态模型
|
||||
2. 招募、关系变化、互动规则与场景语义状态
|
||||
3. 与 story action、runtime、custom world 的 NPC 联动
|
||||
4. 与 `apps/api-server` 的 NPC 相关 facade 与流式交互对接
|
||||
5. 与 `apps/spacetime-module` 的 NPC 表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入对话生成、状态投影与交互规则实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `npc_state`
|
||||
2. 设计 `resolve_npc_interaction`
|
||||
3. 对齐 NPC 关系变化、招募、对话相关兼容输出
|
||||
4. 接入 story 主循环与 custom world 的 NPC 联动
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-npc` 负责 NPC 状态真相与互动规则,外部 LLM 台词生成与流式文本输出不直接塞进模块内部。
|
||||
2. 对话与招募文案生成优先通过对应模块应用层和平台适配完成,NPC 状态最终回写到 `apps/spacetime-module` 聚合的状态模型中。
|
||||
3. 前端兼容接口与 SSE 由 `apps/api-server` 暴露,但 NPC 状态不能再次分散到会话缓存或前端临时状态中。
|
||||
29
server-rs/crates/module-progression/README.md
Normal file
29
server-rs/crates/module-progression/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# module-progression 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-progression` 是成长与章节推进模块 package,后续负责:
|
||||
|
||||
1. `player_progression`、`chapter_progression` 等成长状态模型
|
||||
2. 等级、章节推进、敌对强度与进程规则
|
||||
3. 与 runtime、story、quest 的成长联动
|
||||
4. 与 `apps/spacetime-module` 的成长表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入成长规则、投影与兼容接口实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `player_progression`、`chapter_progression`
|
||||
2. 设计 `update_progression_state`
|
||||
3. 对齐章节推进、成长变化与兼容输出结构
|
||||
4. 接入 runtime 与 story 的成长联动
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-progression` 保持纯领域规则与状态建模,不直接承接 LLM、OSS 或 HTTP 协议。
|
||||
2. 成长状态作为 runtime 与 story 的公共领域组件,不能再次散落回单个 handler 或临时 service 中。
|
||||
3. 前端兼容输出由 `apps/api-server` 暴露,成长状态真相由 `apps/spacetime-module` 聚合。
|
||||
30
server-rs/crates/module-quest/README.md
Normal file
30
server-rs/crates/module-quest/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# module-quest 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-quest` 是任务运行时模块 package,后续负责:
|
||||
|
||||
1. `quest_record` 等任务状态模型
|
||||
2. 任务进度、任务日志、任务信号处理规则
|
||||
3. 与 story、runtime、progression 的任务联动
|
||||
4. 与 `apps/api-server` 的任务兼容接口对接
|
||||
5. 与 `apps/spacetime-module` 的任务表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入任务草案、进度投影与兼容接口实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `quest_record`
|
||||
2. 设计 `apply_quest_signal`
|
||||
3. 对齐任务进度、日志与兼容输出结构
|
||||
4. 接入 runtime 与 story 的任务联动
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-quest` 负责任务状态真相与任务规则,生成型任务草案与外部 AI 编排不直接塞进模块内部。
|
||||
2. 任务状态最终回写到 `apps/spacetime-module` 聚合的状态模型中,前端兼容接口由 `apps/api-server` 暴露。
|
||||
3. 任务不能再次散落到 story service、runtime service 或前端临时状态里分别维护。
|
||||
30
server-rs/crates/module-runtime-item/README.md
Normal file
30
server-rs/crates/module-runtime-item/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# module-runtime-item 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-runtime-item` 是运行时物品模块 package,后续负责:
|
||||
|
||||
1. `treasure_record` 等运行时物品与宝藏状态模型
|
||||
2. 奖励解析、宝藏逻辑、运行时物品结算规则
|
||||
3. 与 story、inventory、quest 的运行时物品联动
|
||||
4. 与 `apps/api-server` 的运行时物品兼容接口对接
|
||||
5. 与 `apps/spacetime-module` 的运行时物品表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入奖励解析、意图生成与兼容接口实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `treasure_record`
|
||||
2. 设计运行时物品结算与宝藏交互 reducer
|
||||
3. 对齐奖励、宝藏、patch 与兼容输出结构
|
||||
4. 接入 story action 主循环的运行时物品联动
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-runtime-item` 负责运行时物品状态真相与奖励规则,生成型物品意图与外部 AI 编排不直接塞进模块内部。
|
||||
2. 奖励与宝藏状态最终回写到 `apps/spacetime-module` 聚合的状态模型中,前端兼容接口由 `apps/api-server` 暴露。
|
||||
3. 运行时物品逻辑不能再次散落到 story、inventory 或 route handler 中分别维护。
|
||||
30
server-rs/crates/module-runtime/README.md
Normal file
30
server-rs/crates/module-runtime/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# module-runtime 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-runtime` 是运行时状态基座模块 package,后续负责:
|
||||
|
||||
1. `runtime_snapshot`、`runtime_setting` 等主状态模型
|
||||
2. profile dashboard、browse history、save archive 相关读写模型
|
||||
3. 运行时状态归一化与兼容聚合快照策略
|
||||
4. 与 `apps/api-server` 的 runtime facade 对接
|
||||
5. 与 `apps/spacetime-module` 的运行时表与 view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入表结构、projection 与接口实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `runtime_snapshot`、`runtime_setting`
|
||||
2. 设计 `profile_dashboard_state`、`profile_wallet_ledger`
|
||||
3. 设计 `profile_played_world`、`profile_save_archive`、`user_browse_history`
|
||||
4. 落地存档、设置、资料页兼容接口
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-runtime` 负责运行时状态真相与模块级 facade 编排,不把主状态继续留在旧式大 JSON repository 中。
|
||||
2. 兼容快照可以作为聚合产物保留,但不能再次成为唯一真相。
|
||||
3. 前端兼容接口由 `apps/api-server` 暴露,状态表与 view 最终由 `apps/spacetime-module` 聚合。
|
||||
30
server-rs/crates/module-story/README.md
Normal file
30
server-rs/crates/module-story/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# module-story 独立模块 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`module-story` 是故事主循环模块 package,后续负责:
|
||||
|
||||
1. `story_session`、`story_event` 等故事会话状态模型
|
||||
2. story action 主循环与状态推进规则
|
||||
3. `currentStory`、story state、兼容视图模型的模块级拼装
|
||||
4. 与 `apps/api-server` 的 story facade 与 SSE 输出对接
|
||||
5. 与 `apps/spacetime-module` 的 story 表、reducer、view 聚合对接
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入 reducer、view 与 SSE 兼容实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 `story_session`、`story_event`
|
||||
2. 设计 `resolve_story_action`、`continue_story`、`begin_story_session`
|
||||
3. 对齐 `RuntimeStoryActionResponse`、`RuntimeStoryOptionView`
|
||||
4. 落地 `/api/runtime/story/*` 兼容链路
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `module-story` 负责故事状态真相与主循环规则,不把外部 LLM、OSS、短信、微信等副作用塞进模块内部。
|
||||
2. 流式文本输出与 HTTP 协议兼容由 `apps/api-server` 暴露,但阶段状态与故事真相必须回写到 `apps/spacetime-module` 聚合的状态模型中。
|
||||
3. 跨模块联动通过明确的 reducer 与模块边界协作,不回到单大 service 直接改整包 JSON 的旧实现方式。
|
||||
30
server-rs/crates/platform-auth/README.md
Normal file
30
server-rs/crates/platform-auth/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# platform-auth 平台适配 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`platform-auth` 是鉴权平台适配 package,后续负责:
|
||||
|
||||
1. JWT 签发与校验适配
|
||||
2. refresh cookie 读写与轮换适配
|
||||
3. 手机验证码发送与校验适配
|
||||
4. 微信 OAuth 相关平台适配
|
||||
5. 供 `module-auth` 与 `apps/api-server` 复用的鉴权基础设施能力
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入 JWT、Cookie、短信与微信平台实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 落地 JWT claims、签发与校验适配
|
||||
2. 落地 refresh cookie 读取、写入与轮换适配
|
||||
3. 落地短信发送、校验与风控适配
|
||||
4. 落地微信 OAuth start / callback 适配
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `platform-auth` 只承接平台适配,不承接 `module-auth` 的业务规则和状态真相。
|
||||
2. 鉴权状态最终由 `module-auth` 与 `apps/spacetime-module` 管理,前端接口由 `apps/api-server` 暴露。
|
||||
3. 不允许把短信、微信、Cookie、JWT 等外部细节重新散落到多个业务模块中各自实现。
|
||||
28
server-rs/crates/platform-llm/README.md
Normal file
28
server-rs/crates/platform-llm/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# platform-llm 平台适配 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`platform-llm` 是大模型平台适配 package,后续负责:
|
||||
|
||||
1. DashScope、Ark 与其他模型供应商适配
|
||||
2. 统一模型调用、流式输出、重试、超时与日志策略
|
||||
3. 供 `module-ai`、`module-story`、`module-npc`、`module-custom-world` 等模块复用的模型基础设施能力
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入具体模型 SDK、流式调用与供应商切换实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 落地统一模型请求与响应适配
|
||||
2. 落地流式文本输出与阶段事件适配
|
||||
3. 落地重试、超时、错误与日志策略
|
||||
4. 设计多供应商切换与能力分层
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `platform-llm` 只承接模型平台适配,不承接业务模块的状态真相与业务规则。
|
||||
2. 生成型状态与结果引用最终由业务模块和 `apps/spacetime-module` 管理,前端接口由 `apps/api-server` 暴露。
|
||||
3. 不允许把供应商 SDK、流式细节和重试策略重新散落到多个业务模块里各自实现。
|
||||
29
server-rs/crates/platform-oss/README.md
Normal file
29
server-rs/crates/platform-oss/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# platform-oss 平台适配 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`platform-oss` 是 OSS 平台适配 package,后续负责:
|
||||
|
||||
1. OSS 直传签名、STS、上传策略适配
|
||||
2. 对象上传、下载、签名 URL 与 `cdn_url` 解析适配
|
||||
3. 对象元数据、标签与内容 hash 适配
|
||||
4. 供 `module-assets`、`module-custom-world` 等模块复用的对象存储基础设施能力
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入 OSS SDK、上传策略与对象读写实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 落地 `PostObject`、STS、服务端上传适配
|
||||
2. 落地对象确认、签名 URL 与 CDN URL 解析适配
|
||||
3. 落地 `x-oss-meta-*` 元数据与对象标签适配
|
||||
4. 对齐旧 `/generated-*` 路径兼容策略
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `platform-oss` 只承接对象存储平台适配,不承接业务实体状态与业务规则。
|
||||
2. 资产状态与对象绑定最终由业务模块和 `apps/spacetime-module` 管理,前端接口由 `apps/api-server` 暴露。
|
||||
3. 不允许把 OSS SDK、签名逻辑和 URL 策略重新散落到多个业务模块里各自实现。
|
||||
29
server-rs/crates/shared-contracts/README.md
Normal file
29
server-rs/crates/shared-contracts/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# shared-contracts 共享 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`shared-contracts` 是前后端兼容 contract 共享 package,后续负责:
|
||||
|
||||
1. HTTP 请求与响应 DTO
|
||||
2. SSE 事件结构与事件名约定
|
||||
3. response envelope、错误结构、兼容头部契约
|
||||
4. 各模块对外暴露的共享协议类型
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入 DTO、事件与兼容结构实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 对齐现有前端直接依赖的响应头与 envelope
|
||||
2. 对齐 story、custom world、chat 等 SSE 事件结构
|
||||
3. 对齐 auth、runtime、assets 等兼容 DTO
|
||||
4. 为 breaking change 建立显式变更边界
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `shared-contracts` 只放协议类型与兼容结构,不承接业务规则、供应商适配或状态写入逻辑。
|
||||
2. 各模块 package 对外暴露的协议优先复用这里的共享定义,避免重复散落。
|
||||
3. 前端兼容契约一旦进入本 package,就必须与任务清单和基线文档同步维护。
|
||||
28
server-rs/crates/shared-kernel/README.md
Normal file
28
server-rs/crates/shared-kernel/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# shared-kernel 共享 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`shared-kernel` 是跨模块共享领域内核 package,后续负责:
|
||||
|
||||
1. 共享 ID、值对象、枚举与基础领域类型
|
||||
2. 共享时间、状态、版本、通用校验等基础规则
|
||||
3. 供各模块 package 复用的最小领域内核
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入具体共享类型与基础规则实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 统一用户、会话、世界、角色、资产等核心 ID 类型
|
||||
2. 统一时间戳、版本号、状态枚举等共享结构
|
||||
3. 抽取真正跨模块复用的最小领域规则
|
||||
4. 避免把模块私有规则错误上提到共享内核
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `shared-kernel` 只放跨模块最小共享内核,不承接具体业务模块的私有规则。
|
||||
2. 任何进入本 package 的类型都必须证明至少被多个模块稳定复用。
|
||||
3. 不能把主模块实现重新堆进共享内核,避免形成新的“大公共垃圾桶”。
|
||||
28
server-rs/crates/spacetime-client/README.md
Normal file
28
server-rs/crates/spacetime-client/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# spacetime-client 共享 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`spacetime-client` 是 SpacetimeDB 客户端适配 package,后续负责:
|
||||
|
||||
1. 生成 bindings 后的客户端访问封装
|
||||
2. Axum 与各模块对 reducer、view、订阅的调用适配
|
||||
3. 身份透传、连接配置与基础错误处理适配
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入 bindings 生成、调用封装与订阅实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计 bindings 生成与更新流程
|
||||
2. 设计 reducer、view、订阅的统一调用接口
|
||||
3. 设计身份透传与连接配置策略
|
||||
4. 设计 Axum / worker / 测试环境下的客户端复用方式
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `spacetime-client` 只承接 SpacetimeDB 客户端访问适配,不承接具体业务模块的规则实现。
|
||||
2. 业务状态真相仍由 `apps/spacetime-module` 管理,业务编排由各模块 package 与 `apps/api-server` 承担。
|
||||
3. 不允许把 reducer、view、订阅调用细节重新散落到多个业务模块里各自实现。
|
||||
37
server-rs/crates/spacetime-module/README.md
Normal file
37
server-rs/crates/spacetime-module/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# spacetime-module 主工程 crate 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. crate 职责
|
||||
|
||||
`spacetime-module` 是新后端的 SpacetimeDB 主工程 crate,后续负责:
|
||||
|
||||
1. 聚合各独立模块 crate 的表定义
|
||||
2. 聚合各独立模块 crate 的 reducer
|
||||
3. 聚合各独立模块 crate 的 view / 读模型
|
||||
4. 生成可发布的 SpacetimeDB wasm 模块
|
||||
5. 由 `../../scripts/spacetime-dev.ps1` 与 `../../scripts/spacetime-dev.sh` 驱动的本地 standalone 启动链路
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前阶段仍未进入具体 schema 与 reducer 实现,但已经补齐本地 standalone 启动脚本,先把 SpacetimeDB 进程入口固定下来。
|
||||
|
||||
后续与本 crate 直接相关的任务包括:
|
||||
|
||||
1. 建立模块聚合入口
|
||||
2. 设计表、reducer、view 的聚合方式
|
||||
3. 接入身份 claims 透传
|
||||
4. 在实体 module scaffold 落地后接入 publish / dev 循环
|
||||
|
||||
当前本地开发脚本约定:
|
||||
|
||||
1. `../../scripts/spacetime-dev.ps1` 与 `../../scripts/spacetime-dev.sh` 当前固定执行 `spacetime start` 的 standalone 模式。
|
||||
2. 默认监听 `127.0.0.1:3001`,避免与 `api-server` 默认 `3000` 端口冲突。
|
||||
3. 本地数据目录固定到 `server-rs/.spacetimedb/local`,避免污染全局 SpacetimeDB 根目录。
|
||||
4. 当前阶段暂不自动 publish `crates/spacetime-module`,待 module 实体 scaffold 与聚合入口落地后再扩展。
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `spacetime-module` 只聚合状态模型,不直接承接 HTTP、Cookie、Header、OSS、短信、微信、LLM 等外部副作用。
|
||||
2. 每个业务模块优先在自己的 `crates/module-*` 中定义状态与规则,再由主工程聚合。
|
||||
3. 主工程不重新吞并各模块实现细节,避免回到单大包结构。
|
||||
28
server-rs/crates/tests-support/README.md
Normal file
28
server-rs/crates/tests-support/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# tests-support 共享 package 占位说明
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
## 1. package 职责
|
||||
|
||||
`tests-support` 是测试支撑共享 package,后续负责:
|
||||
|
||||
1. contract、integration、smoke 测试的共享夹具与辅助工具
|
||||
2. 测试环境配置、测试数据装配与断言工具
|
||||
3. 供 `apps/api-server`、`apps/spacetime-module` 与各模块 package 复用的测试基础设施能力
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前提交仅完成目录占位,不提前进入测试夹具、断言工具与 smoke 支撑实现。
|
||||
|
||||
后续与本 package 直接相关的任务包括:
|
||||
|
||||
1. 设计接口测试与 contract 回归共享夹具
|
||||
2. 设计 reducer / view / projection 测试辅助
|
||||
3. 设计主流程 smoke 支撑工具
|
||||
4. 设计双栈对比与切流回归辅助
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `tests-support` 只承接测试支撑能力,不承接业务规则实现。
|
||||
2. 测试夹具要尽量贴近真实 contract 与真实模块边界,避免重新引入脱离现网的伪环境。
|
||||
3. 不允许把测试辅助逻辑散落到各模块 package 中重复实现。
|
||||
Reference in New Issue
Block a user