迁移后端认证与拆分 Spacetime 客户端
This commit is contained in:
@@ -59,6 +59,8 @@ AUTH_REFRESH_SESSION_TTL_DAYS="30"
|
|||||||
AUTH_REFRESH_COOKIE_PATH="/api/auth"
|
AUTH_REFRESH_COOKIE_PATH="/api/auth"
|
||||||
AUTH_REFRESH_COOKIE_SAME_SITE="Lax"
|
AUTH_REFRESH_COOKIE_SAME_SITE="Lax"
|
||||||
AUTH_REFRESH_COOKIE_SECURE="false"
|
AUTH_REFRESH_COOKIE_SECURE="false"
|
||||||
|
# Rust 鉴权快照路径;包含 password_hash 与 refresh token hash,只能放服务端私有目录。
|
||||||
|
GENARRATIVE_AUTH_STORE_PATH="server-rs/.data/auth-store.json"
|
||||||
|
|
||||||
# 手机号验证码登录配置(阿里云 PNVS)。
|
# 手机号验证码登录配置(阿里云 PNVS)。
|
||||||
# 正式环境请改成你自己的 AccessKey 和短信签名/模板。
|
# 正式环境请改成你自己的 AccessKey 和短信签名/模板。
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ temp*build*/
|
|||||||
!/server-node/data/.gitkeep
|
!/server-node/data/.gitkeep
|
||||||
/server-rs/target/
|
/server-rs/target/
|
||||||
/server-rs/.spacetimedb/
|
/server-rs/.spacetimedb/
|
||||||
|
/server-rs/.data/
|
||||||
/public/generated-animations
|
/public/generated-animations
|
||||||
/public/generated-character-drafts
|
/public/generated-character-drafts
|
||||||
/public/generated-characters
|
/public/generated-characters
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Auth refresh session 持久化热修方案
|
||||||
|
|
||||||
|
日期:`2026-04-24`
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
当前 Rust 鉴权链路已经具备 refresh cookie 自动续签能力:access token 过期后,前端会调用 `POST /api/auth/refresh`,后端轮换 refresh token 并返回新的 access token。
|
||||||
|
|
||||||
|
但 `module-auth` 当前仍使用进程内 `InMemoryAuthStore` 保存账号与 refresh session。只要 `server-rs` 在 access token 生命周期内发生重启,浏览器侧 HttpOnly cookie 仍然存在,服务端却找不到对应账号与 session,最终表现为约 `JWT_EXPIRES_IN` 后需要重新登录。
|
||||||
|
|
||||||
|
## 2. 本次目标
|
||||||
|
|
||||||
|
本次先落一个低风险持久化闭环,解决“后端重启导致 2 小时后必须重新登录”的线上体验问题:
|
||||||
|
|
||||||
|
1. 为当前 `InMemoryAuthStore` 增加 UTF-8 JSON 快照文件。
|
||||||
|
2. 在账号、手机号索引、微信身份、refresh session 发生变更后自动保存快照。
|
||||||
|
3. `api-server` 启动时从配置路径恢复快照。
|
||||||
|
4. 保持现有 `/api/auth/refresh`、`logout`、`sessions` 语义不变。
|
||||||
|
|
||||||
|
## 3. 非目标
|
||||||
|
|
||||||
|
本次不把完整认证域一次性迁入 SpacetimeDB 表,原因是 refresh session 独立持久化不足以解决问题:refresh 成功后还需要按 `user_id` 读取账号快照重新签发 access token,因此账号主数据也必须同源恢复。
|
||||||
|
|
||||||
|
SpacetimeDB 正式表接管仍按以下既有文档继续推进:
|
||||||
|
|
||||||
|
1. `docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md`
|
||||||
|
2. `docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md`
|
||||||
|
3. `docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md`
|
||||||
|
|
||||||
|
## 4. 配置
|
||||||
|
|
||||||
|
新增环境变量:
|
||||||
|
|
||||||
|
| 变量 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GENARRATIVE_AUTH_STORE_PATH` | `server-rs/.data/auth-store.json` | 当前 Rust 鉴权快照文件路径。相对路径按进程工作目录解析。 |
|
||||||
|
|
||||||
|
## 5. 数据边界
|
||||||
|
|
||||||
|
快照文件保存当前 Rust 鉴权服务已经在内存中维护的最小真相:
|
||||||
|
|
||||||
|
1. `next_user_id`
|
||||||
|
2. `users_by_username`
|
||||||
|
3. `phone_to_user_id`
|
||||||
|
4. `sessions_by_id`
|
||||||
|
5. `session_id_by_refresh_token_hash`
|
||||||
|
6. `wechat_identity_by_provider_uid`
|
||||||
|
7. `user_id_by_provider_union_id`
|
||||||
|
|
||||||
|
短信验证码和微信 OAuth state 不持久化,原因是它们是短生命周期挑战数据,重启后失效是可接受行为。
|
||||||
|
|
||||||
|
## 6. 安全约束
|
||||||
|
|
||||||
|
1. refresh token 原文仍只存在浏览器 HttpOnly cookie,快照只保存 `sha256(refresh_token)`。
|
||||||
|
2. 快照包含 `password_hash`、手机号映射和 refresh token hash,部署时必须放在服务端私有目录,不允许暴露到静态资源目录。
|
||||||
|
3. 快照写入必须使用 UTF-8,并通过临时文件原子替换降低写坏风险。
|
||||||
|
|
||||||
|
## 7. 后续 SpacetimeDB 接管点
|
||||||
|
|
||||||
|
当 `user_account`、`auth_identity`、`refresh_session` 表及 reducer 全部落地后,替换策略如下:
|
||||||
|
|
||||||
|
1. 保留 `module-auth` 用例语义。
|
||||||
|
2. 把当前快照仓储替换为 SpacetimeDB 仓储适配器。
|
||||||
|
3. 启动时可提供一次性导入脚本,把 JSON 快照导入 SpacetimeDB 表。
|
||||||
|
4. 导入完成后禁用 `GENARRATIVE_AUTH_STORE_PATH` 快照写入。
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Auth SpacetimeDB 快照迁移 Stage 1
|
||||||
|
|
||||||
|
日期:`2026-04-24`
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
`AUTH_REFRESH_SESSION_PERSISTENCE_HOTFIX_2026-04-24.md` 已把 Rust 鉴权内存态落到本地 JSON 快照,解决 `server-rs` 重启后 refresh session 丢失导致 `JWT_EXPIRES_IN` 到期后必须重新登录的问题。
|
||||||
|
|
||||||
|
本阶段继续把该快照迁入 SpacetimeDB,作为正式 `user_account/auth_identity/refresh_session` 表完全拆分前的过渡真相源。
|
||||||
|
|
||||||
|
## 2. 阶段目标
|
||||||
|
|
||||||
|
1. 在 `spacetime-module` 新增私有 `auth_store_snapshot` 表。
|
||||||
|
2. 表内只保存一条当前 Axum 鉴权快照 JSON,主键固定为 `default`。
|
||||||
|
3. 新增 `get_auth_store_snapshot` 与 `upsert_auth_store_snapshot` procedure,供 `api-server` 同步读写。
|
||||||
|
4. `module-auth` 继续拥有鉴权业务语义,SpacetimeDB 只承接当前阶段的持久化真相。
|
||||||
|
|
||||||
|
## 3. 为什么先做快照表
|
||||||
|
|
||||||
|
只迁 `refresh_session` 表无法恢复登录态,因为 refresh 成功后仍必须按 `user_id` 找到账号快照重新签发 access token。因此正式拆表必须同时完成账号、身份、会话三组 reducer。
|
||||||
|
|
||||||
|
本阶段先把已经验证过的 JSON 快照从本地文件迁到 SpacetimeDB,收益是:
|
||||||
|
|
||||||
|
1. 后端进程重启后可恢复登录态。
|
||||||
|
2. 多实例部署时可共享同一份鉴权快照。
|
||||||
|
3. 后续正式拆表时有统一导入来源。
|
||||||
|
|
||||||
|
## 4. 表设计
|
||||||
|
|
||||||
|
表名:`auth_store_snapshot`
|
||||||
|
|
||||||
|
访问级别:private table
|
||||||
|
|
||||||
|
字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `snapshot_id` | `String` | 主键,固定为 `default`。 |
|
||||||
|
| `snapshot_json` | `String` | `module-auth` 当前持久化快照 JSON。 |
|
||||||
|
| `updated_at` | `Timestamp` | 最近写入时间。 |
|
||||||
|
|
||||||
|
## 5. Procedure 设计
|
||||||
|
|
||||||
|
### 5.1 `get_auth_store_snapshot`
|
||||||
|
|
||||||
|
输入:无。
|
||||||
|
|
||||||
|
输出:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"snapshotJson": "...",
|
||||||
|
"updatedAtMicros": 123456789,
|
||||||
|
"errorMessage": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当记录不存在时,`snapshotJson = null`。
|
||||||
|
|
||||||
|
### 5.2 `upsert_auth_store_snapshot`
|
||||||
|
|
||||||
|
输入:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"snapshotJson": "...",
|
||||||
|
"updatedAtMicros": 123456789
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出同 `get_auth_store_snapshot`。
|
||||||
|
|
||||||
|
## 6. 后续拆表迁移点
|
||||||
|
|
||||||
|
Stage 2 再把 `auth_store_snapshot.snapshot_json` 导入并拆分为:
|
||||||
|
|
||||||
|
1. `user_account`
|
||||||
|
2. `auth_identity`
|
||||||
|
3. `refresh_session`
|
||||||
|
|
||||||
|
拆分完成后,`auth_store_snapshot` 只保留为迁移备份,不再作为运行时写入目标。
|
||||||
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
# 密码登录与自动建号落地设计
|
# 密码登录历史落地设计
|
||||||
|
|
||||||
|
> 2026-04-24 更新:当前产品策略已调整为“不开放密码注册”。新用户必须通过手机号验证码注册/登录,密码登录只面向已经设置密码的账号。密码修改与重置以 [PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md](./PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md) 为准;本文中“密码自动建号”仅保留为历史基线说明,不再作为当前落地依据。
|
||||||
|
|
||||||
日期:`2026-04-21`
|
日期:`2026-04-21`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 密码登录、修改与重置落地设计
|
||||||
|
|
||||||
|
日期:`2026-04-24`
|
||||||
|
|
||||||
|
## 1. 目标边界
|
||||||
|
|
||||||
|
本次迭代开放账号密码登录、登录后修改密码、手机号验证码重置密码,但不开放密码注册。
|
||||||
|
|
||||||
|
1. 新用户只能通过手机号验证码完成注册与首次登录。
|
||||||
|
2. 已有用户可以在登录后设置或修改密码。
|
||||||
|
3. 忘记密码时,只能通过已绑定手机号验证码重置密码。
|
||||||
|
4. 密码登录只校验已存在且已设置密码的账号,不自动创建新账号。
|
||||||
|
|
||||||
|
## 2. 接口设计
|
||||||
|
|
||||||
|
### 2.1 密码登录
|
||||||
|
|
||||||
|
沿用现有 `POST /api/auth/entry`:
|
||||||
|
|
||||||
|
1. 请求字段:`username`、`password`。
|
||||||
|
2. 用户不存在时返回 `401`,不创建账号。
|
||||||
|
3. 用户存在但未设置密码时返回 `401`。
|
||||||
|
4. 校验成功后签发 access token,并写入 refresh cookie。
|
||||||
|
|
||||||
|
### 2.2 修改密码
|
||||||
|
|
||||||
|
新增 `POST /api/auth/password/change`:
|
||||||
|
|
||||||
|
1. 需要 Bearer 登录态。
|
||||||
|
2. 请求字段:`currentPassword`、`newPassword`。
|
||||||
|
3. 若账号未设置过密码,允许 `currentPassword` 为空并设置首个密码。
|
||||||
|
4. 若账号已有密码,必须校验 `currentPassword` 后才能写入 `newPassword`。
|
||||||
|
5. 修改成功后递增用户 `token_version`,使旧 access token 失效;前端沿用当前 refresh 会话刷新登录态。
|
||||||
|
|
||||||
|
### 2.3 重置密码
|
||||||
|
|
||||||
|
新增 `POST /api/auth/password/reset`:
|
||||||
|
|
||||||
|
1. 不需要 Bearer 登录态。
|
||||||
|
2. 请求字段:`phone`、`code`、`newPassword`。
|
||||||
|
3. 使用 `reset_password` 短信场景校验验证码。
|
||||||
|
4. 手机号不存在时返回 `404`,避免用密码重置隐式注册账号。
|
||||||
|
5. 重置成功后签发新的 access token,并写入 refresh cookie,便于用户直接进入登录态。
|
||||||
|
|
||||||
|
### 2.4 发送重置验证码
|
||||||
|
|
||||||
|
复用 `POST /api/auth/phone/send-code`,`scene` 增加 `reset_password`。
|
||||||
|
|
||||||
|
## 3. 前端交互
|
||||||
|
|
||||||
|
登录弹窗拆成两个页签:
|
||||||
|
|
||||||
|
1. `登录`:提供密码登录、手机号验证码登录、忘记密码入口。
|
||||||
|
2. `注册`:只提供手机号验证码注册/登录,不提供账号密码注册。
|
||||||
|
3. `忘记密码`:从登录页进入独立重置面板,不在当前表单下方展开。
|
||||||
|
4. 账号设置面板提供密码修改入口;未设置密码的账号显示为设置密码。
|
||||||
|
|
||||||
|
## 4. 数据约束
|
||||||
|
|
||||||
|
进程内认证快照中的 `password_hash` 改为可空语义:
|
||||||
|
|
||||||
|
1. 手机号新建账号默认没有用户可用密码。
|
||||||
|
2. 微信待绑定账号默认没有用户可用密码。
|
||||||
|
3. 只有用户显式修改或重置密码后,才允许密码登录。
|
||||||
|
|
||||||
|
后续迁移到 SpacetimeDB 表时,保持同一语义:密码哈希字段允许为空,密码登录 reducer 不承担注册能力。
|
||||||
@@ -190,6 +190,8 @@ session snapshot 中的 `resultPreview` 固定输出:
|
|||||||
7. 最终兜底 `还在收集你的世界锚点。`
|
7. 最终兜底 `还在收集你的世界锚点。`
|
||||||
8. `subtitle` 先取 `draft_profile_json.subtitle`
|
8. `subtitle` 先取 `draft_profile_json.subtitle`
|
||||||
9. 否则用 `stageLabel`
|
9. 否则用 `stageLabel`
|
||||||
|
10. 同一 `source_agent_session_id` 同时存在未发布 Agent 会话草稿与 `custom_world_profile` 草稿时,works 只输出一条草稿;优先保留可继续聊天的 `agent_session`,避免作品库把“聊天中的草稿”和“待发布草稿 profile”展示成两份作品。
|
||||||
|
11. 只有找不到同源未发布 Agent 会话,或 profile 已经发布时,`custom_world_profile` 才作为独立作品输出。
|
||||||
|
|
||||||
### 4.4 已发布 works 最小取值规则
|
### 4.4 已发布 works 最小取值规则
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,26 @@ export type AuthEntryResponse = {
|
|||||||
user: AuthUser;
|
user: AuthUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthPasswordChangeRequest = {
|
||||||
|
currentPassword?: string;
|
||||||
|
newPassword: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthPasswordChangeResponse = {
|
||||||
|
user: AuthUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthPasswordResetRequest = {
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
newPassword: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthPasswordResetResponse = {
|
||||||
|
token: string;
|
||||||
|
user: AuthUser;
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthPhoneSendCodeRequest = {
|
export type AuthPhoneSendCodeRequest = {
|
||||||
phone: string;
|
phone: string;
|
||||||
scene?: 'login' | 'bind_phone' | 'change_phone';
|
scene?: 'login' | 'bind_phone' | 'change_phone';
|
||||||
|
|||||||
122
scripts/spacetime-logs-local.sh
Normal file
122
scripts/spacetime-logs-local.sh
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
用法:
|
||||||
|
npm run dev:rust:logs
|
||||||
|
npm run dev:rust:logs -- --follow
|
||||||
|
./scripts/spacetime-logs-local.sh --lines 500 --output logs/spacetime/local.log
|
||||||
|
|
||||||
|
说明:
|
||||||
|
1. 从本地 SpacetimeDB 通过 spacetime logs 提取模块日志到本地文件。
|
||||||
|
2. 默认读取 spacetime.local.json 的 database 字段,默认 server 为 http://127.0.0.1:3101。
|
||||||
|
3. 默认输出到 logs/spacetime/<database>-<timestamp>.log;--follow 会持续追加并同步写到终端。
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
local command_name="$1"
|
||||||
|
|
||||||
|
if ! command -v "${command_name}" >/dev/null 2>&1; then
|
||||||
|
echo "[stdb:logs] 缺少命令: ${command_name}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
read_local_spacetime_database() {
|
||||||
|
local config_path="${REPO_ROOT}/spacetime.local.json"
|
||||||
|
|
||||||
|
if [[ ! -f "${config_path}" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
node -e '
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = process.argv[1];
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(fs.readFileSync(path, "utf8")).database;
|
||||||
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
process.stdout.write(value.trim());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`[stdb:logs] ignore invalid spacetime.local.json: ${error.message}\n`);
|
||||||
|
}
|
||||||
|
' "${config_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
DATABASE=""
|
||||||
|
SPACETIME_SERVER="http://127.0.0.1:3101"
|
||||||
|
LINES="200"
|
||||||
|
OUTPUT=""
|
||||||
|
FOLLOW=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--database)
|
||||||
|
DATABASE="${2:?缺少 --database 的值}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--server)
|
||||||
|
SPACETIME_SERVER="${2:?缺少 --server 的值}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--lines|-n)
|
||||||
|
LINES="${2:?缺少 --lines 的值}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output|-o)
|
||||||
|
OUTPUT="${2:?缺少 --output 的值}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--follow|-f)
|
||||||
|
FOLLOW=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[stdb:logs] 未知参数: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
require_command node
|
||||||
|
require_command spacetime
|
||||||
|
|
||||||
|
if [[ -z "${DATABASE//[[:space:]]/}" ]]; then
|
||||||
|
DATABASE="$(read_local_spacetime_database)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${DATABASE//[[:space:]]/}" ]]; then
|
||||||
|
DATABASE="genarrative-dev"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${OUTPUT//[[:space:]]/}" ]]; then
|
||||||
|
LOG_DIR="${REPO_ROOT}/logs/spacetime"
|
||||||
|
mkdir -p "${LOG_DIR}"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
OUTPUT="${LOG_DIR}/${DATABASE}-${TIMESTAMP}.log"
|
||||||
|
else
|
||||||
|
mkdir -p "$(dirname -- "${OUTPUT}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARGS=(logs "${DATABASE}" --server "${SPACETIME_SERVER}" -n "${LINES}")
|
||||||
|
|
||||||
|
if [[ "${FOLLOW}" -eq 1 ]]; then
|
||||||
|
ARGS+=(-f)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[stdb:logs] database: ${DATABASE}"
|
||||||
|
echo "[stdb:logs] server: ${SPACETIME_SERVER}"
|
||||||
|
echo "[stdb:logs] output: ${OUTPUT}"
|
||||||
|
|
||||||
|
spacetime "${ARGS[@]}" | tee -a "${OUTPUT}"
|
||||||
2
server-rs/Cargo.lock
generated
2
server-rs/Cargo.lock
generated
@@ -1518,6 +1518,8 @@ name = "module-auth"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"platform-auth",
|
"platform-auth",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"shared-kernel",
|
"shared-kernel",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -29,10 +29,12 @@
|
|||||||
7. 基础 `TraceLayer` 挂载
|
7. 基础 `TraceLayer` 挂载
|
||||||
8. 接入 `shared-logging` 完成 `tracing subscriber` 初始化
|
8. 接入 `shared-logging` 完成 `tracing subscriber` 初始化
|
||||||
9. 接入 `POST /api/auth/entry` 首版密码登录链路
|
9. 接入 `POST /api/auth/entry` 首版密码登录链路
|
||||||
10. 接入 `POST /api/assets/direct-upload-tickets` 直传票据接口
|
10. 接入 `POST /api/auth/password/change` 登录后修改密码链路
|
||||||
11. 接入 `GET /api/auth/me` 当前用户查询链路
|
11. 接入 `POST /api/auth/password/reset` 手机验证码重置密码链路
|
||||||
12. 接入 `POST /api/auth/refresh` refresh token 轮换链路
|
12. 接入 `POST /api/assets/direct-upload-tickets` 直传票据接口
|
||||||
13. 接入 `POST /api/auth/logout` 当前设备退出链路
|
13. 接入 `GET /api/auth/me` 当前用户查询链路
|
||||||
|
14. 接入 `POST /api/auth/refresh` refresh token 轮换链路
|
||||||
|
15. 接入 `POST /api/auth/logout` 当前设备退出链路
|
||||||
14. 接入 `POST /api/assets/objects/confirm` 上传完成确认链路
|
14. 接入 `POST /api/assets/objects/confirm` 上传完成确认链路
|
||||||
15. 接入 `GET /api/auth/login-options` 登录方式探测链路
|
15. 接入 `GET /api/auth/login-options` 登录方式探测链路
|
||||||
16. 接入 `POST /api/auth/phone/send-code` 手机验证码发送链路
|
16. 接入 `POST /api/auth/phone/send-code` 手机验证码发送链路
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ use crate::{
|
|||||||
logout::logout,
|
logout::logout,
|
||||||
logout_all::logout_all,
|
logout_all::logout_all,
|
||||||
password_entry::password_entry,
|
password_entry::password_entry,
|
||||||
|
password_management::{change_password, reset_password},
|
||||||
phone_auth::{phone_login, send_phone_code},
|
phone_auth::{phone_login, send_phone_code},
|
||||||
puzzle::{
|
puzzle::{
|
||||||
advance_puzzle_next_level, create_puzzle_agent_session, drag_puzzle_piece_or_group,
|
advance_puzzle_next_level, create_puzzle_agent_session, drag_puzzle_piece_or_group,
|
||||||
@@ -887,6 +888,14 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route("/api/auth/entry", post(password_entry))
|
.route("/api/auth/entry", post(password_entry))
|
||||||
|
.route(
|
||||||
|
"/api/auth/password/change",
|
||||||
|
post(change_password).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route("/api/auth/password/reset", post(reset_password))
|
||||||
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
|
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
|
||||||
.layer(middleware::from_fn(normalize_error_response))
|
.layer(middleware::from_fn(normalize_error_response))
|
||||||
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
|
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ fn map_public_user_search_error(error: module_auth::PasswordEntryError) -> AppEr
|
|||||||
| module_auth::PasswordEntryError::PasswordHash(_)
|
| module_auth::PasswordEntryError::PasswordHash(_)
|
||||||
| module_auth::PasswordEntryError::InvalidUsername
|
| module_auth::PasswordEntryError::InvalidUsername
|
||||||
| module_auth::PasswordEntryError::InvalidPasswordLength
|
| module_auth::PasswordEntryError::InvalidPasswordLength
|
||||||
| module_auth::PasswordEntryError::InvalidCredentials => {
|
| module_auth::PasswordEntryError::InvalidCredentials
|
||||||
|
| module_auth::PasswordEntryError::UserNotFound => {
|
||||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use platform_llm::{
|
|||||||
|
|
||||||
const DEFAULT_LLM_MODEL: &str = "doubao-1-5-pro-32k-character-250715";
|
const DEFAULT_LLM_MODEL: &str = "doubao-1-5-pro-32k-character-250715";
|
||||||
const DEFAULT_INTERNAL_API_SECRET: &str = "genarrative-dev-internal-bridge";
|
const DEFAULT_INTERNAL_API_SECRET: &str = "genarrative-dev-internal-bridge";
|
||||||
|
const DEFAULT_AUTH_STORE_PATH: &str = "server-rs/.data/auth-store.json";
|
||||||
const SPACETIME_LOCAL_CONFIG_FILE: &str = "spacetime.local.json";
|
const SPACETIME_LOCAL_CONFIG_FILE: &str = "spacetime.local.json";
|
||||||
|
|
||||||
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
|
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
|
||||||
@@ -27,6 +28,7 @@ pub struct AppConfig {
|
|||||||
pub refresh_cookie_secure: bool,
|
pub refresh_cookie_secure: bool,
|
||||||
pub refresh_cookie_same_site: String,
|
pub refresh_cookie_same_site: String,
|
||||||
pub refresh_session_ttl_days: u32,
|
pub refresh_session_ttl_days: u32,
|
||||||
|
pub auth_store_path: PathBuf,
|
||||||
pub sms_auth_enabled: bool,
|
pub sms_auth_enabled: bool,
|
||||||
pub sms_auth_provider: String,
|
pub sms_auth_provider: String,
|
||||||
pub sms_endpoint: String,
|
pub sms_endpoint: String,
|
||||||
@@ -109,6 +111,7 @@ impl Default for AppConfig {
|
|||||||
refresh_cookie_secure: false,
|
refresh_cookie_secure: false,
|
||||||
refresh_cookie_same_site: "Lax".to_string(),
|
refresh_cookie_same_site: "Lax".to_string(),
|
||||||
refresh_session_ttl_days: 30,
|
refresh_session_ttl_days: 30,
|
||||||
|
auth_store_path: PathBuf::from(DEFAULT_AUTH_STORE_PATH),
|
||||||
sms_auth_enabled: false,
|
sms_auth_enabled: false,
|
||||||
sms_auth_provider: "mock".to_string(),
|
sms_auth_provider: "mock".to_string(),
|
||||||
sms_endpoint: "dypnsapi.aliyuncs.com".to_string(),
|
sms_endpoint: "dypnsapi.aliyuncs.com".to_string(),
|
||||||
@@ -255,6 +258,10 @@ impl AppConfig {
|
|||||||
config.refresh_session_ttl_days = refresh_session_ttl_days;
|
config.refresh_session_ttl_days = refresh_session_ttl_days;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(auth_store_path) = read_first_non_empty_env(&["GENARRATIVE_AUTH_STORE_PATH"]) {
|
||||||
|
config.auth_store_path = PathBuf::from(auth_store_path);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(sms_auth_enabled) = read_first_bool_env(&["SMS_AUTH_ENABLED"]) {
|
if let Some(sms_auth_enabled) = read_first_bool_env(&["SMS_AUTH_ENABLED"]) {
|
||||||
config.sms_auth_enabled = sms_auth_enabled;
|
config.sms_auth_enabled = sms_auth_enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -988,10 +988,10 @@ pub async fn execute_custom_world_agent_action(
|
|||||||
let result = state
|
let result = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput {
|
.execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput {
|
||||||
session_id,
|
session_id: session_id.clone(),
|
||||||
owner_user_id,
|
owner_user_id: owner_user_id.clone(),
|
||||||
operation_id: build_prefixed_uuid_id("operation-"),
|
operation_id: build_prefixed_uuid_id("operation-"),
|
||||||
action,
|
action: action.clone(),
|
||||||
payload_json: Some(payload_json),
|
payload_json: Some(payload_json),
|
||||||
submitted_at_micros,
|
submitted_at_micros,
|
||||||
})
|
})
|
||||||
@@ -1179,7 +1179,7 @@ fn log_custom_world_publish_gate_diagnostics(
|
|||||||
blocker_codes = %blocker_codes,
|
blocker_codes = %blocker_codes,
|
||||||
has_draft_profile = session.draft_profile.as_object().map(|value| !value.is_empty()).unwrap_or(false),
|
has_draft_profile = session.draft_profile.as_object().map(|value| !value.is_empty()).unwrap_or(false),
|
||||||
has_result_preview = session.result_preview.is_some(),
|
has_result_preview = session.result_preview.is_some(),
|
||||||
preview_source = session.result_preview.as_ref().and_then(|value| value.get("source")).and_then(Value::as_str).unwrap_or(""),
|
preview_source = session.result_preview.as_ref().and_then(|value| value.get("source")).and_then(serde_json::Value::as_str).unwrap_or(""),
|
||||||
has_world_hook = has_custom_world_publish_text(profile, &["worldHook", "creatorIntent.worldHook", "anchorContent.worldPromise.hook", "settingText"]),
|
has_world_hook = has_custom_world_publish_text(profile, &["worldHook", "creatorIntent.worldHook", "anchorContent.worldPromise.hook", "settingText"]),
|
||||||
has_player_premise = has_custom_world_publish_text(profile, &["playerPremise", "creatorIntent.playerPremise", "anchorContent.playerEntryPoint.openingIdentity", "anchorContent.playerEntryPoint.openingProblem", "anchorContent.playerEntryPoint.entryMotivation"]),
|
has_player_premise = has_custom_world_publish_text(profile, &["playerPremise", "creatorIntent.playerPremise", "anchorContent.playerEntryPoint.openingIdentity", "anchorContent.playerEntryPoint.openingProblem", "anchorContent.playerEntryPoint.entryMotivation"]),
|
||||||
has_core_conflicts = has_custom_world_non_empty_text_array(profile, "coreConflicts"),
|
has_core_conflicts = has_custom_world_non_empty_text_array(profile, "coreConflicts"),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, State},
|
extract::{Extension, State},
|
||||||
http::HeaderMap,
|
http::{HeaderMap, StatusCode},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use module_auth::LogoutCurrentSessionInput;
|
use module_auth::LogoutCurrentSessionInput;
|
||||||
@@ -44,6 +44,13 @@ pub async fn logout(
|
|||||||
OffsetDateTime::now_utc(),
|
OffsetDateTime::now_utc(),
|
||||||
)
|
)
|
||||||
.map_err(map_logout_error)?;
|
.map_err(map_logout_error)?;
|
||||||
|
state
|
||||||
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_message(format!("同步认证快照失败:{error}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
attach_set_cookie_header(
|
attach_set_cookie_header(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, State},
|
extract::{Extension, State},
|
||||||
http::HeaderMap,
|
http::{HeaderMap, StatusCode},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use module_auth::LogoutAllSessionsInput;
|
use module_auth::LogoutAllSessionsInput;
|
||||||
@@ -32,6 +32,13 @@ pub async fn logout_all(
|
|||||||
OffsetDateTime::now_utc(),
|
OffsetDateTime::now_utc(),
|
||||||
)
|
)
|
||||||
.map_err(map_logout_error)?;
|
.map_err(map_logout_error)?;
|
||||||
|
state
|
||||||
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_message(format!("同步认证快照失败:{error}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
attach_set_cookie_header(
|
attach_set_cookie_header(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ mod login_options;
|
|||||||
mod logout;
|
mod logout;
|
||||||
mod logout_all;
|
mod logout_all;
|
||||||
mod password_entry;
|
mod password_entry;
|
||||||
|
mod password_management;
|
||||||
mod phone_auth;
|
mod phone_auth;
|
||||||
mod puzzle;
|
mod puzzle;
|
||||||
mod puzzle_agent_turn;
|
mod puzzle_agent_turn;
|
||||||
@@ -67,7 +68,8 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
let bind_address = config.bind_socket_addr();
|
let bind_address = config.bind_socket_addr();
|
||||||
let listener = TcpListener::bind(bind_address).await?;
|
let listener = TcpListener::bind(bind_address).await?;
|
||||||
|
|
||||||
let state = AppState::new(config)
|
let state = AppState::try_restore_auth_store_from_spacetime(config)
|
||||||
|
.await
|
||||||
.map_err(|error| std::io::Error::other(format!("初始化应用状态失败:{error}")))?;
|
.map_err(|error| std::io::Error::other(format!("初始化应用状态失败:{error}")))?;
|
||||||
let router = build_router(state);
|
let router = build_router(state);
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ pub async fn password_entry(
|
|||||||
.map_err(map_password_entry_error)?;
|
.map_err(map_password_entry_error)?;
|
||||||
let session_client = resolve_session_client_context(&headers);
|
let session_client = resolve_session_client_context(&headers);
|
||||||
let signed_session = create_password_auth_session(&state, &result.user, &session_client)?;
|
let signed_session = create_password_auth_session(&state, &result.user, &session_client)?;
|
||||||
|
state
|
||||||
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_message(format!("同步认证快照失败:{error}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
attach_set_cookie_header(
|
attach_set_cookie_header(
|
||||||
@@ -75,6 +82,9 @@ fn map_password_entry_error(error: PasswordEntryError) -> AppError {
|
|||||||
PasswordEntryError::InvalidCredentials => {
|
PasswordEntryError::InvalidCredentials => {
|
||||||
AppError::from_status(StatusCode::UNAUTHORIZED).with_message("用户名或密码错误")
|
AppError::from_status(StatusCode::UNAUTHORIZED).with_message("用户名或密码错误")
|
||||||
}
|
}
|
||||||
|
PasswordEntryError::UserNotFound => {
|
||||||
|
AppError::from_status(StatusCode::UNAUTHORIZED).with_message("用户名或密码错误")
|
||||||
|
}
|
||||||
PasswordEntryError::Store(_) | PasswordEntryError::PasswordHash(_) => {
|
PasswordEntryError::Store(_) | PasswordEntryError::PasswordHash(_) => {
|
||||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
117
server-rs/crates/api-server/src/password_management.rs
Normal file
117
server-rs/crates/api-server/src/password_management.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Extension, State},
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use module_auth::{ChangePasswordInput, PasswordEntryError, ResetPasswordInput};
|
||||||
|
use shared_contracts::auth::{
|
||||||
|
PasswordChangeRequest, PasswordChangeResponse, PasswordResetRequest, PasswordResetResponse,
|
||||||
|
};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api_response::json_success_body,
|
||||||
|
auth::AuthenticatedAccessToken,
|
||||||
|
auth_payload::map_auth_user_payload,
|
||||||
|
auth_session::{
|
||||||
|
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
|
||||||
|
},
|
||||||
|
http_error::AppError,
|
||||||
|
phone_auth::map_phone_auth_error,
|
||||||
|
request_context::RequestContext,
|
||||||
|
session_client::resolve_session_client_context,
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn change_password(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
Json(payload): Json<PasswordChangeRequest>,
|
||||||
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
|
let result = state
|
||||||
|
.password_entry_service()
|
||||||
|
.change_password(ChangePasswordInput {
|
||||||
|
user_id: authenticated.claims().user_id().to_string(),
|
||||||
|
current_password: payload.current_password,
|
||||||
|
new_password: payload.new_password,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(map_password_management_error)?;
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
PasswordChangeResponse {
|
||||||
|
user: map_auth_user_payload(result.user),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset_password(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(payload): Json<PasswordResetRequest>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
if !state.config.sms_auth_enabled {
|
||||||
|
return Err(
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_message("手机号登录暂未启用")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = state
|
||||||
|
.phone_auth_service()
|
||||||
|
.reset_password(
|
||||||
|
ResetPasswordInput {
|
||||||
|
phone_number: payload.phone,
|
||||||
|
verify_code: payload.code,
|
||||||
|
new_password: payload.new_password,
|
||||||
|
},
|
||||||
|
OffsetDateTime::now_utc(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(map_phone_auth_error)?;
|
||||||
|
let session_client = resolve_session_client_context(&headers);
|
||||||
|
let signed_session = create_auth_session(
|
||||||
|
&state,
|
||||||
|
&result.user,
|
||||||
|
&session_client,
|
||||||
|
module_auth::AuthLoginMethod::Password,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
attach_set_cookie_header(
|
||||||
|
&mut headers,
|
||||||
|
build_refresh_session_cookie_header(&state, &signed_session.refresh_token)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
headers,
|
||||||
|
json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
PasswordResetResponse {
|
||||||
|
token: signed_session.access_token,
|
||||||
|
user: map_auth_user_payload(result.user),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_password_management_error(error: PasswordEntryError) -> AppError {
|
||||||
|
match error {
|
||||||
|
PasswordEntryError::InvalidUsername | PasswordEntryError::InvalidPublicUserCode => {
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error.to_string())
|
||||||
|
}
|
||||||
|
PasswordEntryError::InvalidPasswordLength => AppError::from_status(StatusCode::BAD_REQUEST)
|
||||||
|
.with_message("密码长度需要在 6 到 128 位之间"),
|
||||||
|
PasswordEntryError::InvalidCredentials => {
|
||||||
|
AppError::from_status(StatusCode::UNAUTHORIZED).with_message("当前密码错误")
|
||||||
|
}
|
||||||
|
PasswordEntryError::UserNotFound => AppError::from_status(StatusCode::UNAUTHORIZED)
|
||||||
|
.with_message("当前登录态已失效,请重新登录"),
|
||||||
|
PasswordEntryError::Store(_) | PasswordEntryError::PasswordHash(_) => {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,6 +153,13 @@ pub async fn phone_login(
|
|||||||
&session_client,
|
&session_client,
|
||||||
AuthLoginMethod::Phone,
|
AuthLoginMethod::Phone,
|
||||||
)?;
|
)?;
|
||||||
|
state
|
||||||
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_message(format!("同步认证快照失败:{error}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
attach_set_cookie_header(
|
attach_set_cookie_header(
|
||||||
@@ -177,6 +184,7 @@ fn map_phone_auth_scene(raw_scene: Option<&str>) -> Result<PhoneAuthScene, AppEr
|
|||||||
"login" => Ok(PhoneAuthScene::Login),
|
"login" => Ok(PhoneAuthScene::Login),
|
||||||
"bind_phone" => Ok(PhoneAuthScene::BindPhone),
|
"bind_phone" => Ok(PhoneAuthScene::BindPhone),
|
||||||
"change_phone" => Ok(PhoneAuthScene::ChangePhone),
|
"change_phone" => Ok(PhoneAuthScene::ChangePhone),
|
||||||
|
"reset_password" => Ok(PhoneAuthScene::ResetPassword),
|
||||||
_ => Err(AppError::from_status(StatusCode::BAD_REQUEST)
|
_ => Err(AppError::from_status(StatusCode::BAD_REQUEST)
|
||||||
.with_message("短信验证码场景不合法")
|
.with_message("短信验证码场景不合法")
|
||||||
.with_details(json!({ "field": "scene" }))),
|
.with_details(json!({ "field": "scene" }))),
|
||||||
@@ -214,7 +222,7 @@ fn mask_phone_digits(value: &str) -> String {
|
|||||||
masked
|
masked
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_phone_auth_error(error: PhoneAuthError) -> AppError {
|
pub fn map_phone_auth_error(error: PhoneAuthError) -> AppError {
|
||||||
match error {
|
match error {
|
||||||
PhoneAuthError::InvalidPhoneNumber
|
PhoneAuthError::InvalidPhoneNumber
|
||||||
| PhoneAuthError::InvalidVerifyCode
|
| PhoneAuthError::InvalidVerifyCode
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ pub async fn refresh_session(
|
|||||||
&rotated.session.session_id,
|
&rotated.session.session_id,
|
||||||
Some(&rotated.session.issued_by_provider),
|
Some(&rotated.session.issued_by_provider),
|
||||||
)?;
|
)?;
|
||||||
|
state
|
||||||
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
AppError::from_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_message(format!("同步认证快照失败:{error}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
attach_set_cookie_header(
|
attach_set_cookie_header(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use platform_oss::{OssClient, OssConfig, OssError};
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use spacetime_client::{SpacetimeClient, SpacetimeClientConfig, SpacetimeClientError};
|
use spacetime_client::{SpacetimeClient, SpacetimeClientConfig, SpacetimeClientError};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::wechat_provider::{WechatProvider, build_wechat_provider};
|
use crate::wechat_provider::{WechatProvider, build_wechat_provider};
|
||||||
@@ -37,6 +38,7 @@ pub struct AppState {
|
|||||||
admin_runtime: Option<AdminRuntime>,
|
admin_runtime: Option<AdminRuntime>,
|
||||||
refresh_cookie_config: RefreshCookieConfig,
|
refresh_cookie_config: RefreshCookieConfig,
|
||||||
oss_client: Option<OssClient>,
|
oss_client: Option<OssClient>,
|
||||||
|
auth_store: InMemoryAuthStore,
|
||||||
password_entry_service: PasswordEntryService,
|
password_entry_service: PasswordEntryService,
|
||||||
refresh_session_service: RefreshSessionService,
|
refresh_session_service: RefreshSessionService,
|
||||||
auth_user_service: AuthUserService,
|
auth_user_service: AuthUserService,
|
||||||
@@ -86,6 +88,7 @@ pub struct AdminSession {
|
|||||||
pub enum AppStateInitError {
|
pub enum AppStateInitError {
|
||||||
Jwt(JwtError),
|
Jwt(JwtError),
|
||||||
RefreshCookie(RefreshCookieError),
|
RefreshCookie(RefreshCookieError),
|
||||||
|
AuthStore(String),
|
||||||
SmsProvider(SmsProviderError),
|
SmsProvider(SmsProviderError),
|
||||||
Oss(OssError),
|
Oss(OssError),
|
||||||
Llm(LlmError),
|
Llm(LlmError),
|
||||||
@@ -93,6 +96,15 @@ pub enum AppStateInitError {
|
|||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(config: AppConfig) -> Result<Self, AppStateInitError> {
|
pub fn new(config: AppConfig) -> Result<Self, AppStateInitError> {
|
||||||
|
let auth_store = InMemoryAuthStore::from_persistence_path(config.auth_store_path.clone())
|
||||||
|
.map_err(AppStateInitError::AuthStore)?;
|
||||||
|
Self::new_with_auth_store(config, auth_store)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_with_auth_store(
|
||||||
|
config: AppConfig,
|
||||||
|
auth_store: InMemoryAuthStore,
|
||||||
|
) -> Result<Self, AppStateInitError> {
|
||||||
let auth_jwt_config = JwtConfig::new(
|
let auth_jwt_config = JwtConfig::new(
|
||||||
config.jwt_issuer.clone(),
|
config.jwt_issuer.clone(),
|
||||||
config.jwt_secret.clone(),
|
config.jwt_secret.clone(),
|
||||||
@@ -111,7 +123,6 @@ impl AppState {
|
|||||||
config.refresh_session_ttl_days,
|
config.refresh_session_ttl_days,
|
||||||
)?;
|
)?;
|
||||||
let oss_client = build_oss_client(&config)?;
|
let oss_client = build_oss_client(&config)?;
|
||||||
let auth_store = InMemoryAuthStore::default();
|
|
||||||
let sms_provider = SmsAuthProvider::new(SmsAuthConfig::new(
|
let sms_provider = SmsAuthProvider::new(SmsAuthConfig::new(
|
||||||
SmsAuthProviderKind::parse(&config.sms_auth_provider).ok_or_else(|| {
|
SmsAuthProviderKind::parse(&config.sms_auth_provider).ok_or_else(|| {
|
||||||
SmsProviderError::InvalidConfig("短信 provider 配置非法".to_string())
|
SmsProviderError::InvalidConfig("短信 provider 配置非法".to_string())
|
||||||
@@ -141,7 +152,7 @@ impl AppState {
|
|||||||
let wechat_auth_service = WechatAuthService::new(auth_store.clone());
|
let wechat_auth_service = WechatAuthService::new(auth_store.clone());
|
||||||
let wechat_provider = build_wechat_provider(&config);
|
let wechat_provider = build_wechat_provider(&config);
|
||||||
let refresh_session_service =
|
let refresh_session_service =
|
||||||
RefreshSessionService::new(auth_store, config.refresh_session_ttl_days);
|
RefreshSessionService::new(auth_store.clone(), config.refresh_session_ttl_days);
|
||||||
// AI 编排服务当前先挂接内存态 store,后续再按 task table / procedure 接到 SpacetimeDB 真相源。
|
// AI 编排服务当前先挂接内存态 store,后续再按 task table / procedure 接到 SpacetimeDB 真相源。
|
||||||
let ai_task_service = AiTaskService::new(InMemoryAiTaskStore::default());
|
let ai_task_service = AiTaskService::new(InMemoryAiTaskStore::default());
|
||||||
let spacetime_client = SpacetimeClient::new(SpacetimeClientConfig {
|
let spacetime_client = SpacetimeClient::new(SpacetimeClientConfig {
|
||||||
@@ -158,6 +169,7 @@ impl AppState {
|
|||||||
admin_runtime,
|
admin_runtime,
|
||||||
refresh_cookie_config,
|
refresh_cookie_config,
|
||||||
oss_client,
|
oss_client,
|
||||||
|
auth_store,
|
||||||
password_entry_service,
|
password_entry_service,
|
||||||
refresh_session_service,
|
refresh_session_service,
|
||||||
auth_user_service,
|
auth_user_service,
|
||||||
@@ -193,6 +205,49 @@ impl AppState {
|
|||||||
&self.password_entry_service
|
&self.password_entry_service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn sync_auth_store_snapshot_to_spacetime(&self) -> Result<(), SpacetimeClientError> {
|
||||||
|
let snapshot_json = self
|
||||||
|
.auth_store
|
||||||
|
.export_snapshot_json()
|
||||||
|
.map_err(SpacetimeClientError::Runtime)?;
|
||||||
|
let updated_at_micros = i64::try_from(
|
||||||
|
OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000,
|
||||||
|
)
|
||||||
|
.map_err(|_| SpacetimeClientError::Runtime("认证快照更新时间超出 i64 范围".to_string()))?;
|
||||||
|
self.spacetime_client
|
||||||
|
.upsert_auth_store_snapshot(snapshot_json, updated_at_micros)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn try_restore_auth_store_from_spacetime(
|
||||||
|
config: AppConfig,
|
||||||
|
) -> Result<Self, AppStateInitError> {
|
||||||
|
let spacetime_client = SpacetimeClient::new(SpacetimeClientConfig {
|
||||||
|
server_url: config.spacetime_server_url.clone(),
|
||||||
|
database: config.spacetime_database.clone(),
|
||||||
|
token: config.spacetime_token.clone(),
|
||||||
|
pool_size: config.spacetime_pool_size,
|
||||||
|
});
|
||||||
|
match spacetime_client.get_auth_store_snapshot().await {
|
||||||
|
Ok(snapshot) => {
|
||||||
|
if let Some(snapshot_json) = snapshot.snapshot_json {
|
||||||
|
if !snapshot_json.trim().is_empty() {
|
||||||
|
let auth_store = InMemoryAuthStore::from_snapshot_json(&snapshot_json)
|
||||||
|
.map_err(AppStateInitError::AuthStore)?;
|
||||||
|
info!("已从 SpacetimeDB 恢复认证快照");
|
||||||
|
return Self::new_with_auth_store(config, auth_store);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
warn!(error = %error, "从 SpacetimeDB 恢复认证快照失败,回退到本地快照");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::new(config)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn refresh_session_service(&self) -> &RefreshSessionService {
|
pub fn refresh_session_service(&self) -> &RefreshSessionService {
|
||||||
&self.refresh_session_service
|
&self.refresh_session_service
|
||||||
}
|
}
|
||||||
@@ -392,6 +447,7 @@ impl fmt::Display for AppStateInitError {
|
|||||||
match self {
|
match self {
|
||||||
Self::Jwt(error) => write!(f, "{error}"),
|
Self::Jwt(error) => write!(f, "{error}"),
|
||||||
Self::RefreshCookie(error) => write!(f, "{error}"),
|
Self::RefreshCookie(error) => write!(f, "{error}"),
|
||||||
|
Self::AuthStore(error) => write!(f, "{error}"),
|
||||||
Self::SmsProvider(error) => write!(f, "{error}"),
|
Self::SmsProvider(error) => write!(f, "{error}"),
|
||||||
Self::Oss(error) => write!(f, "{error}"),
|
Self::Oss(error) => write!(f, "{error}"),
|
||||||
Self::Llm(error) => write!(f, "{error}"),
|
Self::Llm(error) => write!(f, "{error}"),
|
||||||
|
|||||||
@@ -135,6 +135,13 @@ pub async fn handle_wechat_callback(
|
|||||||
&session_client,
|
&session_client,
|
||||||
AuthLoginMethod::Wechat,
|
AuthLoginMethod::Wechat,
|
||||||
)?;
|
)?;
|
||||||
|
state
|
||||||
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_message(format!("同步认证快照失败:{error}"))
|
||||||
|
})?;
|
||||||
let mut response = Redirect::to(&build_auth_result_redirect_url(
|
let mut response = Redirect::to(&build_auth_result_redirect_url(
|
||||||
&redirect_path,
|
&redirect_path,
|
||||||
&[
|
&[
|
||||||
@@ -187,6 +194,13 @@ pub async fn bind_wechat_phone(
|
|||||||
&session_client,
|
&session_client,
|
||||||
AuthLoginMethod::Wechat,
|
AuthLoginMethod::Wechat,
|
||||||
)?;
|
)?;
|
||||||
|
state
|
||||||
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_message(format!("同步认证快照失败:{error}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut response_headers = HeaderMap::new();
|
let mut response_headers = HeaderMap::new();
|
||||||
attach_set_cookie_header(
|
attach_set_cookie_header(
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
platform-auth = { path = "../platform-auth" }
|
platform-auth = { path = "../platform-auth" }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { path = "../shared-kernel" }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
time = { version = "0.3", features = ["formatting", "parsing"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,34 @@ pub struct PasswordEntryResponse {
|
|||||||
pub user: AuthUserPayload,
|
pub user: AuthUserPayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PasswordChangeRequest {
|
||||||
|
pub current_password: Option<String>,
|
||||||
|
pub new_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PasswordChangeResponse {
|
||||||
|
pub user: AuthUserPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PasswordResetRequest {
|
||||||
|
pub phone: String,
|
||||||
|
pub code: String,
|
||||||
|
pub new_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PasswordResetResponse {
|
||||||
|
pub token: String,
|
||||||
|
pub user: AuthUserPayload,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AuthMeResponse {
|
pub struct AuthMeResponse {
|
||||||
@@ -167,7 +195,7 @@ pub fn build_available_login_methods(
|
|||||||
sms_auth_enabled: bool,
|
sms_auth_enabled: bool,
|
||||||
wechat_auth_enabled: bool,
|
wechat_auth_enabled: bool,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let mut methods = Vec::new();
|
let mut methods = vec![AUTH_LOGIN_METHOD_PASSWORD.to_string()];
|
||||||
if sms_auth_enabled {
|
if sms_auth_enabled {
|
||||||
methods.push(AUTH_LOGIN_METHOD_PHONE.to_string());
|
methods.push(AUTH_LOGIN_METHOD_PHONE.to_string());
|
||||||
}
|
}
|
||||||
@@ -189,6 +217,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
methods,
|
methods,
|
||||||
vec![
|
vec![
|
||||||
|
AUTH_LOGIN_METHOD_PASSWORD.to_string(),
|
||||||
AUTH_LOGIN_METHOD_PHONE.to_string(),
|
AUTH_LOGIN_METHOD_PHONE.to_string(),
|
||||||
AUTH_LOGIN_METHOD_WECHAT.to_string()
|
AUTH_LOGIN_METHOD_WECHAT.to_string()
|
||||||
]
|
]
|
||||||
|
|||||||
205
server-rs/crates/spacetime-client/src/ai.rs
Normal file
205
server-rs/crates/spacetime-client/src/ai.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn create_ai_task(
|
||||||
|
&self,
|
||||||
|
input: DomainAiTaskCreateInput,
|
||||||
|
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_ai_task_create_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().create_ai_task_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_ai_task_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn start_ai_task(
|
||||||
|
&self,
|
||||||
|
input: DomainAiTaskStartInput,
|
||||||
|
) -> Result<(), SpacetimeClientError> {
|
||||||
|
let reducer_input = map_ai_task_start_input(input);
|
||||||
|
|
||||||
|
self.call_reducer_after_connect(move |connection, sender| {
|
||||||
|
let callback_sender = sender.clone();
|
||||||
|
if let Err(error) =
|
||||||
|
connection
|
||||||
|
.reducers
|
||||||
|
.start_ai_task_then(reducer_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(|inner| inner.map_err(SpacetimeClientError::Runtime));
|
||||||
|
send_reducer_once(&callback_sender, mapped);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
send_reducer_once(
|
||||||
|
&sender,
|
||||||
|
Err(SpacetimeClientError::Procedure(error.to_string())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn start_ai_task_stage(
|
||||||
|
&self,
|
||||||
|
input: DomainAiTaskStageStartInput,
|
||||||
|
) -> Result<(), SpacetimeClientError> {
|
||||||
|
let reducer_input = map_ai_task_stage_start_input(input);
|
||||||
|
|
||||||
|
self.call_reducer_after_connect(move |connection, sender| {
|
||||||
|
let callback_sender = sender.clone();
|
||||||
|
if let Err(error) =
|
||||||
|
connection
|
||||||
|
.reducers
|
||||||
|
.start_ai_task_stage_then(reducer_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(|inner| inner.map_err(SpacetimeClientError::Runtime));
|
||||||
|
send_reducer_once(&callback_sender, mapped);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
send_reducer_once(
|
||||||
|
&sender,
|
||||||
|
Err(SpacetimeClientError::Procedure(error.to_string())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn append_ai_text_chunk(
|
||||||
|
&self,
|
||||||
|
input: DomainAiTextChunkAppendInput,
|
||||||
|
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_ai_text_chunk_append_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.append_ai_text_chunk_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_ai_task_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn complete_ai_stage(
|
||||||
|
&self,
|
||||||
|
input: DomainAiStageCompletionInput,
|
||||||
|
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_ai_stage_completion_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().complete_ai_stage_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_ai_task_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn attach_ai_result_reference(
|
||||||
|
&self,
|
||||||
|
input: DomainAiResultReferenceInput,
|
||||||
|
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_ai_result_reference_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.attach_ai_result_reference_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_ai_task_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn complete_ai_task(
|
||||||
|
&self,
|
||||||
|
input: DomainAiTaskFinishInput,
|
||||||
|
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_ai_task_finish_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().complete_ai_task_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_ai_task_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn fail_ai_task(
|
||||||
|
&self,
|
||||||
|
input: DomainAiTaskFailureInput,
|
||||||
|
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_ai_task_failure_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().fail_ai_task_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_ai_task_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn cancel_ai_task(
|
||||||
|
&self,
|
||||||
|
input: DomainAiTaskCancelInput,
|
||||||
|
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_ai_task_cancel_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().cancel_ai_task_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_ai_task_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
44
server-rs/crates/spacetime-client/src/assets.rs
Normal file
44
server-rs/crates/spacetime-client/src/assets.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn confirm_asset_object(
|
||||||
|
&self,
|
||||||
|
input: module_assets::AssetObjectUpsertInput,
|
||||||
|
) -> Result<AssetObjectRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_upsert_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.confirm_asset_object_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn bind_asset_object_to_entity(
|
||||||
|
&self,
|
||||||
|
input: module_assets::AssetEntityBindingInput,
|
||||||
|
) -> Result<AssetEntityBindingRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_entity_binding_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.bind_asset_object_to_entity_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_entity_binding_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
46
server-rs/crates/spacetime-client/src/auth.rs
Normal file
46
server-rs/crates/spacetime-client/src/auth.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn get_auth_store_snapshot(
|
||||||
|
&self,
|
||||||
|
) -> Result<AuthStoreSnapshotRecord, SpacetimeClientError> {
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_auth_store_snapshot_then(move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_auth_store_snapshot_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn upsert_auth_store_snapshot(
|
||||||
|
&self,
|
||||||
|
snapshot_json: String,
|
||||||
|
updated_at_micros: i64,
|
||||||
|
) -> Result<AuthStoreSnapshotRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = AuthStoreSnapshotUpsertInput {
|
||||||
|
snapshot_json,
|
||||||
|
updated_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().upsert_auth_store_snapshot_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_auth_store_snapshot_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
263
server-rs/crates/spacetime-client/src/big_fish.rs
Normal file
263
server-rs/crates/spacetime-client/src/big_fish.rs
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn create_big_fish_session(
|
||||||
|
&self,
|
||||||
|
input: BigFishSessionCreateRecordInput,
|
||||||
|
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishSessionCreateInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
seed_text: input.seed_text,
|
||||||
|
welcome_message_id: input.welcome_message_id,
|
||||||
|
welcome_message_text: input.welcome_message_text,
|
||||||
|
created_at_micros: input.created_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().create_big_fish_session_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_big_fish_session(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishSessionGetInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_big_fish_session_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn list_big_fish_works(
|
||||||
|
&self,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishWorksListInput { owner_user_id };
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.list_big_fish_works_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_works_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn submit_big_fish_message(
|
||||||
|
&self,
|
||||||
|
input: BigFishMessageSubmitRecordInput,
|
||||||
|
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishMessageSubmitInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
user_message_id: input.user_message_id,
|
||||||
|
user_message_text: input.user_message_text,
|
||||||
|
assistant_message_id: input.assistant_message_id,
|
||||||
|
submitted_at_micros: input.submitted_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().submit_big_fish_message_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn compile_big_fish_draft(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
compiled_at_micros: i64,
|
||||||
|
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishDraftCompileInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
compiled_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().compile_big_fish_draft_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn generate_big_fish_asset(
|
||||||
|
&self,
|
||||||
|
input: BigFishAssetGenerateRecordInput,
|
||||||
|
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishAssetGenerateInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
asset_kind: map_big_fish_asset_kind_input(input.asset_kind.as_str())?,
|
||||||
|
level: input.level,
|
||||||
|
motion_key: input.motion_key,
|
||||||
|
asset_url: input.asset_url,
|
||||||
|
generated_at_micros: input.generated_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().generate_big_fish_asset_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn publish_big_fish_game(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
published_at_micros: i64,
|
||||||
|
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishPublishInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
published_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().publish_big_fish_game_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn start_big_fish_run(
|
||||||
|
&self,
|
||||||
|
input: BigFishRunStartRecordInput,
|
||||||
|
) -> Result<BigFishRuntimeRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishRunStartInput {
|
||||||
|
run_id: input.run_id,
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
started_at_micros: input.started_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.start_big_fish_run_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_run_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn submit_big_fish_input(
|
||||||
|
&self,
|
||||||
|
input: BigFishRunInputSubmitRecordInput,
|
||||||
|
) -> Result<BigFishRuntimeRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishRunInputSubmitInput {
|
||||||
|
run_id: input.run_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
input_x: input.input_x,
|
||||||
|
input_y: input.input_y,
|
||||||
|
submitted_at_micros: input.submitted_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().submit_big_fish_input_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_run_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_big_fish_run(
|
||||||
|
&self,
|
||||||
|
run_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<BigFishRuntimeRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishRunGetInput {
|
||||||
|
run_id,
|
||||||
|
owner_user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_big_fish_run_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_big_fish_run_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
72
server-rs/crates/spacetime-client/src/combat.rs
Normal file
72
server-rs/crates/spacetime-client/src/combat.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn create_battle_state(
|
||||||
|
&self,
|
||||||
|
input: DomainBattleStateInput,
|
||||||
|
) -> Result<BattleStateRecord, SpacetimeClientError> {
|
||||||
|
validate_battle_state_input(&input)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?;
|
||||||
|
let procedure_input = map_battle_state_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().create_battle_state_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_battle_state_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_battle_state(
|
||||||
|
&self,
|
||||||
|
battle_state_id: String,
|
||||||
|
) -> Result<BattleStateRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_battle_state_query_input(
|
||||||
|
build_battle_state_query_input(battle_state_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_battle_state_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_battle_state_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn resolve_combat_action(
|
||||||
|
&self,
|
||||||
|
input: DomainResolveCombatActionInput,
|
||||||
|
) -> Result<ResolveCombatActionRecord, SpacetimeClientError> {
|
||||||
|
validate_resolve_combat_action_input(&input)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?;
|
||||||
|
let procedure_input = map_resolve_combat_action_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.resolve_combat_action_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_resolve_combat_action_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
481
server-rs/crates/spacetime-client/src/custom_world.rs
Normal file
481
server-rs/crates/spacetime-client/src/custom_world.rs
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn list_custom_world_profiles(
|
||||||
|
&self,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<Vec<CustomWorldLibraryEntryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldProfileListInput { owner_user_id };
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().list_custom_world_profiles_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_profile_list_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_custom_world_library_detail(
|
||||||
|
&self,
|
||||||
|
owner_user_id: String,
|
||||||
|
profile_id: String,
|
||||||
|
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldLibraryDetailInput {
|
||||||
|
owner_user_id,
|
||||||
|
profile_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_custom_world_library_detail_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_library_detail_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn upsert_custom_world_profile(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldProfileUpsertRecordInput,
|
||||||
|
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_custom_world_profile_upsert_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.upsert_custom_world_profile_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_library_mutation_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn publish_custom_world_profile(
|
||||||
|
&self,
|
||||||
|
profile_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
public_work_code: Option<String>,
|
||||||
|
author_public_user_code: String,
|
||||||
|
author_display_name: String,
|
||||||
|
published_at_micros: i64,
|
||||||
|
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldProfilePublishInput {
|
||||||
|
profile_id,
|
||||||
|
owner_user_id,
|
||||||
|
public_work_code,
|
||||||
|
author_public_user_code,
|
||||||
|
author_display_name,
|
||||||
|
published_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.publish_custom_world_profile_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_library_mutation_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn unpublish_custom_world_profile(
|
||||||
|
&self,
|
||||||
|
profile_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
author_display_name: String,
|
||||||
|
updated_at_micros: i64,
|
||||||
|
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldProfileUnpublishInput {
|
||||||
|
profile_id,
|
||||||
|
owner_user_id,
|
||||||
|
author_display_name,
|
||||||
|
updated_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.unpublish_custom_world_profile_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_library_mutation_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn delete_custom_world_profile(
|
||||||
|
&self,
|
||||||
|
profile_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
deleted_at_micros: i64,
|
||||||
|
) -> Result<Vec<CustomWorldLibraryEntryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldProfileDeleteInput {
|
||||||
|
profile_id,
|
||||||
|
owner_user_id,
|
||||||
|
deleted_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.delete_custom_world_profile_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_profile_list_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn list_custom_world_gallery_entries(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<CustomWorldGalleryEntryRecord>, SpacetimeClientError> {
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.list_custom_world_gallery_entries_then(move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_gallery_list_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_custom_world_gallery_detail(
|
||||||
|
&self,
|
||||||
|
owner_user_id: String,
|
||||||
|
profile_id: String,
|
||||||
|
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldGalleryDetailInput {
|
||||||
|
owner_user_id,
|
||||||
|
profile_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_custom_world_gallery_detail_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_library_mutation_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_custom_world_gallery_detail_by_code(
|
||||||
|
&self,
|
||||||
|
public_work_code: String,
|
||||||
|
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldGalleryDetailByCodeInput { public_work_code };
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_custom_world_gallery_detail_by_code_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_library_mutation_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn publish_custom_world_world(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldPublishWorldRecordInput,
|
||||||
|
) -> Result<CustomWorldPublishWorldRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_custom_world_publish_world_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().publish_custom_world_world_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_publish_world_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn create_custom_world_agent_session(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentSessionCreateRecordInput,
|
||||||
|
) -> Result<CustomWorldAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldAgentSessionCreateInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
seed_text: input.seed_text,
|
||||||
|
welcome_message_id: input.welcome_message_id,
|
||||||
|
welcome_message_text: input.welcome_message_text,
|
||||||
|
anchor_content_json: input.anchor_content_json,
|
||||||
|
creator_intent_json: input.creator_intent_json,
|
||||||
|
creator_intent_readiness_json: input.creator_intent_readiness_json,
|
||||||
|
anchor_pack_json: input.anchor_pack_json,
|
||||||
|
lock_state_json: input.lock_state_json,
|
||||||
|
draft_profile_json: input.draft_profile_json,
|
||||||
|
pending_clarifications_json: input.pending_clarifications_json,
|
||||||
|
suggested_actions_json: input.suggested_actions_json,
|
||||||
|
recommended_replies_json: input.recommended_replies_json,
|
||||||
|
quality_findings_json: input.quality_findings_json,
|
||||||
|
asset_coverage_json: input.asset_coverage_json,
|
||||||
|
checkpoints_json: input.checkpoints_json,
|
||||||
|
created_at_micros: input.created_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.create_custom_world_agent_session_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_custom_world_agent_session(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<CustomWorldAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldAgentSessionGetInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_custom_world_agent_session_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn list_custom_world_works(
|
||||||
|
&self,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<Vec<CustomWorldWorkSummaryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldWorksListInput { owner_user_id };
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().list_custom_world_works_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_works_list_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_custom_world_agent_card_detail(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
card_id: String,
|
||||||
|
) -> Result<CustomWorldDraftCardDetailRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldAgentCardDetailGetInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
card_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_custom_world_agent_card_detail_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_draft_card_detail_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn execute_custom_world_agent_action(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentActionExecuteRecordInput,
|
||||||
|
) -> Result<CustomWorldAgentActionExecuteRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldAgentActionExecuteInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
operation_id: input.operation_id,
|
||||||
|
action: input.action,
|
||||||
|
payload_json: input.payload_json,
|
||||||
|
submitted_at_micros: input.submitted_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.execute_custom_world_agent_action_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_agent_action_execute_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn submit_custom_world_agent_message(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentMessageSubmitRecordInput,
|
||||||
|
) -> Result<CustomWorldAgentOperationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldAgentMessageSubmitInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
user_message_id: input.user_message_id,
|
||||||
|
user_message_text: input.user_message_text,
|
||||||
|
operation_id: input.operation_id,
|
||||||
|
submitted_at_micros: input.submitted_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.submit_custom_world_agent_message_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_agent_operation_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn finalize_custom_world_agent_message(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentMessageFinalizeRecordInput,
|
||||||
|
) -> Result<CustomWorldAgentOperationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldAgentMessageFinalizeInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
operation_id: input.operation_id,
|
||||||
|
assistant_message_id: input.assistant_message_id,
|
||||||
|
assistant_reply_text: input.assistant_reply_text,
|
||||||
|
phase_label: input.phase_label,
|
||||||
|
phase_detail: input.phase_detail,
|
||||||
|
operation_status: parse_rpg_agent_operation_status_record(
|
||||||
|
input.operation_status.as_str(),
|
||||||
|
)?,
|
||||||
|
operation_progress: input.operation_progress,
|
||||||
|
stage: parse_rpg_agent_stage_record(input.stage.as_str())?,
|
||||||
|
progress_percent: input.progress_percent,
|
||||||
|
focus_card_id: input.focus_card_id,
|
||||||
|
anchor_content_json: input.anchor_content_json,
|
||||||
|
creator_intent_json: input.creator_intent_json,
|
||||||
|
creator_intent_readiness_json: input.creator_intent_readiness_json,
|
||||||
|
anchor_pack_json: input.anchor_pack_json,
|
||||||
|
draft_profile_json: input.draft_profile_json,
|
||||||
|
pending_clarifications_json: input.pending_clarifications_json,
|
||||||
|
suggested_actions_json: input.suggested_actions_json,
|
||||||
|
recommended_replies_json: input.recommended_replies_json,
|
||||||
|
quality_findings_json: input.quality_findings_json,
|
||||||
|
asset_coverage_json: input.asset_coverage_json,
|
||||||
|
error_message: input.error_message,
|
||||||
|
updated_at_micros: input.updated_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.finalize_custom_world_agent_message_turn_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_agent_operation_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_custom_world_agent_operation(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
operation_id: String,
|
||||||
|
) -> Result<CustomWorldAgentOperationRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = CustomWorldAgentOperationGetInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
operation_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_custom_world_agent_operation_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_agent_operation_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
29
server-rs/crates/spacetime-client/src/inventory.rs
Normal file
29
server-rs/crates/spacetime-client/src/inventory.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn get_runtime_inventory_state(
|
||||||
|
&self,
|
||||||
|
runtime_session_id: String,
|
||||||
|
actor_user_id: String,
|
||||||
|
) -> Result<RuntimeInventoryStateRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_inventory_state_query_input(
|
||||||
|
build_runtime_inventory_state_query_input(runtime_session_id, actor_user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_runtime_inventory_state_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_inventory_state_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,21 @@
|
|||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub mod module_bindings;
|
pub mod module_bindings;
|
||||||
|
|
||||||
|
mod mapper;
|
||||||
|
pub use mapper::*;
|
||||||
|
|
||||||
|
pub mod ai;
|
||||||
|
pub mod assets;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod big_fish;
|
||||||
|
pub mod combat;
|
||||||
|
pub mod custom_world;
|
||||||
|
pub mod inventory;
|
||||||
|
pub mod npc;
|
||||||
|
pub mod puzzle;
|
||||||
|
pub mod runtime;
|
||||||
|
pub mod story;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt,
|
fmt,
|
||||||
@@ -69,9 +84,11 @@ use module_puzzle::{
|
|||||||
PuzzleWorkProfile as DomainPuzzleWorkProfile,
|
PuzzleWorkProfile as DomainPuzzleWorkProfile,
|
||||||
};
|
};
|
||||||
use module_runtime::{
|
use module_runtime::{
|
||||||
RuntimeBrowseHistoryRecord, RuntimeBrowseHistoryThemeMode, RuntimePlatformTheme,
|
RuntimeBrowseHistoryRecord, RuntimeBrowseHistoryThemeMode as DomainRuntimeBrowseHistoryThemeMode,
|
||||||
|
RuntimePlatformTheme as DomainRuntimePlatformTheme,
|
||||||
RuntimeProfileDashboardRecord, RuntimeProfilePlayStatsRecord, RuntimeProfileSaveArchiveRecord,
|
RuntimeProfileDashboardRecord, RuntimeProfilePlayStatsRecord, RuntimeProfileSaveArchiveRecord,
|
||||||
RuntimeProfileWalletLedgerEntryRecord, RuntimeProfileWalletLedgerSourceType,
|
RuntimeProfileWalletLedgerEntryRecord,
|
||||||
|
RuntimeProfileWalletLedgerSourceType as DomainRuntimeProfileWalletLedgerSourceType,
|
||||||
RuntimeSettingsRecord, RuntimeSnapshotRecord, build_runtime_browse_history_clear_input,
|
RuntimeSettingsRecord, RuntimeSnapshotRecord, build_runtime_browse_history_clear_input,
|
||||||
build_runtime_browse_history_list_input, build_runtime_browse_history_record,
|
build_runtime_browse_history_list_input, build_runtime_browse_history_record,
|
||||||
build_runtime_browse_history_sync_input, build_runtime_profile_dashboard_get_input,
|
build_runtime_browse_history_sync_input, build_runtime_profile_dashboard_get_input,
|
||||||
@@ -104,6 +121,7 @@ use tokio::{
|
|||||||
time::timeout,
|
time::timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
use crate::module_bindings::{
|
use crate::module_bindings::{
|
||||||
AiResultReferenceInput as BindingAiResultReferenceInput,
|
AiResultReferenceInput as BindingAiResultReferenceInput,
|
||||||
AiResultReferenceKind as BindingAiResultReferenceKind,
|
AiResultReferenceKind as BindingAiResultReferenceKind,
|
||||||
@@ -360,6 +378,9 @@ use crate::module_bindings::{
|
|||||||
upsert_runtime_setting_and_return_procedure::upsert_runtime_setting_and_return as _,
|
upsert_runtime_setting_and_return_procedure::upsert_runtime_setting_and_return as _,
|
||||||
upsert_runtime_snapshot_and_return_procedure::upsert_runtime_snapshot_and_return as _,
|
upsert_runtime_snapshot_and_return_procedure::upsert_runtime_snapshot_and_return as _,
|
||||||
};
|
};
|
||||||
|
=======
|
||||||
|
use crate::module_bindings::*;
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SpacetimeClientConfig {
|
pub struct SpacetimeClientConfig {
|
||||||
@@ -369,6 +390,12 @@ pub struct SpacetimeClientConfig {
|
|||||||
pub pool_size: u32,
|
pub pool_size: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct AuthStoreSnapshotRecord {
|
||||||
|
pub snapshot_json: Option<String>,
|
||||||
|
pub updated_at_micros: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SpacetimeClient {
|
pub struct SpacetimeClient {
|
||||||
config: SpacetimeClientConfig,
|
config: SpacetimeClientConfig,
|
||||||
@@ -431,6 +458,7 @@ impl SpacetimeClient {
|
|||||||
Self { config, pool }
|
Self { config, pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
pub async fn create_ai_task(
|
pub async fn create_ai_task(
|
||||||
&self,
|
&self,
|
||||||
input: DomainAiTaskCreateInput,
|
input: DomainAiTaskCreateInput,
|
||||||
@@ -2362,6 +2390,8 @@ impl SpacetimeClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
async fn call_after_connect<T>(
|
async fn call_after_connect<T>(
|
||||||
&self,
|
&self,
|
||||||
call: impl FnOnce(&DbConnection, ProcedureResultSender<T>) + Send + 'static,
|
call: impl FnOnce(&DbConnection, ProcedureResultSender<T>) + Send + 'static,
|
||||||
@@ -2596,6 +2626,7 @@ fn send_connect_once(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
fn map_entity_binding_input(
|
fn map_entity_binding_input(
|
||||||
input: module_assets::AssetEntityBindingInput,
|
input: module_assets::AssetEntityBindingInput,
|
||||||
) -> BindingAssetEntityBindingInput {
|
) -> BindingAssetEntityBindingInput {
|
||||||
@@ -7089,6 +7120,8 @@ fn map_inventory_item_source_kind(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
impl fmt::Display for SpacetimeClientError {
|
impl fmt::Display for SpacetimeClientError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
4494
server-rs/crates/spacetime-client/src/mapper.rs
Normal file
4494
server-rs/crates/spacetime-client/src/mapper.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::auth_store_snapshot_record_type::AuthStoreSnapshotRecord;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct AuthStoreSnapshotProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub record: Option::<AuthStoreSnapshotRecord>,
|
||||||
|
pub error_message: Option::<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for AuthStoreSnapshotProcedureResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct AuthStoreSnapshotRecord {
|
||||||
|
pub snapshot_json: Option::<String>,
|
||||||
|
pub updated_at_micros: Option::<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for AuthStoreSnapshotRecord {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
use super::auth_store_snapshot_type::AuthStoreSnapshot;
|
||||||
|
|
||||||
|
/// Table handle for the table `auth_store_snapshot`.
|
||||||
|
///
|
||||||
|
/// Obtain a handle from the [`AuthStoreSnapshotTableAccess::auth_store_snapshot`] method on [`super::RemoteTables`],
|
||||||
|
/// like `ctx.db.auth_store_snapshot()`.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.auth_store_snapshot().on_insert(...)`.
|
||||||
|
pub struct AuthStoreSnapshotTableHandle<'ctx> {
|
||||||
|
imp: __sdk::TableHandle<AuthStoreSnapshot>,
|
||||||
|
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the table `auth_store_snapshot`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteTables`].
|
||||||
|
pub trait AuthStoreSnapshotTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Obtain a [`AuthStoreSnapshotTableHandle`], which mediates access to the table `auth_store_snapshot`.
|
||||||
|
fn auth_store_snapshot(&self) -> AuthStoreSnapshotTableHandle<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthStoreSnapshotTableAccess for super::RemoteTables {
|
||||||
|
fn auth_store_snapshot(&self) -> AuthStoreSnapshotTableHandle<'_> {
|
||||||
|
AuthStoreSnapshotTableHandle {
|
||||||
|
imp: self.imp.get_table::<AuthStoreSnapshot>("auth_store_snapshot"),
|
||||||
|
ctx: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthStoreSnapshotInsertCallbackId(__sdk::CallbackId);
|
||||||
|
pub struct AuthStoreSnapshotDeleteCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::Table for AuthStoreSnapshotTableHandle<'ctx> {
|
||||||
|
type Row = AuthStoreSnapshot;
|
||||||
|
type EventContext = super::EventContext;
|
||||||
|
|
||||||
|
fn count(&self) -> u64 { self.imp.count() }
|
||||||
|
fn iter(&self) -> impl Iterator<Item = AuthStoreSnapshot> + '_ { self.imp.iter() }
|
||||||
|
|
||||||
|
type InsertCallbackId = AuthStoreSnapshotInsertCallbackId;
|
||||||
|
|
||||||
|
fn on_insert(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> AuthStoreSnapshotInsertCallbackId {
|
||||||
|
AuthStoreSnapshotInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_insert(&self, callback: AuthStoreSnapshotInsertCallbackId) {
|
||||||
|
self.imp.remove_on_insert(callback.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCallbackId = AuthStoreSnapshotDeleteCallbackId;
|
||||||
|
|
||||||
|
fn on_delete(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> AuthStoreSnapshotDeleteCallbackId {
|
||||||
|
AuthStoreSnapshotDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_delete(&self, callback: AuthStoreSnapshotDeleteCallbackId) {
|
||||||
|
self.imp.remove_on_delete(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthStoreSnapshotUpdateCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::TableWithPrimaryKey for AuthStoreSnapshotTableHandle<'ctx> {
|
||||||
|
type UpdateCallbackId = AuthStoreSnapshotUpdateCallbackId;
|
||||||
|
|
||||||
|
fn on_update(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||||
|
) -> AuthStoreSnapshotUpdateCallbackId {
|
||||||
|
AuthStoreSnapshotUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_update(&self, callback: AuthStoreSnapshotUpdateCallbackId) {
|
||||||
|
self.imp.remove_on_update(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to the `snapshot_id` unique index on the table `auth_store_snapshot`,
|
||||||
|
/// which allows point queries on the field of the same name
|
||||||
|
/// via the [`AuthStoreSnapshotSnapshotIdUnique::find`] method.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.auth_store_snapshot().snapshot_id().find(...)`.
|
||||||
|
pub struct AuthStoreSnapshotSnapshotIdUnique<'ctx> {
|
||||||
|
imp: __sdk::UniqueConstraintHandle<AuthStoreSnapshot, String>,
|
||||||
|
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> AuthStoreSnapshotTableHandle<'ctx> {
|
||||||
|
/// Get a handle on the `snapshot_id` unique index on the table `auth_store_snapshot`.
|
||||||
|
pub fn snapshot_id(&self) -> AuthStoreSnapshotSnapshotIdUnique<'ctx> {
|
||||||
|
AuthStoreSnapshotSnapshotIdUnique {
|
||||||
|
imp: self.imp.get_unique_constraint::<String>("snapshot_id"),
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> AuthStoreSnapshotSnapshotIdUnique<'ctx> {
|
||||||
|
/// Find the subscribed row whose `snapshot_id` column value is equal to `col_val`,
|
||||||
|
/// if such a row is present in the client cache.
|
||||||
|
pub fn find(&self, col_val: &String) -> Option<AuthStoreSnapshot> {
|
||||||
|
self.imp.find(col_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||||
|
|
||||||
|
let _table = client_cache.get_or_make_table::<AuthStoreSnapshot>("auth_store_snapshot");
|
||||||
|
_table.add_unique_constraint::<String>("snapshot_id", |row| &row.snapshot_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn parse_table_update(
|
||||||
|
raw_updates: __ws::v2::TableUpdate,
|
||||||
|
) -> __sdk::Result<__sdk::TableUpdate<AuthStoreSnapshot>> {
|
||||||
|
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||||
|
__sdk::InternalError::failed_parse(
|
||||||
|
"TableUpdate<AuthStoreSnapshot>",
|
||||||
|
"TableUpdate",
|
||||||
|
).with_cause(e).into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for query builder access to the table `AuthStoreSnapshot`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||||
|
pub trait auth_store_snapshotQueryTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Get a query builder for the table `AuthStoreSnapshot`.
|
||||||
|
fn auth_store_snapshot(&self) -> __sdk::__query_builder::Table<AuthStoreSnapshot>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl auth_store_snapshotQueryTableAccess for __sdk::QueryTableAccessor {
|
||||||
|
fn auth_store_snapshot(&self) -> __sdk::__query_builder::Table<AuthStoreSnapshot> {
|
||||||
|
__sdk::__query_builder::Table::new("auth_store_snapshot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct AuthStoreSnapshot {
|
||||||
|
pub snapshot_id: String,
|
||||||
|
pub snapshot_json: String,
|
||||||
|
pub updated_at: __sdk::Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for AuthStoreSnapshot {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `AuthStoreSnapshot`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct AuthStoreSnapshotCols {
|
||||||
|
pub snapshot_id: __sdk::__query_builder::Col<AuthStoreSnapshot, String>,
|
||||||
|
pub snapshot_json: __sdk::__query_builder::Col<AuthStoreSnapshot, String>,
|
||||||
|
pub updated_at: __sdk::__query_builder::Col<AuthStoreSnapshot, __sdk::Timestamp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for AuthStoreSnapshot {
|
||||||
|
type Cols = AuthStoreSnapshotCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
AuthStoreSnapshotCols {
|
||||||
|
snapshot_id: __sdk::__query_builder::Col::new(table_name, "snapshot_id"),
|
||||||
|
snapshot_json: __sdk::__query_builder::Col::new(table_name, "snapshot_json"),
|
||||||
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `AuthStoreSnapshot`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct AuthStoreSnapshotIxCols {
|
||||||
|
pub snapshot_id: __sdk::__query_builder::IxCol<AuthStoreSnapshot, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for AuthStoreSnapshot {
|
||||||
|
type IxCols = AuthStoreSnapshotIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
AuthStoreSnapshotIxCols {
|
||||||
|
snapshot_id: __sdk::__query_builder::IxCol::new(table_name, "snapshot_id"),
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for AuthStoreSnapshot {}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct AuthStoreSnapshotUpsertInput {
|
||||||
|
pub snapshot_json: String,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for AuthStoreSnapshotUpsertInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::auth_store_snapshot_procedure_result_type::AuthStoreSnapshotProcedureResult;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct GetAuthStoreSnapshotArgs {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for GetAuthStoreSnapshotArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `get_auth_store_snapshot`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait get_auth_store_snapshot {
|
||||||
|
fn get_auth_store_snapshot(&self, ) {
|
||||||
|
self.get_auth_store_snapshot_then( |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_auth_store_snapshot_then(
|
||||||
|
&self,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<AuthStoreSnapshotProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl get_auth_store_snapshot for super::RemoteProcedures {
|
||||||
|
fn get_auth_store_snapshot_then(
|
||||||
|
&self,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<AuthStoreSnapshotProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
) {
|
||||||
|
self.imp.invoke_procedure_with_callback::<_, AuthStoreSnapshotProcedureResult>(
|
||||||
|
"get_auth_store_snapshot",
|
||||||
|
GetAuthStoreSnapshotArgs { },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -44,6 +44,13 @@ pub mod asset_object_access_policy_type;
|
|||||||
pub mod asset_object_procedure_result_type;
|
pub mod asset_object_procedure_result_type;
|
||||||
pub mod asset_object_upsert_input_type;
|
pub mod asset_object_upsert_input_type;
|
||||||
pub mod asset_object_upsert_snapshot_type;
|
pub mod asset_object_upsert_snapshot_type;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
pub mod auth_store_snapshot_type;
|
||||||
|
pub mod auth_store_snapshot_procedure_result_type;
|
||||||
|
pub mod auth_store_snapshot_record_type;
|
||||||
|
pub mod auth_store_snapshot_upsert_input_type;
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
pub mod battle_mode_type;
|
pub mod battle_mode_type;
|
||||||
pub mod battle_state_type;
|
pub mod battle_state_type;
|
||||||
pub mod battle_state_input_type;
|
pub mod battle_state_input_type;
|
||||||
@@ -339,6 +346,10 @@ pub mod ai_task_stage_table;
|
|||||||
pub mod ai_text_chunk_table;
|
pub mod ai_text_chunk_table;
|
||||||
pub mod asset_entity_binding_table;
|
pub mod asset_entity_binding_table;
|
||||||
pub mod asset_object_table;
|
pub mod asset_object_table;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
pub mod auth_store_snapshot_table;
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
pub mod battle_state_table;
|
pub mod battle_state_table;
|
||||||
pub mod big_fish_agent_message_table;
|
pub mod big_fish_agent_message_table;
|
||||||
pub mod big_fish_asset_slot_table;
|
pub mod big_fish_asset_slot_table;
|
||||||
@@ -396,10 +407,17 @@ pub mod delete_runtime_snapshot_and_return_procedure;
|
|||||||
pub mod drag_puzzle_piece_or_group_procedure;
|
pub mod drag_puzzle_piece_or_group_procedure;
|
||||||
pub mod execute_custom_world_agent_action_procedure;
|
pub mod execute_custom_world_agent_action_procedure;
|
||||||
pub mod fail_ai_task_and_return_procedure;
|
pub mod fail_ai_task_and_return_procedure;
|
||||||
|
<<<<<<< HEAD
|
||||||
pub mod finalize_big_fish_agent_message_turn_procedure;
|
pub mod finalize_big_fish_agent_message_turn_procedure;
|
||||||
pub mod finalize_custom_world_agent_message_turn_procedure;
|
pub mod finalize_custom_world_agent_message_turn_procedure;
|
||||||
pub mod finalize_puzzle_agent_message_turn_procedure;
|
pub mod finalize_puzzle_agent_message_turn_procedure;
|
||||||
pub mod generate_big_fish_asset_procedure;
|
pub mod generate_big_fish_asset_procedure;
|
||||||
|
=======
|
||||||
|
pub mod finalize_custom_world_agent_message_turn_procedure;
|
||||||
|
pub mod finalize_puzzle_agent_message_turn_procedure;
|
||||||
|
pub mod generate_big_fish_asset_procedure;
|
||||||
|
pub mod get_auth_store_snapshot_procedure;
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
pub mod get_battle_state_procedure;
|
pub mod get_battle_state_procedure;
|
||||||
pub mod get_big_fish_run_procedure;
|
pub mod get_big_fish_run_procedure;
|
||||||
pub mod get_big_fish_session_procedure;
|
pub mod get_big_fish_session_procedure;
|
||||||
@@ -452,6 +470,7 @@ pub mod submit_puzzle_agent_message_procedure;
|
|||||||
pub mod swap_puzzle_pieces_procedure;
|
pub mod swap_puzzle_pieces_procedure;
|
||||||
pub mod unpublish_custom_world_profile_and_return_procedure;
|
pub mod unpublish_custom_world_profile_and_return_procedure;
|
||||||
pub mod update_puzzle_work_procedure;
|
pub mod update_puzzle_work_procedure;
|
||||||
|
pub mod upsert_auth_store_snapshot_procedure;
|
||||||
pub mod upsert_chapter_progression_and_return_procedure;
|
pub mod upsert_chapter_progression_and_return_procedure;
|
||||||
pub mod upsert_custom_world_profile_and_return_procedure;
|
pub mod upsert_custom_world_profile_and_return_procedure;
|
||||||
pub mod upsert_npc_state_and_return_procedure;
|
pub mod upsert_npc_state_and_return_procedure;
|
||||||
@@ -492,6 +511,13 @@ pub use asset_object_access_policy_type::AssetObjectAccessPolicy;
|
|||||||
pub use asset_object_procedure_result_type::AssetObjectProcedureResult;
|
pub use asset_object_procedure_result_type::AssetObjectProcedureResult;
|
||||||
pub use asset_object_upsert_input_type::AssetObjectUpsertInput;
|
pub use asset_object_upsert_input_type::AssetObjectUpsertInput;
|
||||||
pub use asset_object_upsert_snapshot_type::AssetObjectUpsertSnapshot;
|
pub use asset_object_upsert_snapshot_type::AssetObjectUpsertSnapshot;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
pub use auth_store_snapshot_type::AuthStoreSnapshot;
|
||||||
|
pub use auth_store_snapshot_procedure_result_type::AuthStoreSnapshotProcedureResult;
|
||||||
|
pub use auth_store_snapshot_record_type::AuthStoreSnapshotRecord;
|
||||||
|
pub use auth_store_snapshot_upsert_input_type::AuthStoreSnapshotUpsertInput;
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
pub use battle_mode_type::BattleMode;
|
pub use battle_mode_type::BattleMode;
|
||||||
pub use battle_state_type::BattleState;
|
pub use battle_state_type::BattleState;
|
||||||
pub use battle_state_input_type::BattleStateInput;
|
pub use battle_state_input_type::BattleStateInput;
|
||||||
@@ -763,6 +789,10 @@ pub use ai_task_stage_table::*;
|
|||||||
pub use ai_text_chunk_table::*;
|
pub use ai_text_chunk_table::*;
|
||||||
pub use asset_entity_binding_table::*;
|
pub use asset_entity_binding_table::*;
|
||||||
pub use asset_object_table::*;
|
pub use asset_object_table::*;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
pub use auth_store_snapshot_table::*;
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
pub use battle_state_table::*;
|
pub use battle_state_table::*;
|
||||||
pub use big_fish_agent_message_table::*;
|
pub use big_fish_agent_message_table::*;
|
||||||
pub use big_fish_asset_slot_table::*;
|
pub use big_fish_asset_slot_table::*;
|
||||||
@@ -844,10 +874,17 @@ pub use delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_an
|
|||||||
pub use drag_puzzle_piece_or_group_procedure::drag_puzzle_piece_or_group;
|
pub use drag_puzzle_piece_or_group_procedure::drag_puzzle_piece_or_group;
|
||||||
pub use execute_custom_world_agent_action_procedure::execute_custom_world_agent_action;
|
pub use execute_custom_world_agent_action_procedure::execute_custom_world_agent_action;
|
||||||
pub use fail_ai_task_and_return_procedure::fail_ai_task_and_return;
|
pub use fail_ai_task_and_return_procedure::fail_ai_task_and_return;
|
||||||
|
<<<<<<< HEAD
|
||||||
pub use finalize_big_fish_agent_message_turn_procedure::finalize_big_fish_agent_message_turn;
|
pub use finalize_big_fish_agent_message_turn_procedure::finalize_big_fish_agent_message_turn;
|
||||||
pub use finalize_custom_world_agent_message_turn_procedure::finalize_custom_world_agent_message_turn;
|
pub use finalize_custom_world_agent_message_turn_procedure::finalize_custom_world_agent_message_turn;
|
||||||
pub use finalize_puzzle_agent_message_turn_procedure::finalize_puzzle_agent_message_turn;
|
pub use finalize_puzzle_agent_message_turn_procedure::finalize_puzzle_agent_message_turn;
|
||||||
pub use generate_big_fish_asset_procedure::generate_big_fish_asset;
|
pub use generate_big_fish_asset_procedure::generate_big_fish_asset;
|
||||||
|
=======
|
||||||
|
pub use finalize_custom_world_agent_message_turn_procedure::finalize_custom_world_agent_message_turn;
|
||||||
|
pub use finalize_puzzle_agent_message_turn_procedure::finalize_puzzle_agent_message_turn;
|
||||||
|
pub use generate_big_fish_asset_procedure::generate_big_fish_asset;
|
||||||
|
pub use get_auth_store_snapshot_procedure::get_auth_store_snapshot;
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
pub use get_battle_state_procedure::get_battle_state;
|
pub use get_battle_state_procedure::get_battle_state;
|
||||||
pub use get_big_fish_run_procedure::get_big_fish_run;
|
pub use get_big_fish_run_procedure::get_big_fish_run;
|
||||||
pub use get_big_fish_session_procedure::get_big_fish_session;
|
pub use get_big_fish_session_procedure::get_big_fish_session;
|
||||||
@@ -900,6 +937,7 @@ pub use submit_puzzle_agent_message_procedure::submit_puzzle_agent_message;
|
|||||||
pub use swap_puzzle_pieces_procedure::swap_puzzle_pieces;
|
pub use swap_puzzle_pieces_procedure::swap_puzzle_pieces;
|
||||||
pub use unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return;
|
pub use unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return;
|
||||||
pub use update_puzzle_work_procedure::update_puzzle_work;
|
pub use update_puzzle_work_procedure::update_puzzle_work;
|
||||||
|
pub use upsert_auth_store_snapshot_procedure::upsert_auth_store_snapshot;
|
||||||
pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return;
|
pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return;
|
||||||
pub use upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return;
|
pub use upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return;
|
||||||
pub use upsert_npc_state_and_return_procedure::upsert_npc_state_and_return;
|
pub use upsert_npc_state_and_return_procedure::upsert_npc_state_and_return;
|
||||||
@@ -1162,6 +1200,7 @@ pub struct DbUpdate {
|
|||||||
ai_text_chunk: __sdk::TableUpdate<AiTextChunk>,
|
ai_text_chunk: __sdk::TableUpdate<AiTextChunk>,
|
||||||
asset_entity_binding: __sdk::TableUpdate<AssetEntityBinding>,
|
asset_entity_binding: __sdk::TableUpdate<AssetEntityBinding>,
|
||||||
asset_object: __sdk::TableUpdate<AssetObject>,
|
asset_object: __sdk::TableUpdate<AssetObject>,
|
||||||
|
auth_store_snapshot: __sdk::TableUpdate<AuthStoreSnapshot>,
|
||||||
battle_state: __sdk::TableUpdate<BattleState>,
|
battle_state: __sdk::TableUpdate<BattleState>,
|
||||||
big_fish_agent_message: __sdk::TableUpdate<BigFishAgentMessage>,
|
big_fish_agent_message: __sdk::TableUpdate<BigFishAgentMessage>,
|
||||||
big_fish_asset_slot: __sdk::TableUpdate<BigFishAssetSlot>,
|
big_fish_asset_slot: __sdk::TableUpdate<BigFishAssetSlot>,
|
||||||
@@ -1210,6 +1249,10 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
|||||||
"ai_text_chunk" => db_update.ai_text_chunk.append(ai_text_chunk_table::parse_table_update(table_update)?),
|
"ai_text_chunk" => db_update.ai_text_chunk.append(ai_text_chunk_table::parse_table_update(table_update)?),
|
||||||
"asset_entity_binding" => db_update.asset_entity_binding.append(asset_entity_binding_table::parse_table_update(table_update)?),
|
"asset_entity_binding" => db_update.asset_entity_binding.append(asset_entity_binding_table::parse_table_update(table_update)?),
|
||||||
"asset_object" => db_update.asset_object.append(asset_object_table::parse_table_update(table_update)?),
|
"asset_object" => db_update.asset_object.append(asset_object_table::parse_table_update(table_update)?),
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
"auth_store_snapshot" => db_update.auth_store_snapshot.append(auth_store_snapshot_table::parse_table_update(table_update)?),
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
"battle_state" => db_update.battle_state.append(battle_state_table::parse_table_update(table_update)?),
|
"battle_state" => db_update.battle_state.append(battle_state_table::parse_table_update(table_update)?),
|
||||||
"big_fish_agent_message" => db_update.big_fish_agent_message.append(big_fish_agent_message_table::parse_table_update(table_update)?),
|
"big_fish_agent_message" => db_update.big_fish_agent_message.append(big_fish_agent_message_table::parse_table_update(table_update)?),
|
||||||
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(big_fish_asset_slot_table::parse_table_update(table_update)?),
|
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(big_fish_asset_slot_table::parse_table_update(table_update)?),
|
||||||
@@ -1270,6 +1313,10 @@ impl __sdk::DbUpdate for DbUpdate {
|
|||||||
diff.ai_text_chunk = cache.apply_diff_to_table::<AiTextChunk>("ai_text_chunk", &self.ai_text_chunk).with_updates_by_pk(|row| &row.text_chunk_row_id);
|
diff.ai_text_chunk = cache.apply_diff_to_table::<AiTextChunk>("ai_text_chunk", &self.ai_text_chunk).with_updates_by_pk(|row| &row.text_chunk_row_id);
|
||||||
diff.asset_entity_binding = cache.apply_diff_to_table::<AssetEntityBinding>("asset_entity_binding", &self.asset_entity_binding).with_updates_by_pk(|row| &row.binding_id);
|
diff.asset_entity_binding = cache.apply_diff_to_table::<AssetEntityBinding>("asset_entity_binding", &self.asset_entity_binding).with_updates_by_pk(|row| &row.binding_id);
|
||||||
diff.asset_object = cache.apply_diff_to_table::<AssetObject>("asset_object", &self.asset_object).with_updates_by_pk(|row| &row.asset_object_id);
|
diff.asset_object = cache.apply_diff_to_table::<AssetObject>("asset_object", &self.asset_object).with_updates_by_pk(|row| &row.asset_object_id);
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
diff.auth_store_snapshot = cache.apply_diff_to_table::<AuthStoreSnapshot>("auth_store_snapshot", &self.auth_store_snapshot).with_updates_by_pk(|row| &row.snapshot_id);
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
diff.battle_state = cache.apply_diff_to_table::<BattleState>("battle_state", &self.battle_state).with_updates_by_pk(|row| &row.battle_state_id);
|
diff.battle_state = cache.apply_diff_to_table::<BattleState>("battle_state", &self.battle_state).with_updates_by_pk(|row| &row.battle_state_id);
|
||||||
diff.big_fish_agent_message = cache.apply_diff_to_table::<BigFishAgentMessage>("big_fish_agent_message", &self.big_fish_agent_message).with_updates_by_pk(|row| &row.message_id);
|
diff.big_fish_agent_message = cache.apply_diff_to_table::<BigFishAgentMessage>("big_fish_agent_message", &self.big_fish_agent_message).with_updates_by_pk(|row| &row.message_id);
|
||||||
diff.big_fish_asset_slot = cache.apply_diff_to_table::<BigFishAssetSlot>("big_fish_asset_slot", &self.big_fish_asset_slot).with_updates_by_pk(|row| &row.slot_id);
|
diff.big_fish_asset_slot = cache.apply_diff_to_table::<BigFishAssetSlot>("big_fish_asset_slot", &self.big_fish_asset_slot).with_updates_by_pk(|row| &row.slot_id);
|
||||||
@@ -1315,6 +1362,10 @@ for table_rows in raw.tables {
|
|||||||
"ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
"ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
"asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
"asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
"asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
"asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
"auth_store_snapshot" => db_update.auth_store_snapshot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
"battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
"battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
"big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
"big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
@@ -1360,6 +1411,10 @@ for table_rows in raw.tables {
|
|||||||
"ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
"ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
"asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
"asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
"asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
"asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
"auth_store_snapshot" => db_update.auth_store_snapshot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
"battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
"battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
"big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
"big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
@@ -1407,6 +1462,7 @@ pub struct AppliedDiff<'r> {
|
|||||||
ai_text_chunk: __sdk::TableAppliedDiff<'r, AiTextChunk>,
|
ai_text_chunk: __sdk::TableAppliedDiff<'r, AiTextChunk>,
|
||||||
asset_entity_binding: __sdk::TableAppliedDiff<'r, AssetEntityBinding>,
|
asset_entity_binding: __sdk::TableAppliedDiff<'r, AssetEntityBinding>,
|
||||||
asset_object: __sdk::TableAppliedDiff<'r, AssetObject>,
|
asset_object: __sdk::TableAppliedDiff<'r, AssetObject>,
|
||||||
|
auth_store_snapshot: __sdk::TableAppliedDiff<'r, AuthStoreSnapshot>,
|
||||||
battle_state: __sdk::TableAppliedDiff<'r, BattleState>,
|
battle_state: __sdk::TableAppliedDiff<'r, BattleState>,
|
||||||
big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>,
|
big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>,
|
||||||
big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>,
|
big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>,
|
||||||
@@ -1455,6 +1511,10 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
|||||||
callbacks.invoke_table_row_callbacks::<AiTextChunk>("ai_text_chunk", &self.ai_text_chunk, event);
|
callbacks.invoke_table_row_callbacks::<AiTextChunk>("ai_text_chunk", &self.ai_text_chunk, event);
|
||||||
callbacks.invoke_table_row_callbacks::<AssetEntityBinding>("asset_entity_binding", &self.asset_entity_binding, event);
|
callbacks.invoke_table_row_callbacks::<AssetEntityBinding>("asset_entity_binding", &self.asset_entity_binding, event);
|
||||||
callbacks.invoke_table_row_callbacks::<AssetObject>("asset_object", &self.asset_object, event);
|
callbacks.invoke_table_row_callbacks::<AssetObject>("asset_object", &self.asset_object, event);
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
callbacks.invoke_table_row_callbacks::<AuthStoreSnapshot>("auth_store_snapshot", &self.auth_store_snapshot, event);
|
||||||
|
>>>>>>> 4f272a50 (迁移后端认证与拆分 Spacetime 客户端)
|
||||||
callbacks.invoke_table_row_callbacks::<BattleState>("battle_state", &self.battle_state, event);
|
callbacks.invoke_table_row_callbacks::<BattleState>("battle_state", &self.battle_state, event);
|
||||||
callbacks.invoke_table_row_callbacks::<BigFishAgentMessage>("big_fish_agent_message", &self.big_fish_agent_message, event);
|
callbacks.invoke_table_row_callbacks::<BigFishAgentMessage>("big_fish_agent_message", &self.big_fish_agent_message, event);
|
||||||
callbacks.invoke_table_row_callbacks::<BigFishAssetSlot>("big_fish_asset_slot", &self.big_fish_asset_slot, event);
|
callbacks.invoke_table_row_callbacks::<BigFishAssetSlot>("big_fish_asset_slot", &self.big_fish_asset_slot, event);
|
||||||
@@ -2144,6 +2204,7 @@ fn register_tables(client_cache: &mut __sdk::ClientCache<Self>) {
|
|||||||
ai_text_chunk_table::register_table(client_cache);
|
ai_text_chunk_table::register_table(client_cache);
|
||||||
asset_entity_binding_table::register_table(client_cache);
|
asset_entity_binding_table::register_table(client_cache);
|
||||||
asset_object_table::register_table(client_cache);
|
asset_object_table::register_table(client_cache);
|
||||||
|
auth_store_snapshot_table::register_table(client_cache);
|
||||||
battle_state_table::register_table(client_cache);
|
battle_state_table::register_table(client_cache);
|
||||||
big_fish_agent_message_table::register_table(client_cache);
|
big_fish_agent_message_table::register_table(client_cache);
|
||||||
big_fish_asset_slot_table::register_table(client_cache);
|
big_fish_asset_slot_table::register_table(client_cache);
|
||||||
@@ -2184,6 +2245,7 @@ const ALL_TABLE_NAMES: &'static [&'static str] = &[
|
|||||||
"ai_text_chunk",
|
"ai_text_chunk",
|
||||||
"asset_entity_binding",
|
"asset_entity_binding",
|
||||||
"asset_object",
|
"asset_object",
|
||||||
|
"auth_store_snapshot",
|
||||||
"battle_state",
|
"battle_state",
|
||||||
"big_fish_agent_message",
|
"big_fish_agent_message",
|
||||||
"big_fish_asset_slot",
|
"big_fish_asset_slot",
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::auth_store_snapshot_procedure_result_type::AuthStoreSnapshotProcedureResult;
|
||||||
|
use super::auth_store_snapshot_upsert_input_type::AuthStoreSnapshotUpsertInput;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct UpsertAuthStoreSnapshotArgs {
|
||||||
|
pub input: AuthStoreSnapshotUpsertInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for UpsertAuthStoreSnapshotArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `upsert_auth_store_snapshot`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait upsert_auth_store_snapshot {
|
||||||
|
fn upsert_auth_store_snapshot(&self, input: AuthStoreSnapshotUpsertInput,
|
||||||
|
) {
|
||||||
|
self.upsert_auth_store_snapshot_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upsert_auth_store_snapshot_then(
|
||||||
|
&self,
|
||||||
|
input: AuthStoreSnapshotUpsertInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<AuthStoreSnapshotProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl upsert_auth_store_snapshot for super::RemoteProcedures {
|
||||||
|
fn upsert_auth_store_snapshot_then(
|
||||||
|
&self,
|
||||||
|
input: AuthStoreSnapshotUpsertInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<AuthStoreSnapshotProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
) {
|
||||||
|
self.imp.invoke_procedure_with_callback::<_, AuthStoreSnapshotProcedureResult>(
|
||||||
|
"upsert_auth_store_snapshot",
|
||||||
|
UpsertAuthStoreSnapshotArgs { input, },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
28
server-rs/crates/spacetime-client/src/npc.rs
Normal file
28
server-rs/crates/spacetime-client/src/npc.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn resolve_npc_battle_interaction(
|
||||||
|
&self,
|
||||||
|
input: ResolveNpcBattleInteractionInput,
|
||||||
|
) -> Result<NpcBattleInteractionRecord, SpacetimeClientError> {
|
||||||
|
validate_npc_battle_interaction_input(&input)?;
|
||||||
|
let procedure_input = map_resolve_npc_battle_interaction_input(input);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.resolve_npc_battle_interaction_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_npc_battle_interaction_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
459
server-rs/crates/spacetime-client/src/puzzle.rs
Normal file
459
server-rs/crates/spacetime-client/src/puzzle.rs
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn create_puzzle_agent_session(
|
||||||
|
&self,
|
||||||
|
input: PuzzleAgentSessionCreateRecordInput,
|
||||||
|
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleAgentSessionCreateInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
seed_text: input.seed_text,
|
||||||
|
welcome_message_id: input.welcome_message_id,
|
||||||
|
welcome_message_text: input.welcome_message_text,
|
||||||
|
created_at_micros: input.created_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().create_puzzle_agent_session_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_puzzle_agent_session(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleAgentSessionGetInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_puzzle_agent_session_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn submit_puzzle_agent_message(
|
||||||
|
&self,
|
||||||
|
input: PuzzleAgentMessageSubmitRecordInput,
|
||||||
|
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleAgentMessageSubmitInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
user_message_id: input.user_message_id,
|
||||||
|
user_message_text: input.user_message_text,
|
||||||
|
submitted_at_micros: input.submitted_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().submit_puzzle_agent_message_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn finalize_puzzle_agent_message(
|
||||||
|
&self,
|
||||||
|
input: PuzzleAgentMessageFinalizeRecordInput,
|
||||||
|
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleAgentMessageFinalizeInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
assistant_message_id: input.assistant_message_id,
|
||||||
|
assistant_reply_text: input.assistant_reply_text,
|
||||||
|
stage: parse_puzzle_agent_stage_record(input.stage.as_str())?,
|
||||||
|
progress_percent: input.progress_percent,
|
||||||
|
anchor_pack_json: input.anchor_pack_json,
|
||||||
|
error_message: input.error_message,
|
||||||
|
updated_at_micros: input.updated_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.finalize_puzzle_agent_message_turn_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn compile_puzzle_agent_draft(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
compiled_at_micros: i64,
|
||||||
|
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleDraftCompileInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
compiled_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().compile_puzzle_agent_draft_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn save_puzzle_generated_images(
|
||||||
|
&self,
|
||||||
|
input: PuzzleGeneratedImagesSaveRecordInput,
|
||||||
|
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleGeneratedImagesSaveInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
candidates_json: input.candidates_json,
|
||||||
|
saved_at_micros: input.saved_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().save_puzzle_generated_images_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn select_puzzle_cover_image(
|
||||||
|
&self,
|
||||||
|
input: PuzzleSelectCoverImageRecordInput,
|
||||||
|
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleSelectCoverImageInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
candidate_id: input.candidate_id,
|
||||||
|
selected_at_micros: input.selected_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().select_puzzle_cover_image_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_agent_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn publish_puzzle_work(
|
||||||
|
&self,
|
||||||
|
input: PuzzlePublishRecordInput,
|
||||||
|
) -> Result<PuzzleWorkProfileRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzlePublishInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
work_id: input.work_id,
|
||||||
|
profile_id: input.profile_id,
|
||||||
|
author_display_name: input.author_display_name,
|
||||||
|
level_name: input.level_name,
|
||||||
|
summary: input.summary,
|
||||||
|
theme_tags: input.theme_tags,
|
||||||
|
published_at_micros: input.published_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.publish_puzzle_work_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_work_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn list_puzzle_works(
|
||||||
|
&self,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<Vec<PuzzleWorkProfileRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleWorksListInput { owner_user_id };
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.list_puzzle_works_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_works_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_puzzle_work_detail(
|
||||||
|
&self,
|
||||||
|
profile_id: String,
|
||||||
|
) -> Result<PuzzleWorkProfileRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleWorkGetInput { profile_id };
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_puzzle_work_detail_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_work_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn update_puzzle_work(
|
||||||
|
&self,
|
||||||
|
input: PuzzleWorkUpsertRecordInput,
|
||||||
|
) -> Result<PuzzleWorkProfileRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleWorkUpsertInput {
|
||||||
|
profile_id: input.profile_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
level_name: input.level_name,
|
||||||
|
summary: input.summary,
|
||||||
|
theme_tags: input.theme_tags,
|
||||||
|
cover_image_src: input.cover_image_src,
|
||||||
|
cover_asset_id: input.cover_asset_id,
|
||||||
|
updated_at_micros: input.updated_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.update_puzzle_work_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_work_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn list_puzzle_gallery(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<PuzzleWorkProfileRecord>, SpacetimeClientError> {
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.list_puzzle_gallery_then(move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_works_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_puzzle_gallery_detail(
|
||||||
|
&self,
|
||||||
|
profile_id: String,
|
||||||
|
) -> Result<PuzzleWorkProfileRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleWorkGetInput { profile_id };
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_puzzle_gallery_detail_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_work_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn start_puzzle_run(
|
||||||
|
&self,
|
||||||
|
input: PuzzleRunStartRecordInput,
|
||||||
|
) -> Result<PuzzleRunRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleRunStartInput {
|
||||||
|
run_id: input.run_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
profile_id: input.profile_id,
|
||||||
|
started_at_micros: input.started_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.start_puzzle_run_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_run_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_puzzle_run(
|
||||||
|
&self,
|
||||||
|
run_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<PuzzleRunRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleRunGetInput {
|
||||||
|
run_id,
|
||||||
|
owner_user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_puzzle_run_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_run_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn swap_puzzle_pieces(
|
||||||
|
&self,
|
||||||
|
input: PuzzleRunSwapRecordInput,
|
||||||
|
) -> Result<PuzzleRunRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleRunSwapInput {
|
||||||
|
run_id: input.run_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
first_piece_id: input.first_piece_id,
|
||||||
|
second_piece_id: input.second_piece_id,
|
||||||
|
swapped_at_micros: input.swapped_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.swap_puzzle_pieces_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_run_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn drag_puzzle_piece_or_group(
|
||||||
|
&self,
|
||||||
|
input: PuzzleRunDragRecordInput,
|
||||||
|
) -> Result<PuzzleRunRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleRunDragInput {
|
||||||
|
run_id: input.run_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
piece_id: input.piece_id,
|
||||||
|
target_row: input.target_row,
|
||||||
|
target_col: input.target_col,
|
||||||
|
dragged_at_micros: input.dragged_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().drag_puzzle_piece_or_group_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_run_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn advance_puzzle_next_level(
|
||||||
|
&self,
|
||||||
|
input: PuzzleRunNextLevelRecordInput,
|
||||||
|
) -> Result<PuzzleRunRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = PuzzleRunNextLevelInput {
|
||||||
|
run_id: input.run_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
advanced_at_micros: input.advanced_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().advance_puzzle_next_level_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_puzzle_run_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
339
server-rs/crates/spacetime-client/src/runtime.rs
Normal file
339
server-rs/crates/spacetime-client/src/runtime.rs
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn get_runtime_settings(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<RuntimeSettingsRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_setting_get_input(
|
||||||
|
build_runtime_setting_get_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_runtime_setting_or_default_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_setting_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn list_platform_browse_history(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<Vec<RuntimeBrowseHistoryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_browse_history_list_input(
|
||||||
|
build_runtime_browse_history_list_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().list_platform_browse_history_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_browse_history_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_profile_dashboard(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<RuntimeProfileDashboardRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_profile_dashboard_get_input(
|
||||||
|
build_runtime_profile_dashboard_get_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_profile_dashboard_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_profile_dashboard_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn list_profile_wallet_ledger(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<Vec<RuntimeProfileWalletLedgerEntryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_profile_wallet_ledger_list_input(
|
||||||
|
build_runtime_profile_wallet_ledger_list_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().list_profile_wallet_ledger_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_profile_wallet_ledger_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_profile_play_stats(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<RuntimeProfilePlayStatsRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_profile_play_stats_get_input(
|
||||||
|
build_runtime_profile_play_stats_get_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_profile_play_stats_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_profile_play_stats_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_runtime_snapshot(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<Option<RuntimeSnapshotRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_snapshot_get_input(
|
||||||
|
build_runtime_snapshot_get_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_runtime_snapshot_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_snapshot_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn put_runtime_snapshot(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
saved_at_micros: i64,
|
||||||
|
bottom_tab: String,
|
||||||
|
game_state: serde_json::Value,
|
||||||
|
current_story: Option<serde_json::Value>,
|
||||||
|
updated_at_micros: i64,
|
||||||
|
) -> Result<RuntimeSnapshotRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_snapshot_upsert_input(
|
||||||
|
build_runtime_snapshot_upsert_input(
|
||||||
|
user_id,
|
||||||
|
saved_at_micros,
|
||||||
|
bottom_tab,
|
||||||
|
game_state,
|
||||||
|
current_story,
|
||||||
|
updated_at_micros,
|
||||||
|
)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.upsert_runtime_snapshot_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_snapshot_required_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn delete_runtime_snapshot(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<bool, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_snapshot_delete_input(
|
||||||
|
build_runtime_snapshot_delete_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.delete_runtime_snapshot_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_snapshot_delete_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn list_profile_save_archives(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<Vec<RuntimeProfileSaveArchiveRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_profile_save_archive_list_input(
|
||||||
|
build_runtime_profile_save_archive_list_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().list_profile_save_archives_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_profile_save_archive_list_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn resume_profile_save_archive(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
world_key: String,
|
||||||
|
) -> Result<(RuntimeProfileSaveArchiveRecord, RuntimeSnapshotRecord), SpacetimeClientError>
|
||||||
|
{
|
||||||
|
let procedure_input = map_runtime_profile_save_archive_resume_input(
|
||||||
|
build_runtime_profile_save_archive_resume_input(user_id, world_key)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.resume_profile_save_archive_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_profile_save_archive_resume_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn put_runtime_settings(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
music_volume: f32,
|
||||||
|
platform_theme: RuntimePlatformTheme,
|
||||||
|
updated_at_micros: i64,
|
||||||
|
) -> Result<RuntimeSettingsRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_setting_upsert_input(
|
||||||
|
build_runtime_setting_upsert_input(
|
||||||
|
user_id,
|
||||||
|
music_volume,
|
||||||
|
platform_theme,
|
||||||
|
updated_at_micros,
|
||||||
|
)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.upsert_runtime_setting_and_return_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_setting_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn upsert_platform_browse_history_entries(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
entries: Vec<module_runtime::RuntimeBrowseHistoryWriteInput>,
|
||||||
|
updated_at_micros: i64,
|
||||||
|
) -> Result<Vec<RuntimeBrowseHistoryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_browse_history_sync_input(
|
||||||
|
build_runtime_browse_history_sync_input(user_id, entries, updated_at_micros)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.upsert_platform_browse_history_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_browse_history_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn clear_platform_browse_history(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<Vec<RuntimeBrowseHistoryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_runtime_browse_history_clear_input(
|
||||||
|
build_runtime_browse_history_clear_input(user_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.clear_platform_browse_history_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_runtime_browse_history_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
100
server-rs/crates/spacetime-client/src/story.rs
Normal file
100
server-rs/crates/spacetime-client/src/story.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl SpacetimeClient {
|
||||||
|
pub async fn begin_story_session(
|
||||||
|
&self,
|
||||||
|
story_session_id: String,
|
||||||
|
runtime_session_id: String,
|
||||||
|
actor_user_id: String,
|
||||||
|
world_profile_id: String,
|
||||||
|
initial_prompt: String,
|
||||||
|
opening_summary: Option<String>,
|
||||||
|
created_at_micros: i64,
|
||||||
|
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_story_session_input(
|
||||||
|
build_story_session_input(
|
||||||
|
story_session_id,
|
||||||
|
runtime_session_id,
|
||||||
|
actor_user_id,
|
||||||
|
world_profile_id,
|
||||||
|
initial_prompt,
|
||||||
|
opening_summary,
|
||||||
|
created_at_micros,
|
||||||
|
)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().begin_story_session_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_story_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn continue_story(
|
||||||
|
&self,
|
||||||
|
story_session_id: String,
|
||||||
|
event_id: String,
|
||||||
|
narrative_text: String,
|
||||||
|
choice_function_id: Option<String>,
|
||||||
|
updated_at_micros: i64,
|
||||||
|
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_story_continue_input(
|
||||||
|
build_story_continue_input(
|
||||||
|
story_session_id,
|
||||||
|
event_id,
|
||||||
|
narrative_text,
|
||||||
|
choice_function_id,
|
||||||
|
updated_at_micros,
|
||||||
|
)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().continue_story_and_return_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_story_session_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_story_session_state(
|
||||||
|
&self,
|
||||||
|
story_session_id: String,
|
||||||
|
) -> Result<StorySessionStateRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = map_story_session_state_input(
|
||||||
|
build_story_session_state_input(story_session_id)
|
||||||
|
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection.procedures().get_story_session_state_then(
|
||||||
|
procedure_input,
|
||||||
|
move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_story_session_state_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
122
server-rs/crates/spacetime-module/src/auth.rs
Normal file
122
server-rs/crates/spacetime-module/src/auth.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
use crate::*;
|
||||||
|
|
||||||
|
const AUTH_STORE_SNAPSHOT_ID: &str = "default";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||||
|
pub struct AuthStoreSnapshotRecord {
|
||||||
|
pub snapshot_json: Option<String>,
|
||||||
|
pub updated_at_micros: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||||
|
pub struct AuthStoreSnapshotUpsertInput {
|
||||||
|
pub snapshot_json: String,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||||
|
pub struct AuthStoreSnapshotProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub record: Option<AuthStoreSnapshotRecord>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[spacetimedb::table(accessor = auth_store_snapshot)]
|
||||||
|
pub struct AuthStoreSnapshot {
|
||||||
|
#[primary_key]
|
||||||
|
pub(crate) snapshot_id: String,
|
||||||
|
pub(crate) snapshot_json: String,
|
||||||
|
pub(crate) updated_at: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axum 启动恢复认证状态时读取当前快照;记录不存在代表尚未产生登录态。
|
||||||
|
#[spacetimedb::procedure]
|
||||||
|
pub fn get_auth_store_snapshot(ctx: &mut ProcedureContext) -> AuthStoreSnapshotProcedureResult {
|
||||||
|
match ctx.try_with_tx(|tx| get_auth_store_snapshot_tx(tx)) {
|
||||||
|
Ok(record) => AuthStoreSnapshotProcedureResult {
|
||||||
|
ok: true,
|
||||||
|
record: Some(record),
|
||||||
|
error_message: None,
|
||||||
|
},
|
||||||
|
Err(message) => AuthStoreSnapshotProcedureResult {
|
||||||
|
ok: false,
|
||||||
|
record: None,
|
||||||
|
error_message: Some(message),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axum 每次鉴权仓储变更后覆盖写入整份快照,后续拆表阶段再替换为细粒度 reducer。
|
||||||
|
#[spacetimedb::procedure]
|
||||||
|
pub fn upsert_auth_store_snapshot(
|
||||||
|
ctx: &mut ProcedureContext,
|
||||||
|
input: AuthStoreSnapshotUpsertInput,
|
||||||
|
) -> AuthStoreSnapshotProcedureResult {
|
||||||
|
match ctx.try_with_tx(|tx| upsert_auth_store_snapshot_tx(tx, input.clone())) {
|
||||||
|
Ok(record) => AuthStoreSnapshotProcedureResult {
|
||||||
|
ok: true,
|
||||||
|
record: Some(record),
|
||||||
|
error_message: None,
|
||||||
|
},
|
||||||
|
Err(message) => AuthStoreSnapshotProcedureResult {
|
||||||
|
ok: false,
|
||||||
|
record: None,
|
||||||
|
error_message: Some(message),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_auth_store_snapshot_tx(ctx: &ReducerContext) -> Result<AuthStoreSnapshotRecord, String> {
|
||||||
|
Ok(
|
||||||
|
match ctx
|
||||||
|
.db
|
||||||
|
.auth_store_snapshot()
|
||||||
|
.snapshot_id()
|
||||||
|
.find(&AUTH_STORE_SNAPSHOT_ID.to_string())
|
||||||
|
{
|
||||||
|
Some(row) => AuthStoreSnapshotRecord {
|
||||||
|
snapshot_json: Some(row.snapshot_json),
|
||||||
|
updated_at_micros: Some(row.updated_at.to_micros_since_unix_epoch()),
|
||||||
|
},
|
||||||
|
None => AuthStoreSnapshotRecord {
|
||||||
|
snapshot_json: None,
|
||||||
|
updated_at_micros: None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upsert_auth_store_snapshot_tx(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
input: AuthStoreSnapshotUpsertInput,
|
||||||
|
) -> Result<AuthStoreSnapshotRecord, String> {
|
||||||
|
let snapshot_json = input.snapshot_json.trim().to_string();
|
||||||
|
if snapshot_json.is_empty() {
|
||||||
|
return Err("认证快照 JSON 不能为空".to_string());
|
||||||
|
}
|
||||||
|
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||||
|
|
||||||
|
if ctx
|
||||||
|
.db
|
||||||
|
.auth_store_snapshot()
|
||||||
|
.snapshot_id()
|
||||||
|
.find(&AUTH_STORE_SNAPSHOT_ID.to_string())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
ctx.db
|
||||||
|
.auth_store_snapshot()
|
||||||
|
.snapshot_id()
|
||||||
|
.delete(&AUTH_STORE_SNAPSHOT_ID.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.db.auth_store_snapshot().insert(AuthStoreSnapshot {
|
||||||
|
snapshot_id: AUTH_STORE_SNAPSHOT_ID.to_string(),
|
||||||
|
snapshot_json: snapshot_json.clone(),
|
||||||
|
updated_at,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(AuthStoreSnapshotRecord {
|
||||||
|
snapshot_json: Some(snapshot_json),
|
||||||
|
updated_at_micros: Some(input.updated_at_micros),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -21,9 +21,11 @@ use module_quest::{
|
|||||||
pub(crate) use serde_json::{Map as JsonMap, Value as JsonValue, json};
|
pub(crate) use serde_json::{Map as JsonMap, Value as JsonValue, json};
|
||||||
pub(crate) use shared_kernel::format_timestamp_micros;
|
pub(crate) use shared_kernel::format_timestamp_micros;
|
||||||
pub use spacetimedb::{ProcedureContext, ReducerContext, SpacetimeType, Table, Timestamp};
|
pub use spacetimedb::{ProcedureContext, ReducerContext, SpacetimeType, Table, Timestamp};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
mod ai;
|
mod ai;
|
||||||
mod asset_metadata;
|
mod asset_metadata;
|
||||||
|
mod auth;
|
||||||
mod big_fish;
|
mod big_fish;
|
||||||
mod domain_types;
|
mod domain_types;
|
||||||
mod entry;
|
mod entry;
|
||||||
@@ -32,6 +34,7 @@ mod runtime;
|
|||||||
|
|
||||||
pub use ai::*;
|
pub use ai::*;
|
||||||
pub use asset_metadata::*;
|
pub use asset_metadata::*;
|
||||||
|
pub use auth::*;
|
||||||
pub use big_fish::*;
|
pub use big_fish::*;
|
||||||
pub use domain_types::*;
|
pub use domain_types::*;
|
||||||
pub use entry::*;
|
pub use entry::*;
|
||||||
@@ -2733,10 +2736,12 @@ fn list_custom_world_work_snapshots(
|
|||||||
validate_custom_world_works_list_input(&input).map_err(|error| error.to_string())?;
|
validate_custom_world_works_list_input(&input).map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
let mut active_agent_session_ids = HashSet::new();
|
||||||
|
|
||||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||||
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
||||||
}) {
|
}) {
|
||||||
|
active_agent_session_ids.insert(session.session_id.clone());
|
||||||
let gate = build_custom_world_publish_gate_from_session(&session);
|
let gate = build_custom_world_publish_gate_from_session(&session);
|
||||||
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref());
|
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref());
|
||||||
let title = resolve_session_work_title(&session, draft_profile.as_ref());
|
let title = resolve_session_work_title(&session, draft_profile.as_ref());
|
||||||
@@ -2780,6 +2785,7 @@ fn list_custom_world_work_snapshots(
|
|||||||
.custom_world_profile()
|
.custom_world_profile()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
||||||
|
.filter(|row| should_include_custom_world_profile_work(row, &active_agent_session_ids))
|
||||||
{
|
{
|
||||||
items.push(CustomWorldWorkSummarySnapshot {
|
items.push(CustomWorldWorkSummarySnapshot {
|
||||||
work_id: format!("published:{}", profile.profile_id),
|
work_id: format!("published:{}", profile.profile_id),
|
||||||
@@ -2834,6 +2840,24 @@ fn list_custom_world_work_snapshots(
|
|||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_include_custom_world_profile_work(
|
||||||
|
row: &CustomWorldProfile,
|
||||||
|
active_agent_session_ids: &HashSet<String>,
|
||||||
|
) -> bool {
|
||||||
|
// 已发布 profile 是正式作品;即使来源会话还存在,也必须保留独立入口。
|
||||||
|
if row.publication_status == CustomWorldPublicationStatus::Published {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未发布 profile 若来源于仍可继续聊天的 Agent 会话,只是同一草稿的编译产物,
|
||||||
|
// works 里保留 agent_session 即可,避免草稿分组显示两份同名作品。
|
||||||
|
row.source_agent_session_id
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |session_id| {
|
||||||
|
!active_agent_session_ids.contains(session_id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn get_custom_world_agent_card_detail_tx(
|
fn get_custom_world_agent_card_detail_tx(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
input: CustomWorldAgentCardDetailGetInput,
|
input: CustomWorldAgentCardDetailGetInput,
|
||||||
@@ -5996,6 +6020,113 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_world_works_hides_compiled_draft_profile_when_agent_session_is_active() {
|
||||||
|
fn build_test_custom_world_profile(
|
||||||
|
profile_id: &str,
|
||||||
|
source_agent_session_id: Option<&str>,
|
||||||
|
publication_status: CustomWorldPublicationStatus,
|
||||||
|
) -> CustomWorldProfile {
|
||||||
|
CustomWorldProfile {
|
||||||
|
profile_id: profile_id.to_string(),
|
||||||
|
owner_user_id: "user-1".to_string(),
|
||||||
|
public_work_code: if publication_status == CustomWorldPublicationStatus::Published {
|
||||||
|
Some("CW-00000001".to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
author_public_user_code: None,
|
||||||
|
source_agent_session_id: source_agent_session_id.map(str::to_string),
|
||||||
|
publication_status,
|
||||||
|
world_name: "潮雾列岛".to_string(),
|
||||||
|
subtitle: String::new(),
|
||||||
|
summary_text: String::new(),
|
||||||
|
theme_mode: CustomWorldThemeMode::Mythic,
|
||||||
|
cover_image_src: None,
|
||||||
|
profile_payload_json: "{}".to_string(),
|
||||||
|
playable_npc_count: 0,
|
||||||
|
landmark_count: 0,
|
||||||
|
author_display_name: "玩家".to_string(),
|
||||||
|
published_at: if publication_status == CustomWorldPublicationStatus::Published {
|
||||||
|
Some(Timestamp::from_micros_since_unix_epoch(2))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
deleted_at: None,
|
||||||
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let draft_profile = build_test_custom_world_profile(
|
||||||
|
"profile-1",
|
||||||
|
Some("session-1"),
|
||||||
|
CustomWorldPublicationStatus::Draft,
|
||||||
|
);
|
||||||
|
let orphan_draft_profile = build_test_custom_world_profile(
|
||||||
|
"profile-2",
|
||||||
|
Some("session-2"),
|
||||||
|
CustomWorldPublicationStatus::Draft,
|
||||||
|
);
|
||||||
|
let published_profile = build_test_custom_world_profile(
|
||||||
|
"profile-3",
|
||||||
|
Some("session-1"),
|
||||||
|
CustomWorldPublicationStatus::Published,
|
||||||
|
);
|
||||||
|
let mut active_agent_session_ids = HashSet::new();
|
||||||
|
active_agent_session_ids.insert("session-1".to_string());
|
||||||
|
|
||||||
|
assert!(!should_include_custom_world_profile_work(
|
||||||
|
&draft_profile,
|
||||||
|
&active_agent_session_ids,
|
||||||
|
));
|
||||||
|
assert!(should_include_custom_world_profile_work(
|
||||||
|
&orphan_draft_profile,
|
||||||
|
&active_agent_session_ids,
|
||||||
|
));
|
||||||
|
assert!(should_include_custom_world_profile_work(
|
||||||
|
&published_profile,
|
||||||
|
&active_agent_session_ids,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_world_works_keeps_compiled_draft_profile_without_active_agent_session() {
|
||||||
|
let draft_profile = CustomWorldProfile {
|
||||||
|
profile_id: "profile-1".to_string(),
|
||||||
|
owner_user_id: "user-1".to_string(),
|
||||||
|
public_work_code: None,
|
||||||
|
author_public_user_code: None,
|
||||||
|
source_agent_session_id: Some("session-1".to_string()),
|
||||||
|
publication_status: CustomWorldPublicationStatus::Draft,
|
||||||
|
world_name: "潮雾列岛".to_string(),
|
||||||
|
subtitle: String::new(),
|
||||||
|
summary_text: String::new(),
|
||||||
|
theme_mode: CustomWorldThemeMode::Mythic,
|
||||||
|
cover_image_src: None,
|
||||||
|
profile_payload_json: "{}".to_string(),
|
||||||
|
playable_npc_count: 0,
|
||||||
|
landmark_count: 0,
|
||||||
|
author_display_name: "玩家".to_string(),
|
||||||
|
published_at: None,
|
||||||
|
deleted_at: None,
|
||||||
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
};
|
||||||
|
let mut active_agent_session_ids = HashSet::new();
|
||||||
|
|
||||||
|
assert!(should_include_custom_world_profile_work(
|
||||||
|
&draft_profile,
|
||||||
|
&active_agent_session_ids,
|
||||||
|
));
|
||||||
|
|
||||||
|
active_agent_session_ids.insert("session-2".to_string());
|
||||||
|
assert!(should_include_custom_world_profile_work(
|
||||||
|
&draft_profile,
|
||||||
|
&active_agent_session_ids,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn summarize_publish_gate_accepts_current_agent_result_schema() {
|
fn summarize_publish_gate_accepts_current_agent_result_schema() {
|
||||||
let draft_profile = serde_json::from_str::<JsonValue>(
|
let draft_profile = serde_json::from_str::<JsonValue>(
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ function renderAccountModal(overrides?: {
|
|||||||
expiresInSeconds: 300,
|
expiresInSeconds: 300,
|
||||||
})}
|
})}
|
||||||
onChangePhone={vi.fn().mockResolvedValue(undefined)}
|
onChangePhone={vi.fn().mockResolvedValue(undefined)}
|
||||||
|
onChangePassword={vi.fn().mockResolvedValue(undefined)}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ type AccountModalProps = {
|
|||||||
expiresInSeconds: number;
|
expiresInSeconds: number;
|
||||||
}>;
|
}>;
|
||||||
onChangePhone: (phone: string, code: string) => Promise<void>;
|
onChangePhone: (phone: string, code: string) => Promise<void>;
|
||||||
|
onChangePassword: (
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string,
|
||||||
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_SECTIONS: Array<{
|
const SETTINGS_SECTIONS: Array<{
|
||||||
@@ -285,24 +289,31 @@ export function AccountModal({
|
|||||||
changePhoneCaptchaChallenge,
|
changePhoneCaptchaChallenge,
|
||||||
onSendChangePhoneCode,
|
onSendChangePhoneCode,
|
||||||
onChangePhone,
|
onChangePhone,
|
||||||
|
onChangePassword,
|
||||||
}: AccountModalProps) {
|
}: AccountModalProps) {
|
||||||
const [activeSection, setActiveSection] =
|
const [activeSection, setActiveSection] =
|
||||||
useState<PrimarySettingsSection | null>(
|
useState<PrimarySettingsSection | null>(
|
||||||
normalizeSettingsSection(initialSection),
|
normalizeSettingsSection(initialSection),
|
||||||
);
|
);
|
||||||
const [isChangePhonePanelOpen, setIsChangePhonePanelOpen] = useState(false);
|
const [isChangePhonePanelOpen, setIsChangePhonePanelOpen] = useState(false);
|
||||||
|
const [isPasswordPanelOpen, setIsPasswordPanelOpen] = useState(false);
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [captchaAnswer, setCaptchaAnswer] = useState('');
|
const [captchaAnswer, setCaptchaAnswer] = useState('');
|
||||||
const [changePhoneError, setChangePhoneError] = useState('');
|
const [changePhoneError, setChangePhoneError] = useState('');
|
||||||
|
const [passwordError, setPasswordError] = useState('');
|
||||||
const [changePhoneHint, setChangePhoneHint] = useState('');
|
const [changePhoneHint, setChangePhoneHint] = useState('');
|
||||||
const [accountNotice, setAccountNotice] = useState('');
|
const [accountNotice, setAccountNotice] = useState('');
|
||||||
const [sendingCode, setSendingCode] = useState(false);
|
const [sendingCode, setSendingCode] = useState(false);
|
||||||
const [changingPhone, setChangingPhone] = useState(false);
|
const [changingPhone, setChangingPhone] = useState(false);
|
||||||
|
const [changingPassword, setChangingPassword] = useState(false);
|
||||||
const [cooldownSeconds, setCooldownSeconds] = useState(0);
|
const [cooldownSeconds, setCooldownSeconds] = useState(0);
|
||||||
const settingsHomeRef = useRef<HTMLDivElement | null>(null);
|
const settingsHomeRef = useRef<HTMLDivElement | null>(null);
|
||||||
const sectionTriggerRef = useRef<HTMLButtonElement | null>(null);
|
const sectionTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const changePhoneTriggerRef = useRef<HTMLButtonElement | null>(null);
|
const changePhoneTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
const passwordTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const focusAfterNextPaint = useCallback((element: HTMLElement | null) => {
|
const focusAfterNextPaint = useCallback((element: HTMLElement | null) => {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
@@ -325,6 +336,12 @@ export function AccountModal({
|
|||||||
setCooldownSeconds(0);
|
setCooldownSeconds(0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const resetPasswordDraft = useCallback(() => {
|
||||||
|
setCurrentPassword('');
|
||||||
|
setNewPassword('');
|
||||||
|
setPasswordError('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return;
|
return;
|
||||||
@@ -332,11 +349,14 @@ export function AccountModal({
|
|||||||
|
|
||||||
setActiveSection(normalizeSettingsSection(initialSection));
|
setActiveSection(normalizeSettingsSection(initialSection));
|
||||||
setIsChangePhonePanelOpen(false);
|
setIsChangePhonePanelOpen(false);
|
||||||
|
setIsPasswordPanelOpen(false);
|
||||||
setAccountNotice('');
|
setAccountNotice('');
|
||||||
sectionTriggerRef.current = null;
|
sectionTriggerRef.current = null;
|
||||||
changePhoneTriggerRef.current = null;
|
changePhoneTriggerRef.current = null;
|
||||||
|
passwordTriggerRef.current = null;
|
||||||
resetChangePhoneDraft();
|
resetChangePhoneDraft();
|
||||||
}, [initialSection, isOpen, resetChangePhoneDraft]);
|
resetPasswordDraft();
|
||||||
|
}, [initialSection, isOpen, resetChangePhoneDraft, resetPasswordDraft]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const settingsHome = settingsHomeRef.current;
|
const settingsHome = settingsHomeRef.current;
|
||||||
@@ -368,10 +388,12 @@ export function AccountModal({
|
|||||||
const closeSectionPanel = useCallback(() => {
|
const closeSectionPanel = useCallback(() => {
|
||||||
const sectionTrigger = sectionTriggerRef.current;
|
const sectionTrigger = sectionTriggerRef.current;
|
||||||
setIsChangePhonePanelOpen(false);
|
setIsChangePhonePanelOpen(false);
|
||||||
|
setIsPasswordPanelOpen(false);
|
||||||
setActiveSection(null);
|
setActiveSection(null);
|
||||||
resetChangePhoneDraft();
|
resetChangePhoneDraft();
|
||||||
|
resetPasswordDraft();
|
||||||
focusAfterNextPaint(sectionTrigger);
|
focusAfterNextPaint(sectionTrigger);
|
||||||
}, [focusAfterNextPaint, resetChangePhoneDraft]);
|
}, [focusAfterNextPaint, resetChangePhoneDraft, resetPasswordDraft]);
|
||||||
|
|
||||||
const closeChangePhonePanel = useCallback(() => {
|
const closeChangePhonePanel = useCallback(() => {
|
||||||
const changePhoneTrigger = changePhoneTriggerRef.current;
|
const changePhoneTrigger = changePhoneTriggerRef.current;
|
||||||
@@ -380,6 +402,13 @@ export function AccountModal({
|
|||||||
focusAfterNextPaint(changePhoneTrigger);
|
focusAfterNextPaint(changePhoneTrigger);
|
||||||
}, [focusAfterNextPaint, resetChangePhoneDraft]);
|
}, [focusAfterNextPaint, resetChangePhoneDraft]);
|
||||||
|
|
||||||
|
const closePasswordPanel = useCallback(() => {
|
||||||
|
const passwordTrigger = passwordTriggerRef.current;
|
||||||
|
setIsPasswordPanelOpen(false);
|
||||||
|
resetPasswordDraft();
|
||||||
|
focusAfterNextPaint(passwordTrigger);
|
||||||
|
}, [focusAfterNextPaint, resetPasswordDraft]);
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -556,6 +585,31 @@ export function AccountModal({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="platform-subpanel rounded-2xl px-4 py-4">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||||
|
登录密码
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-sm text-[var(--platform-text-base)]">
|
||||||
|
在独立面板中设置或修改账号密码。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-[11px]"
|
||||||
|
onClick={(event) => {
|
||||||
|
passwordTriggerRef.current = event.currentTarget;
|
||||||
|
setAccountNotice('');
|
||||||
|
resetPasswordDraft();
|
||||||
|
setIsPasswordPanelOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
修改密码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="platform-subpanel rounded-2xl px-4 py-4">
|
<div className="platform-subpanel rounded-2xl px-4 py-4">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -893,6 +947,74 @@ export function AccountModal({
|
|||||||
</div>
|
</div>
|
||||||
</OverlayPanel>
|
</OverlayPanel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{isPasswordPanelOpen ? (
|
||||||
|
<OverlayPanel
|
||||||
|
eyebrow="账号安全"
|
||||||
|
title="修改登录密码"
|
||||||
|
description="输入当前密码与新密码。首次设置密码时当前密码可留空。"
|
||||||
|
onBack={closePasswordPanel}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>当前密码</span>
|
||||||
|
<input
|
||||||
|
className="platform-input h-11"
|
||||||
|
value={currentPassword}
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
placeholder="首次设置可留空"
|
||||||
|
onChange={(event) => setCurrentPassword(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>新密码</span>
|
||||||
|
<input
|
||||||
|
className="platform-input h-11"
|
||||||
|
value={newPassword}
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder="设置新密码"
|
||||||
|
onChange={(event) => setNewPassword(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{passwordError ? (
|
||||||
|
<div className="platform-banner platform-banner--danger text-sm">
|
||||||
|
{passwordError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={changingPassword || !newPassword.trim()}
|
||||||
|
className="platform-button platform-button--primary h-11 w-full text-sm disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
onClick={() => {
|
||||||
|
void (async () => {
|
||||||
|
setChangingPassword(true);
|
||||||
|
setPasswordError('');
|
||||||
|
try {
|
||||||
|
await onChangePassword(currentPassword, newPassword);
|
||||||
|
setAccountNotice('密码已更新。');
|
||||||
|
closePasswordPanel();
|
||||||
|
} catch (error) {
|
||||||
|
setPasswordError(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: '修改密码失败,请稍后再试。',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setChangingPassword(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{changingPassword ? '提交中...' : '确认修改密码'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OverlayPanel>
|
||||||
|
) : null}
|
||||||
</OverlayPanel>
|
</OverlayPanel>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import {
|
|||||||
type AuthRiskBlockSummary,
|
type AuthRiskBlockSummary,
|
||||||
type AuthSessionSummary,
|
type AuthSessionSummary,
|
||||||
type AuthUser,
|
type AuthUser,
|
||||||
|
authEntry,
|
||||||
bindWechatPhone,
|
bindWechatPhone,
|
||||||
|
changePassword,
|
||||||
changePhoneNumber,
|
changePhoneNumber,
|
||||||
consumeAuthCallbackResult,
|
consumeAuthCallbackResult,
|
||||||
ensureAutoAuthUser,
|
ensureAutoAuthUser,
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
loginWithPhoneCode,
|
loginWithPhoneCode,
|
||||||
logoutAllAuthSessions,
|
logoutAllAuthSessions,
|
||||||
logoutAuthUser,
|
logoutAuthUser,
|
||||||
|
resetPassword,
|
||||||
revokeAuthSession,
|
revokeAuthSession,
|
||||||
sendPhoneLoginCode,
|
sendPhoneLoginCode,
|
||||||
startWechatLogin,
|
startWechatLogin,
|
||||||
@@ -648,6 +651,10 @@ export function AuthGate({ children }: AuthGateProps) {
|
|||||||
setChangePhoneCaptchaChallenge(null);
|
setChangePhoneCaptchaChallenge(null);
|
||||||
setUser(nextUser);
|
setUser(nextUser);
|
||||||
}}
|
}}
|
||||||
|
onChangePassword={async (currentPassword, newPassword) => {
|
||||||
|
const nextUser = await changePassword(currentPassword, newPassword);
|
||||||
|
setUser(nextUser);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<LoginScreen
|
<LoginScreen
|
||||||
@@ -660,11 +667,11 @@ export function AuthGate({ children }: AuthGateProps) {
|
|||||||
error={error}
|
error={error}
|
||||||
captchaChallenge={loginCaptchaChallenge}
|
captchaChallenge={loginCaptchaChallenge}
|
||||||
onClose={closeLoginModal}
|
onClose={closeLoginModal}
|
||||||
onSendCode={async (phone, captcha) => {
|
onSendCode={async (phone, scene, captcha) => {
|
||||||
setSendingCode(true);
|
setSendingCode(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const result = await sendPhoneLoginCode(phone, 'login', captcha);
|
const result = await sendPhoneLoginCode(phone, scene, captcha);
|
||||||
setLoginCaptchaChallenge(null);
|
setLoginCaptchaChallenge(null);
|
||||||
return result;
|
return result;
|
||||||
} catch (sendError) {
|
} catch (sendError) {
|
||||||
@@ -682,7 +689,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
|||||||
setSendingCode(false);
|
setSendingCode(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSubmit={async (phone, code) => {
|
onPhoneSubmit={async (phone, code) => {
|
||||||
setLoggingIn(true);
|
setLoggingIn(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
@@ -699,6 +706,38 @@ export function AuthGate({ children }: AuthGateProps) {
|
|||||||
setLoggingIn(false);
|
setLoggingIn(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onPasswordSubmit={async (username, password) => {
|
||||||
|
setLoggingIn(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const nextUser = await authEntry(username, password);
|
||||||
|
activateReadyUser(nextUser);
|
||||||
|
} catch (loginError) {
|
||||||
|
setError(
|
||||||
|
loginError instanceof Error
|
||||||
|
? loginError.message
|
||||||
|
: '登录失败,请稍后再试。',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoggingIn(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onResetPassword={async (phone, code, newPassword) => {
|
||||||
|
setLoggingIn(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const nextUser = await resetPassword(phone, code, newPassword);
|
||||||
|
activateReadyUser(nextUser);
|
||||||
|
} catch (resetError) {
|
||||||
|
setError(
|
||||||
|
resetError instanceof Error
|
||||||
|
? resetError.message
|
||||||
|
: '重置密码失败,请稍后再试。',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoggingIn(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onStartWechatLogin={async () => {
|
onStartWechatLogin={async () => {
|
||||||
setWechatLoading(true);
|
setWechatLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
} from '../../services/authService';
|
} from '../../services/authService';
|
||||||
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
import { CaptchaChallengeField } from './CaptchaChallengeField';
|
||||||
|
|
||||||
|
type SmsScene = 'login' | 'reset_password';
|
||||||
|
|
||||||
type LoginScreenProps = {
|
type LoginScreenProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
platformTheme: PlatformTheme;
|
platformTheme: PlatformTheme;
|
||||||
@@ -20,6 +22,7 @@ type LoginScreenProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSendCode: (
|
onSendCode: (
|
||||||
phone: string,
|
phone: string,
|
||||||
|
scene: SmsScene,
|
||||||
captcha?: {
|
captcha?: {
|
||||||
challengeId?: string;
|
challengeId?: string;
|
||||||
answer?: string;
|
answer?: string;
|
||||||
@@ -28,7 +31,13 @@ type LoginScreenProps = {
|
|||||||
cooldownSeconds: number;
|
cooldownSeconds: number;
|
||||||
expiresInSeconds: number;
|
expiresInSeconds: number;
|
||||||
}>;
|
}>;
|
||||||
onSubmit: (phone: string, code: string) => Promise<void>;
|
onPhoneSubmit: (phone: string, code: string) => Promise<void>;
|
||||||
|
onPasswordSubmit: (username: string, password: string) => Promise<void>;
|
||||||
|
onResetPassword: (
|
||||||
|
phone: string,
|
||||||
|
code: string,
|
||||||
|
newPassword: string,
|
||||||
|
) => Promise<void>;
|
||||||
onStartWechatLogin: () => Promise<void>;
|
onStartWechatLogin: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,14 +52,25 @@ export function LoginScreen({
|
|||||||
captchaChallenge,
|
captchaChallenge,
|
||||||
onClose,
|
onClose,
|
||||||
onSendCode,
|
onSendCode,
|
||||||
onSubmit,
|
onPhoneSubmit,
|
||||||
|
onPasswordSubmit,
|
||||||
|
onResetPassword,
|
||||||
onStartWechatLogin,
|
onStartWechatLogin,
|
||||||
}: LoginScreenProps) {
|
}: LoginScreenProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login');
|
||||||
|
const [isResetPanelOpen, setIsResetPanelOpen] = useState(false);
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
|
const [resetPhone, setResetPhone] = useState('');
|
||||||
|
const [resetCode, setResetCode] = useState('');
|
||||||
|
const [resetPasswordValue, setResetPasswordValue] = useState('');
|
||||||
const [captchaAnswer, setCaptchaAnswer] = useState('');
|
const [captchaAnswer, setCaptchaAnswer] = useState('');
|
||||||
const [cooldownSeconds, setCooldownSeconds] = useState(0);
|
const [cooldownSeconds, setCooldownSeconds] = useState(0);
|
||||||
|
const [resetCooldownSeconds, setResetCooldownSeconds] = useState(0);
|
||||||
const [hint, setHint] = useState('');
|
const [hint, setHint] = useState('');
|
||||||
|
const passwordLoginEnabled = availableLoginMethods.includes('password');
|
||||||
const phoneLoginEnabled = availableLoginMethods.includes('phone');
|
const phoneLoginEnabled = availableLoginMethods.includes('phone');
|
||||||
const wechatLoginEnabled = availableLoginMethods.includes('wechat');
|
const wechatLoginEnabled = availableLoginMethods.includes('wechat');
|
||||||
|
|
||||||
@@ -63,15 +83,27 @@ export function LoginScreen({
|
|||||||
setCooldownSeconds((current) => Math.max(0, current - 1));
|
setCooldownSeconds((current) => Math.max(0, current - 1));
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => {
|
return () => window.clearTimeout(timeoutId);
|
||||||
window.clearTimeout(timeoutId);
|
|
||||||
};
|
|
||||||
}, [cooldownSeconds]);
|
}, [cooldownSeconds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resetCooldownSeconds <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
setResetCooldownSeconds((current) => Math.max(0, current - 1));
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}, [resetCooldownSeconds]);
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitDisabled = loggingIn || sendingCode;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[120] flex items-end justify-center px-3 py-4 text-[var(--platform-text-strong)] backdrop-blur-sm sm:items-center sm:p-4`}
|
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[120] flex items-end justify-center px-3 py-4 text-[var(--platform-text-strong)] backdrop-blur-sm sm:items-center sm:p-4`}
|
||||||
@@ -82,16 +114,14 @@ export function LoginScreen({
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="auth-login-dialog-title"
|
aria-labelledby="auth-login-dialog-title"
|
||||||
className="platform-auth-card w-full max-w-md overflow-hidden rounded-[2rem]"
|
className="platform-auth-card w-full max-w-md overflow-hidden rounded-[2rem]"
|
||||||
onClick={(event) => {
|
onClick={(event) => event.stopPropagation()}
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||||
<div
|
<div
|
||||||
id="auth-login-dialog-title"
|
id="auth-login-dialog-title"
|
||||||
className="text-lg font-semibold text-[var(--platform-text-strong)]"
|
className="text-lg font-semibold text-[var(--platform-text-strong)]"
|
||||||
>
|
>
|
||||||
登录账号
|
{isResetPanelOpen ? '重置密码' : '账号入口'}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -103,122 +133,408 @@ export function LoginScreen({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
{isResetPanelOpen ? (
|
||||||
className="flex flex-col gap-4 px-5 py-5"
|
<PasswordResetPanel
|
||||||
onSubmit={(event) => {
|
phone={resetPhone}
|
||||||
event.preventDefault();
|
code={resetCode}
|
||||||
if (!phoneLoginEnabled) {
|
password={resetPasswordValue}
|
||||||
return;
|
sendingCode={sendingCode}
|
||||||
}
|
loggingIn={loggingIn}
|
||||||
void onSubmit(phone, code);
|
cooldownSeconds={resetCooldownSeconds}
|
||||||
}}
|
error={error}
|
||||||
>
|
onPhoneChange={setResetPhone}
|
||||||
{phoneLoginEnabled ? (
|
onCodeChange={setResetCode}
|
||||||
<>
|
onPasswordChange={setResetPasswordValue}
|
||||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
onBack={() => setIsResetPanelOpen(false)}
|
||||||
<span>手机号</span>
|
onSendCode={async () => {
|
||||||
<input
|
const result = await onSendCode(resetPhone, 'reset_password');
|
||||||
className="platform-input"
|
setResetCooldownSeconds(result.cooldownSeconds);
|
||||||
autoComplete="tel"
|
}}
|
||||||
inputMode="numeric"
|
onSubmit={() => onResetPassword(resetPhone, resetCode, resetPasswordValue)}
|
||||||
value={phone}
|
/>
|
||||||
onChange={(event) => setPhone(event.target.value)}
|
) : (
|
||||||
placeholder="13800000000"
|
<div className="flex flex-col gap-4 px-5 py-5">
|
||||||
/>
|
<div className="grid grid-cols-2 gap-2 rounded-full bg-[var(--platform-subpanel-bg)] p-1">
|
||||||
</label>
|
<TabButton
|
||||||
|
active={activeTab === 'login'}
|
||||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
label="登录"
|
||||||
<span>验证码</span>
|
onClick={() => setActiveTab('login')}
|
||||||
<div className="flex gap-3">
|
/>
|
||||||
<input
|
<TabButton
|
||||||
className="platform-input min-w-0 flex-1"
|
active={activeTab === 'register'}
|
||||||
inputMode="numeric"
|
label="注册"
|
||||||
value={code}
|
onClick={() => setActiveTab('register')}
|
||||||
onChange={(event) => setCode(event.target.value)}
|
|
||||||
placeholder="输入验证码"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={sendingCode || cooldownSeconds > 0 || !phone.trim()}
|
|
||||||
className="platform-button platform-button--secondary h-12 shrink-0 px-4 text-sm disabled:cursor-not-allowed disabled:opacity-55"
|
|
||||||
onClick={() => {
|
|
||||||
void (async () => {
|
|
||||||
setHint('');
|
|
||||||
try {
|
|
||||||
const result = await onSendCode(phone, {
|
|
||||||
challengeId: captchaChallenge?.challengeId,
|
|
||||||
answer: captchaAnswer,
|
|
||||||
});
|
|
||||||
setCooldownSeconds(result.cooldownSeconds);
|
|
||||||
setHint(
|
|
||||||
`短信请求已提交,请留意手机短信。验证码有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`,
|
|
||||||
);
|
|
||||||
setCaptchaAnswer('');
|
|
||||||
} catch {
|
|
||||||
// Error state is handled by the parent.
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sendingCode
|
|
||||||
? '发送中'
|
|
||||||
: cooldownSeconds > 0
|
|
||||||
? `${cooldownSeconds}s`
|
|
||||||
: '获取验证码'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<CaptchaChallengeField
|
|
||||||
challenge={captchaChallenge}
|
|
||||||
answer={captchaAnswer}
|
|
||||||
onAnswerChange={setCaptchaAnswer}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hint ? (
|
|
||||||
<div className="platform-banner platform-banner--success text-sm">
|
|
||||||
{hint}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{error ? (
|
|
||||||
<div className="platform-banner platform-banner--danger text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{phoneLoginEnabled ? (
|
{activeTab === 'login' ? (
|
||||||
<button
|
<form
|
||||||
type="submit"
|
className="flex flex-col gap-4"
|
||||||
disabled={loggingIn || !phone.trim() || !code.trim()}
|
onSubmit={(event) => {
|
||||||
className="platform-button platform-button--primary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
event.preventDefault();
|
||||||
>
|
if (!passwordLoginEnabled) {
|
||||||
{loggingIn ? '登录中' : '登录'}
|
return;
|
||||||
</button>
|
}
|
||||||
) : null}
|
void onPasswordSubmit(username, password);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{passwordLoginEnabled ? (
|
||||||
|
<>
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>账号</span>
|
||||||
|
<input
|
||||||
|
className="platform-input"
|
||||||
|
autoComplete="username"
|
||||||
|
value={username}
|
||||||
|
onChange={(event) => setUsername(event.target.value)}
|
||||||
|
placeholder="用户名"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>密码</span>
|
||||||
|
<input
|
||||||
|
className="platform-input"
|
||||||
|
autoComplete="current-password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
placeholder="输入密码"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{wechatLoginEnabled ? (
|
{error ? <ErrorBanner message={error} /> : null}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={wechatLoading || sendingCode || loggingIn}
|
|
||||||
className="platform-button platform-button--secondary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
onClick={() => {
|
|
||||||
void onStartWechatLogin();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{wechatLoading ? '跳转中' : '微信登录'}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!phoneLoginEnabled && !wechatLoginEnabled ? (
|
{passwordLoginEnabled ? (
|
||||||
<div className="platform-subpanel rounded-2xl px-4 py-4 text-sm text-[var(--platform-text-base)]">
|
<button
|
||||||
当前登录入口暂不可用。
|
type="submit"
|
||||||
</div>
|
disabled={submitDisabled || !username.trim() || !password.trim()}
|
||||||
) : null}
|
className="platform-button platform-button--primary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
</form>
|
>
|
||||||
|
{loggingIn ? '登录中' : '登录'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="self-center text-sm text-[var(--platform-accent)]"
|
||||||
|
onClick={() => setIsResetPanelOpen(true)}
|
||||||
|
>
|
||||||
|
忘记密码
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{wechatLoginEnabled ? (
|
||||||
|
<WechatButton
|
||||||
|
loading={wechatLoading}
|
||||||
|
disabled={submitDisabled}
|
||||||
|
onClick={onStartWechatLogin}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<PhoneCodeForm
|
||||||
|
phone={phone}
|
||||||
|
code={code}
|
||||||
|
captchaAnswer={captchaAnswer}
|
||||||
|
captchaChallenge={captchaChallenge}
|
||||||
|
cooldownSeconds={cooldownSeconds}
|
||||||
|
sendingCode={sendingCode}
|
||||||
|
loggingIn={loggingIn}
|
||||||
|
error={error}
|
||||||
|
hint={hint}
|
||||||
|
submitLabel="注册并登录"
|
||||||
|
enabled={phoneLoginEnabled}
|
||||||
|
onPhoneChange={setPhone}
|
||||||
|
onCodeChange={setCode}
|
||||||
|
onCaptchaAnswerChange={setCaptchaAnswer}
|
||||||
|
onSendCode={async () => {
|
||||||
|
setHint('');
|
||||||
|
const result = await onSendCode(phone, 'login', {
|
||||||
|
challengeId: captchaChallenge?.challengeId,
|
||||||
|
answer: captchaAnswer,
|
||||||
|
});
|
||||||
|
setCooldownSeconds(result.cooldownSeconds);
|
||||||
|
setHint(
|
||||||
|
`短信请求已提交,验证码有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`,
|
||||||
|
);
|
||||||
|
setCaptchaAnswer('');
|
||||||
|
}}
|
||||||
|
onSubmit={() => onPhoneSubmit(phone, code)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!passwordLoginEnabled && !phoneLoginEnabled && !wechatLoginEnabled ? (
|
||||||
|
<div className="platform-subpanel rounded-2xl px-4 py-4 text-sm text-[var(--platform-text-base)]">
|
||||||
|
当前登录入口暂不可用。
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TabButton({
|
||||||
|
active,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
active: boolean;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-10 rounded-full text-sm font-medium transition ${
|
||||||
|
active
|
||||||
|
? 'bg-[var(--platform-panel-bg)] text-[var(--platform-text-strong)] shadow-sm'
|
||||||
|
: 'text-[var(--platform-text-muted)]'
|
||||||
|
}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PhoneCodeForm({
|
||||||
|
phone,
|
||||||
|
code,
|
||||||
|
captchaAnswer,
|
||||||
|
captchaChallenge,
|
||||||
|
cooldownSeconds,
|
||||||
|
sendingCode,
|
||||||
|
loggingIn,
|
||||||
|
error,
|
||||||
|
hint,
|
||||||
|
submitLabel,
|
||||||
|
enabled,
|
||||||
|
onPhoneChange,
|
||||||
|
onCodeChange,
|
||||||
|
onCaptchaAnswerChange,
|
||||||
|
onSendCode,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
captchaAnswer: string;
|
||||||
|
captchaChallenge: AuthCaptchaChallenge | null;
|
||||||
|
cooldownSeconds: number;
|
||||||
|
sendingCode: boolean;
|
||||||
|
loggingIn: boolean;
|
||||||
|
error: string;
|
||||||
|
hint: string;
|
||||||
|
submitLabel: string;
|
||||||
|
enabled: boolean;
|
||||||
|
onPhoneChange: (value: string) => void;
|
||||||
|
onCodeChange: (value: string) => void;
|
||||||
|
onCaptchaAnswerChange: (value: string) => void;
|
||||||
|
onSendCode: () => Promise<void>;
|
||||||
|
onSubmit: () => Promise<void>;
|
||||||
|
}) {
|
||||||
|
if (!enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void onSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>手机号</span>
|
||||||
|
<input
|
||||||
|
className="platform-input"
|
||||||
|
autoComplete="tel"
|
||||||
|
inputMode="numeric"
|
||||||
|
value={phone}
|
||||||
|
onChange={(event) => onPhoneChange(event.target.value)}
|
||||||
|
placeholder="13800000000"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>验证码</span>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<input
|
||||||
|
className="platform-input min-w-0 flex-1"
|
||||||
|
inputMode="numeric"
|
||||||
|
value={code}
|
||||||
|
onChange={(event) => onCodeChange(event.target.value)}
|
||||||
|
placeholder="输入验证码"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={sendingCode || cooldownSeconds > 0 || !phone.trim()}
|
||||||
|
className="platform-button platform-button--secondary h-12 shrink-0 px-4 text-sm disabled:cursor-not-allowed disabled:opacity-55"
|
||||||
|
onClick={() => void onSendCode()}
|
||||||
|
>
|
||||||
|
{sendingCode
|
||||||
|
? '发送中'
|
||||||
|
: cooldownSeconds > 0
|
||||||
|
? `${cooldownSeconds}s`
|
||||||
|
: '获取验证码'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<CaptchaChallengeField
|
||||||
|
challenge={captchaChallenge}
|
||||||
|
answer={captchaAnswer}
|
||||||
|
onAnswerChange={onCaptchaAnswerChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hint ? <SuccessBanner message={hint} /> : null}
|
||||||
|
{error ? <ErrorBanner message={error} /> : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loggingIn || !phone.trim() || !code.trim()}
|
||||||
|
className="platform-button platform-button--primary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{loggingIn ? '处理中' : submitLabel}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PasswordResetPanel({
|
||||||
|
phone,
|
||||||
|
code,
|
||||||
|
password,
|
||||||
|
sendingCode,
|
||||||
|
loggingIn,
|
||||||
|
cooldownSeconds,
|
||||||
|
error,
|
||||||
|
onPhoneChange,
|
||||||
|
onCodeChange,
|
||||||
|
onPasswordChange,
|
||||||
|
onBack,
|
||||||
|
onSendCode,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
password: string;
|
||||||
|
sendingCode: boolean;
|
||||||
|
loggingIn: boolean;
|
||||||
|
cooldownSeconds: number;
|
||||||
|
error: string;
|
||||||
|
onPhoneChange: (value: string) => void;
|
||||||
|
onCodeChange: (value: string) => void;
|
||||||
|
onPasswordChange: (value: string) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
onSendCode: () => Promise<void>;
|
||||||
|
onSubmit: () => Promise<void>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-4 px-5 py-5"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void onSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>手机号</span>
|
||||||
|
<input
|
||||||
|
className="platform-input"
|
||||||
|
autoComplete="tel"
|
||||||
|
inputMode="numeric"
|
||||||
|
value={phone}
|
||||||
|
onChange={(event) => onPhoneChange(event.target.value)}
|
||||||
|
placeholder="13800000000"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>验证码</span>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<input
|
||||||
|
className="platform-input min-w-0 flex-1"
|
||||||
|
inputMode="numeric"
|
||||||
|
value={code}
|
||||||
|
onChange={(event) => onCodeChange(event.target.value)}
|
||||||
|
placeholder="输入验证码"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={sendingCode || cooldownSeconds > 0 || !phone.trim()}
|
||||||
|
className="platform-button platform-button--secondary h-12 shrink-0 px-4 text-sm disabled:cursor-not-allowed disabled:opacity-55"
|
||||||
|
onClick={() => void onSendCode()}
|
||||||
|
>
|
||||||
|
{sendingCode
|
||||||
|
? '发送中'
|
||||||
|
: cooldownSeconds > 0
|
||||||
|
? `${cooldownSeconds}s`
|
||||||
|
: '获取验证码'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||||
|
<span>新密码</span>
|
||||||
|
<input
|
||||||
|
className="platform-input"
|
||||||
|
autoComplete="new-password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => onPasswordChange(event.target.value)}
|
||||||
|
placeholder="设置新密码"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{error ? <ErrorBanner message={error} /> : null}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="platform-button platform-button--secondary h-12 px-4 text-base"
|
||||||
|
onClick={onBack}
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loggingIn || !phone.trim() || !code.trim() || !password.trim()}
|
||||||
|
className="platform-button platform-button--primary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{loggingIn ? '处理中' : '重置密码'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WechatButton({
|
||||||
|
loading,
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
loading: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
onClick: () => Promise<void>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={loading || disabled}
|
||||||
|
className="platform-button platform-button--secondary h-12 px-4 text-base disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
onClick={() => void onClick()}
|
||||||
|
>
|
||||||
|
{loading ? '跳转中' : '微信登录'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ErrorBanner({ message }: { message: string }) {
|
||||||
|
return <div className="platform-banner platform-banner--danger text-sm">{message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SuccessBanner({ message }: { message: string }) {
|
||||||
|
return <div className="platform-banner platform-banner--success text-sm">{message}</div>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type {
|
|||||||
AuthLoginMethod,
|
AuthLoginMethod,
|
||||||
AuthLoginOptionsResponse,
|
AuthLoginOptionsResponse,
|
||||||
AuthLogoutAllResponse,
|
AuthLogoutAllResponse,
|
||||||
|
AuthPasswordChangeResponse,
|
||||||
|
AuthPasswordResetResponse,
|
||||||
AuthMeResponse,
|
AuthMeResponse,
|
||||||
AuthPhoneChangeResponse,
|
AuthPhoneChangeResponse,
|
||||||
AuthPhoneLoginResponse,
|
AuthPhoneLoginResponse,
|
||||||
@@ -137,7 +139,7 @@ export function clearAuthSession() {
|
|||||||
|
|
||||||
export async function sendPhoneLoginCode(
|
export async function sendPhoneLoginCode(
|
||||||
phone: string,
|
phone: string,
|
||||||
scene: 'login' | 'bind_phone' | 'change_phone' = 'login',
|
scene: 'login' | 'bind_phone' | 'change_phone' | 'reset_password' = 'login',
|
||||||
captcha?: {
|
captcha?: {
|
||||||
challengeId?: string;
|
challengeId?: string;
|
||||||
answer?: string;
|
answer?: string;
|
||||||
@@ -257,6 +259,50 @@ export async function authEntry(username: string, password: string) {
|
|||||||
return response.user;
|
return response.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function changePassword(
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string,
|
||||||
|
) {
|
||||||
|
const response = await requestJson<AuthPasswordChangeResponse>(
|
||||||
|
'/api/auth/password/change',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
currentPassword: currentPassword.trim() || undefined,
|
||||||
|
newPassword: newPassword.trim(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'修改密码失败',
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetPassword(
|
||||||
|
phone: string,
|
||||||
|
code: string,
|
||||||
|
newPassword: string,
|
||||||
|
) {
|
||||||
|
const response = await requestJson<AuthPasswordResetResponse>(
|
||||||
|
'/api/auth/password/reset',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
phone: normalizePhoneInput(phone),
|
||||||
|
code: code.trim(),
|
||||||
|
newPassword: newPassword.trim(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'重置密码失败',
|
||||||
|
PUBLIC_AUTH_REQUEST_OPTIONS,
|
||||||
|
);
|
||||||
|
|
||||||
|
setStoredAccessToken(response.token, { emit: false });
|
||||||
|
return response.user;
|
||||||
|
}
|
||||||
|
|
||||||
export async function authEntryWithStoredCredentials(
|
export async function authEntryWithStoredCredentials(
|
||||||
credentials: AutoAuthCredentials,
|
credentials: AutoAuthCredentials,
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user