Fosi Fossil Service
Small teams. Open source. Built on Fossil.

IaC / SwiftFlow

High-performance WMS with wave picking, smart routing, and real-time inventory visibility.

Public | Owner: IaC | Default branch: trunk | Last update: 2026-10-23 14:37

Artifact e05ac2c9dd07b6e2f026fd71bfad00da1183e8632715371d4c8748ff92e324be:


import gleam/dynamic/decode
import gleam/http
import gleam/http/request
import gleam/httpc
import gleam/json
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import ql361api/models.{type ApiError, type ApiSession}

pub type Client {
  Client(
    base_url: String,
    session: Option(ApiSession),
    user_agent: String,
  )
}

pub const default_base_url = "https://22beta.ql361.com/v_22beta/api.cc"

pub const default_user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"

pub fn new(base_url: String) -> Client {
  Client(
    base_url: base_url,
    session: None,
    user_agent: default_user_agent,
  )
}

pub fn default_client() -> Client {
  new(default_base_url)
}

pub fn with_session(client: Client, session: ApiSession) -> Client {
  Client(..client, session: Some(session))
}

pub fn with_user_agent(client: Client, user_agent: String) -> Client {
  Client(..client, user_agent: user_agent)
}

pub fn with_base_url(client: Client, base_url: String) -> Client {
  Client(..client, base_url: base_url)
}

pub fn post_rpc(
  client: Client,
  method: String,
  page_type: String,
  payload_json: String,
) -> Result(String, ApiError) {
  let token_query = case client.session {
    Some(sess) -> "&s=" <> sess.s <> "&tok=" <> sess.tok
    None -> ""
  }

  let full_url =
    client.base_url
    <> "?___method="
    <> method
    <> "&pagetype="
    <> page_type
    <> "&from=allinoneclient&platform=pc"
    <> token_query

  let cookie_header = case client.session {
    Some(sess) -> sess.cookie
    None -> ""
  }

  use req <- result.try(
    request.to(full_url)
    |> result.replace_error(models.UrlParseError),
  )

  let req =
    req
    |> request.set_method(http.Post)
    |> request.set_header("content-type", "application/json")
    |> request.set_header("user-agent", client.user_agent)
    |> request.set_header("referer", "https://22beta.ql361.com/desktop.html?v=196647593")
    |> request.set_body(payload_json)

  let req = case cookie_header {
    "" -> req
    cookie -> request.set_header(req, "cookie", cookie)
  }

  case httpc.send(req) {
    Ok(resp) -> {
      case resp.status {
        200 -> check_envelope(resp.body)
        status -> Error(models.HttpError(status: status, body: resp.body))
      }
    }
    Error(err) -> Error(models.NetworkError(string.inspect(err)))
  }
}

fn check_envelope(body: String) -> Result(String, ApiError) {
  let envelope_decoder = {
    use response_type <- decode.field("type", decode.string)
    use msg <- decode.field("msg", decode.string)
    use product_version <- decode.optional_field("productVersion", "", decode.string)
    decode.success(models.RawEnvelope(response_type, msg, product_version))
  }

  case json.parse(body, envelope_decoder) {
    Ok(envelope) -> {
      case envelope.response_type {
        "normal" -> Ok(body)
        "logoff" -> Error(models.AuthExpired(envelope.msg))
        _ -> {
          case string.contains(envelope.msg, "已退出登录") {
            True -> Error(models.AuthExpired(envelope.msg))
            False -> Error(models.ApiFailure(envelope.msg))
          }
        }
      }
    }
    Error(err) -> {
      // In case QL361 returned non-standard envelope or raw data
      case string.contains(body, "已退出登录") {
        True -> Error(models.AuthExpired("Session expired"))
        False -> Error(models.JsonError(string.inspect(err)))
      }
    }
  }
}

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/IaC/SwiftFlow

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.