百积木文档
开发指南平台应用、模块与 Bundle 开发模块开发

模块调用上下文

了解模块方法当前可以读取的 Runtime、调用方、用户和请求上下文字段。

模块调用上下文是平台在解析并校验一次模块方法调用时生成的 JSON 对象。它与页面或调用方提交的业务参数 相互独立,用于向方法实现提供当前 Runtime、调用方、访问用户和请求路由信息。

模块不能假定所有字段在所有入口都存在。方法只应读取本页列出的稳定字段,并根据调用入口决定哪些字段 必须存在。没有建立对应身份的调用不会伪造默认值,也不会从请求体中的同名字段回退。

稳定字段

字段当前类型何时存在含义
applicationRuntimeIdstring所有已经解析到 Runtime 的模块后端调用当前应用 Runtime ID。平台根据实际执行的 Runtime 写入,并覆盖调用入口提供的同名上下文值。
callerBusinessIdstring已验证出调用方业务身份时调用当前方法的源模块或服务 businessId。模块通过服务引用调用另一模块时使用该字段区分调用方;直接用户调用或没有业务身份的入口可能不存在。
targetBusinessIdstring按模块或服务 businessId 路由方法时当前被调用模块或服务的 businessId。按全局方法名解析时不存在。
targetMethodNamestring已成功解析方法时当前目标方法名。
userIdnumber | string当前入口完成用户身份校验并返回用户时当前平台用户 ID。未登录、可选认证未建立身份或服务调用未携带用户身份时不存在。
workspaceIdnumber | string当前入口完成工作区身份校验时当前可信工作区 ID。它不会仅根据请求参数或请求体中的同名字段生成。
httpMethodstringHTTP 入口进入 Runtime 的 HTTP 方法,例如 GETPOST
requestPathstringHTTP 入口进入 Runtime 的原始请求路径。

number | string 表示当前不同鉴权入口可能保留数字 ID,也可能保留字符串形式。ID 用作比较、存储键或 HTTP Header 前应先规范化为非空字符串,不要进行算术运算。

userId 是目标方法可读取的已验证上下文字段。外部模块后端继续通过 ServiceReference 调用下游服务时, 不能把这个数字重新放进业务参数或自定义用户 Header;应转发 Runtime 为本次请求注入的不透明 X-Runtime-Actor-Assertion。该 assertion 不属于本表的 JSON 上下文字段,也不由 methodBody 映射,完整 规则见 Runtime 用户委托

Runtime 与模块级隔离

需要按 Runtime 和调用模块隔离数据时,同时把 applicationRuntimeIdcallerBusinessId 声明为 必需上下文。若数据还属于工作区,再同时要求 workspaceId。任一必需字段缺失时应拒绝调用,不能回退 到仅工作区、仅 Runtime 或公共目录。

映射到 HTTP 请求

上下文不会自动完整转发给模块后端。HTTP 方法必须在 methodBody 中显式选择字段,并映射到 Header、 Path、Query 或 Body。以下示例把 Runtime、调用模块、工作区和可选用户映射为模块自己的业务 Header:

{
  "header": {
    "X-Module-Runtime-Id": {
      "position": "context",
      "key": "applicationRuntimeId",
      "required": true
    },
    "X-Module-Caller-Business-Id": {
      "position": "context",
      "key": "callerBusinessId",
      "required": true
    },
    "X-Module-Workspace-Id": {
      "position": "context",
      "key": "workspaceId",
      "required": true
    },
    "X-Module-User-Id": {
      "position": "context",
      "key": "userId",
      "required": false
    }
  }
}

position: "context"key 精确读取上下文字段。输出字段名与上下文键不同时必须写 key;省略 key 时使用输出字段名查找。required: true 的字段缺失会终止调用,不应使用固定值或更宽作用域代替。

需要转换字段或组装请求体时可以使用表达式:

{
  "body": {
    "runtimeId": {
      "expression": "context.applicationRuntimeId",
      "required": true
    },
    "callerBusinessId": {
      "expression": "context.callerBusinessId",
      "required": true
    }
  }
}

表达式中的 context 只包含当前调用已经建立的字段。不要在表达式中从业务参数回退身份字段。

Rust / Axum 接入

Rust 后端不会自动获得一个 Runtime SDK 对象、请求扩展或完整上下文 JSON。Runtime 只按当前模块方法的 methodBody 映射发送选中的字段;Rust 服务读取的是映射后的普通 HTTP Header、Path、Query 或 Body。 因此,Rust 代码中的字段名由模块自己的 HTTP 契约决定;除非映射时刻意使用同名输出字段,否则不要假定 applicationRuntimeId 等网关内部键会直接出现在服务请求中。

使用上面的 Header 映射时,Axum 可以在 HTTP 入口立即转换成类型化上下文:

use axum::{
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Response},
};

#[derive(Debug)]
struct RuntimeCallContext {
    application_runtime_id: String,
    caller_business_id: String,
    workspace_id: String,
    user_id: Option<String>,
}

#[derive(Debug)]
enum ContextError {
    Missing(&'static str),
    Invalid(&'static str),
}

impl IntoResponse for ContextError {
    fn into_response(self) -> Response {
        let message = match self {
            Self::Missing(name) => format!("missing required runtime context: {name}"),
            Self::Invalid(name) => format!("invalid runtime context: {name}"),
        };
        (StatusCode::BAD_REQUEST, message).into_response()
    }
}

fn read_header(
    headers: &HeaderMap,
    name: &'static str,
) -> Result<Option<String>, ContextError> {
    let Some(value) = headers.get(name) else {
        return Ok(None);
    };
    let value = value.to_str().map_err(|_| ContextError::Invalid(name))?;
    let value = value.trim();
    if value.is_empty() {
        return Err(ContextError::Invalid(name));
    }
    Ok(Some(value.to_owned()))
}

fn required_header(
    headers: &HeaderMap,
    name: &'static str,
) -> Result<String, ContextError> {
    read_header(headers, name)?.ok_or(ContextError::Missing(name))
}

impl TryFrom<&HeaderMap> for RuntimeCallContext {
    type Error = ContextError;

    fn try_from(headers: &HeaderMap) -> Result<Self, Self::Error> {
        Ok(Self {
            application_runtime_id: required_header(headers, "x-module-runtime-id")?,
            caller_business_id: required_header(headers, "x-module-caller-business-id")?,
            workspace_id: required_header(headers, "x-module-workspace-id")?,
            user_id: read_header(headers, "x-module-user-id")?,
        })
    }
}

async fn handle_module_call(headers: HeaderMap) -> Result<StatusCode, ContextError> {
    let context = RuntimeCallContext::try_from(&headers)?;

    // 从这里开始,业务逻辑只使用类型化 context,不再直接读取 HeaderMap。
    let _ = (
        context.application_runtime_id,
        context.caller_business_id,
        context.workspace_id,
        context.user_id,
    );
    Ok(StatusCode::NO_CONTENT)
}

映射到 Header 后,Runtime 会把数字或字符串形式的 ID 都序列化为 Header 字符串;上面的 Rust 类型统一 使用非空 String,不会对 ID 做算术运算。可选的 userId 未建立时不会发送对应 Header,Rust 中保持 None。如果业务要求当前用户,应该同时把 X-Module-User-Idrequired 改为 true,并把 user_id 改为必需字段。

上下文不是 Endpoint 凭据

X-Module-* 是模块自己定义的 HTTP 字段,不是平台凭据。只有请求确实经过受控 Runtime 调用链,并且 Endpoint 已按环境配置与服务鉴权限制直接访问时, 这些映射值才能作为该次 Runtime 调用的上下文使用。对外公开的 Endpoint 不能只凭这些 Header 完成鉴权。

Actor Header 不是方法自定义 Header

模块定义不得声明、固定或通过表达式生成 X-Runtime-Actor-Assertion。Runtime 只在已建立最终用户身份时 注入该请求级值;外部后端只把收到的值用于当前同步调用链,不解析、不记录、不持久化。

信任与缺失规则

  • params、Query、普通 Header 和请求体都是业务输入;其中的同名字段不会自动成为可信上下文。
  • applicationRuntimeId 来自当前实际执行的 Runtime,不由页面或模块自行选择。
  • callerBusinessId 只在平台验证出调用方业务身份后存在;不能由目标模块根据请求内容猜测。
  • userIdworkspaceId 只代表当前入口已经建立的身份。字段缺失表示该身份没有建立,不表示值为 0、空字符串或系统管理员。
  • 外部服务转发的 Actor assertion 必须经过 Runtime 验证;它不能由普通 Header、请求体或已保存的 userId 重新构造。服务链进入新的目标后,下一跳 assertion 由 Runtime 自动轮换。
  • 方法依赖某个字段时应声明 required: true 并失败关闭。不要设置固定管理员、默认工作区、公共 Runtime 或其他兼容回退。
  • 日志和错误信息可以记录字段是否存在,但不应输出完整身份声明、凭据或敏感模块属性。

本页内容