This commit is contained in:
2026-04-24 12:23:52 +08:00
11 changed files with 93 additions and 1061 deletions

View File

@@ -12,6 +12,7 @@ VOLCENGINE_ACCESS_KEY_ID="AKLTZWFjMmYzZTdjZTIxNDRiNTkzMTZiMTk2NzVmNTUxOGI"
VOLCENGINE_SECRET_ACCESS_KEY="TURRMk56bGhZalE0TjJReE5ERmpNMkpoTUdaa1lqRmtaVGt5TVRrM1lXSQ=="
WECHAT_AUTH_ENABLED="false"
WECHAT_AUTH_PROVIDER="mock"
JWT_EXPIRES_IN="7d"
SMS_AUTH_ENABLED="true"
SMS_AUTH_PROVIDER="aliyun"

View File

@@ -1,6 +1,7 @@
# AGENTS.md
## 项目约束
- 在修改server-rs的内容时不要去兼容server-node中的任何内容只允许参考以及把server-node中未迁移到server-rs的内容迁移过来
- 代码需要有完善的中文注释
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。
- 对工程的修改不仅要落地到代码更面还要更改对应文档若没有生成新的文档文档统一存在doc目录中

View File

@@ -7,7 +7,7 @@
本方案为当前仓库补齐 3 条 Jenkins 流水线:
1. `构建`:只负责在仓库根目录执行 `npm run deploy:rust:remote -- --skip-upload`,生成发布包。
2. `部署`:只负责把指定发布版本部署到 `/var/lib/jenkins/deploy/Genarrative/`禁止人工直接点击执行,并支持按参数决定是否清空 SpacetimeDB 数据。
2. `部署`:只负责把指定发布版本部署到 `/var/lib/jenkins/deploy/Genarrative/`允许人工按参数启动,并支持按参数决定是否清空 SpacetimeDB 数据。
3. `构建并部署`:先构建,再把构建出的版本号传给 `部署` 流水线并等待部署完成;同时暴露 `WEB_PORT` 参数,默认把发布包 Web 端口写成 `80`,并透传是否清库。
本次只补 Jenkins 编排与本地部署脚本,不改现有 Rust 发布包构建逻辑,不恢复旧 `server-node` 部署链。
@@ -16,13 +16,14 @@
1. 构建产物目录统一使用 `build/<版本号>/`
2. 默认使用 Jenkins `BUILD_NUMBER` 作为版本号,避免依赖时间戳;如有需要也允许显式传 `BUILD_VERSION`
3. `部署` 流水线必须校验当前构建原因包含上游触发 cause没有上游触发则直接失败
4. `部署` 流水线额外校验上游作业名与传入的 `EXPECTED_UPSTREAM_JOB` 一致;如配置了环境变量 `GENARRATIVE_ALLOWED_UPSTREAM_JOB`,还必须与该值一致
5. `构建并部署` 在触发 `部署` 前先释放自己的构建节点,避免单执行器节点出现死锁
6. `部署` 不重新构建,不重新上传,不从 Jenkins 插件仓库复制产物,直接使用上游构建节点的本地 `build/<版本号>/` 目录
7. `部署` 流水线读取触发原因时必须使用 `currentBuild.getBuildCauses(...)` 这类白名单方法,不能直接访问 `currentBuild.rawBuild`,否则会被 Jenkins Script Security 拦截
8. 由于 Jenkins Pipeline 的 `build` 步骤触发下游时,原因类型通常是 `org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause`,实现上需要同时兼容它和经典的 `hudson.model.Cause$UpstreamCause`,否则会把真实的上游触发误判成人工执行
9. 如果线上进程的启停必须经过 `sudo`,只允许 `start.sh` / `stop.sh` 这两个 hook 使用 `sudo -n` 执行,部署目录清空与文件覆盖仍保持普通权限
3. `构建``构建并部署``checkout scm` 后、实际构建前必须执行 `git reset --hard HEAD``git clean -fd`,避免固定源码目录内的 Git 变更和未跟踪文件影响发布包;不使用 `-x`,避免删除 `node_modules/` 等忽略目录后与 `RUN_NPM_CI=false` 冲突
4. `部署` 流水线允许人工启动;没有上游触发 cause 时按人工部署处理,不再直接失败
5. `部署` 流水线仅在存在上游触发 cause 时校验上游作业名与传入的 `EXPECTED_UPSTREAM_JOB` 一致;如配置了环境变量 `GENARRATIVE_ALLOWED_UPSTREAM_JOB`,还必须与该值一致
6. `构建并部署` 在触发 `部署` 前先释放自己的构建节点,避免单执行器节点出现死锁
7. `部署` 不重新构建,不重新上传,不从 Jenkins 插件仓库复制产物,直接使用上游构建节点的本地 `build/<版本号>/` 目录
8. `部署` 流水线读取触发原因时必须使用 `currentBuild.getBuildCauses(...)` 这类白名单方法,不能直接访问 `currentBuild.rawBuild`,否则会被 Jenkins Script Security 拦截
9. 由于 Jenkins Pipeline 的 `build` 步骤触发下游时,原因类型通常是 `org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause`,实现上需要同时兼容它和经典的 `hudson.model.Cause$UpstreamCause`,否则会把真实的上游触发误判成人工执行
10. 如果线上进程的启停必须经过 `sudo`,只允许 `start.sh` / `stop.sh` 这两个 hook 使用 `sudo -n` 执行,部署目录清空与文件覆盖仍保持普通权限。
## 3. 节点与工作区要求
@@ -49,15 +50,16 @@ jenkins/Jenkinsfile.build
核心流程:
1. 可选执行 `npm ci`
2. 在源码根目录执行:
1. `checkout scm` 后执行 `git reset --hard HEAD``git clean -fd` 清理工作区
2. 可选执行 `npm ci`
3. 在源码根目录执行:
```bash
npm run deploy:rust:remote -- --skip-upload --name <BUILD_VERSION>
```
3. 校验 `build/<BUILD_VERSION>/` 存在。
4. 归档 `build/<BUILD_VERSION>/**` 作为 Jenkins 产物。
4. 校验 `build/<BUILD_VERSION>/` 存在。
5. 归档 `build/<BUILD_VERSION>/**` 作为 Jenkins 产物。
默认版本号:
@@ -75,7 +77,7 @@ jenkins/Jenkinsfile.deploy
核心流程:
1. 校验触发原因必须是上游流水线,而不是人工点击;实现上同时兼容 `BuildUpstreamCause` 与经典 `UpstreamCause`
1. 读取触发原因;人工启动时跳过上游门禁,上游触发时同时兼容 `BuildUpstreamCause` 与经典 `UpstreamCause` 并继续校验上游作业名
2. 校验 `BUILD_VERSION``SOURCE_WORKSPACE_ROOT``DEPLOY_DIRECTORY` 非空。
3. 执行:
@@ -109,11 +111,12 @@ jenkins/Jenkinsfile.build-and-deploy
核心流程:
1. 复用与 `构建` 相同的构建命令生成 `build/<BUILD_VERSION>/`
2. 归档 `build/<BUILD_VERSION>/**`
3. 记录当前 `NODE_NAME`、源码根目录、版本号
4. 构建时额外透传 `--web-port <WEB_PORT>`,默认生成监听 `80` 的发布包
5. 触发 `部署` 流水线,并传递:
1. `checkout scm` 后执行 `git reset --hard HEAD``git clean -fd` 清理工作区
2. 复用与 `构建` 相同的构建命令生成 `build/<BUILD_VERSION>/`
3. 归档 `build/<BUILD_VERSION>/**`
4. 记录当前 `NODE_NAME`、源码根目录、版本号
5. 构建时额外透传 `--web-port <WEB_PORT>`,默认生成监听 `80` 的发布包。
6. 触发 `部署` 流水线,并传递:
- `BUILD_VERSION`
- `SOURCE_WORKSPACE_ROOT`
- `SOURCE_NODE_NAME`

View File

@@ -261,6 +261,12 @@
| `created_at` | `Timestamp` | 是 | 创建时间 |
| `updated_at` | `Timestamp` | 是 | 更新时间 |
### 主键约束
1. `card_id` 是 SpacetimeDB 表级全局主键,不能只使用 `world-foundation` 这类跨会话固定值。
2. Agent 自动生成的世界底稿卡统一使用 `custom-world:{session_id}:world-foundation`,确保同一会话内稳定 upsert、不同会话间不会发生唯一键冲突。
3. 不保留历史 `world-foundation` 主键兼容逻辑;线上旧脏数据如需清理,应通过一次性运维脚本处理,不进入 reducer 主链。
### 索引
1. `session_id`

View File

@@ -30,6 +30,16 @@ pipeline {
dir("${env.WORKSPACE_ROOT}") {
checkout scm
sh '''
bash -lc '
set -euo pipefail
# 构建前清理工作区内的 Git 变更和未跟踪文件,避免复用固定源码目录时受到上次构建残留影响。
# 这里不使用 -x避免删除 node_modules 等忽略目录后与 RUN_NPM_CI=false 的配置冲突。
git reset --hard HEAD
git clean -fd
'
'''
script {
// 是否重装依赖交给流水线参数决定,避免每次构建都重复执行 npm ci。
if (params.RUN_NPM_CI) {

View File

@@ -37,6 +37,16 @@ pipeline {
dir("${env.WORKSPACE_ROOT}") {
checkout scm
sh '''
bash -lc '
set -euo pipefail
# 构建前清理工作区内的 Git 变更和未跟踪文件,避免复用固定源码目录时受到上次构建残留影响。
# 这里不使用 -x避免删除 node_modules 等忽略目录后与 RUN_NPM_CI=false 的配置冲突。
git reset --hard HEAD
git clean -fd
'
'''
script {
// 是否重装依赖交给流水线参数决定,避免每次构建都重复执行 npm ci。
if (params.RUN_NPM_CI) {

View File

@@ -24,6 +24,7 @@ pipeline {
steps {
script {
// 部署流水线允许手动启动;如存在上游触发原因,则继续执行上游作业名门禁。
// Pipeline 的 build 步骤通常会把下游触发原因记录成 BuildUpstreamCause
// 直接只查经典 UpstreamCause 会把真实的上游触发误判成“人工执行”。
def pipelineUpstreamCauses = currentBuild.getBuildCauses('org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause')
@@ -36,10 +37,6 @@ pipeline {
upstreamCause = classicUpstreamCauses[0]
}
if (!upstreamCause) {
error('部署流水线禁止人工直接执行,只允许由上游构建并部署流水线触发。')
}
def actualUpstreamJob = upstreamCause?.upstreamProject ?: ''
def expectedUpstreamJob = params.EXPECTED_UPSTREAM_JOB?.trim()
def allowedUpstreamJob = env.GENARRATIVE_ALLOWED_UPSTREAM_JOB?.trim()
@@ -56,19 +53,19 @@ pipeline {
error('SOURCE_NODE_NAME 不能为空。')
}
if (!actualUpstreamJob?.trim()) {
if (upstreamCause && !actualUpstreamJob?.trim()) {
error('无法从上游触发原因中解析作业名,请检查 Jenkins Pipeline Build Step 插件版本与触发链。')
}
if (expectedUpstreamJob && actualUpstreamJob != expectedUpstreamJob) {
if (actualUpstreamJob && expectedUpstreamJob && actualUpstreamJob != expectedUpstreamJob) {
error("上游作业校验失败,期望 ${expectedUpstreamJob},实际 ${actualUpstreamJob}")
}
if (allowedUpstreamJob && actualUpstreamJob != allowedUpstreamJob) {
if (actualUpstreamJob && allowedUpstreamJob && actualUpstreamJob != allowedUpstreamJob) {
error("环境门禁校验失败,仅允许 ${allowedUpstreamJob} 触发,实际 ${actualUpstreamJob}")
}
env.UPSTREAM_JOB_NAME = actualUpstreamJob
env.UPSTREAM_JOB_NAME = actualUpstreamJob ?: 'MANUAL'
}
}
}

View File

@@ -550,8 +550,8 @@ PUBLISH_ARGS=(
)
if [[ "${CLEAR_DATABASE}" -eq 1 ]]; then
# 按当前 SpacetimeDB CLI 约定使用 -c=always,等价于 --delete-data always。
PUBLISH_ARGS+=(-c always)
# 按当前 SpacetimeDB CLI 约定使用 -c等价于 --delete-data always。
PUBLISH_ARGS+=(-c)
fi
echo "[start] 发布 SpacetimeDB wasm: ${SPACETIME_DATABASE}"

File diff suppressed because it is too large Load Diff

View File

@@ -4142,7 +4142,13 @@ fn upsert_world_foundation_card(
draft_profile: &JsonMap<String, JsonValue>,
updated_at_micros: i64,
) -> Result<(), String> {
let card_id = "world-foundation".to_string();
let card_id = build_world_foundation_card_id(session_id);
let existing_card = ctx
.db
.custom_world_draft_card()
.card_id()
.find(&card_id)
.filter(|row| row.session_id == session_id);
let title = read_optional_text_field(draft_profile, &["name", "title"])
.unwrap_or_else(|| "世界底稿".to_string());
let subtitle = read_optional_text_field(draft_profile, &["subtitle"]).unwrap_or_default();
@@ -4164,13 +4170,7 @@ fn upsert_world_foundation_card(
"warningMessages": [],
}))?;
if let Some(existing) = ctx
.db
.custom_world_draft_card()
.card_id()
.find(&card_id)
.filter(|row| row.session_id == session_id)
{
if let Some(existing) = existing_card {
replace_custom_world_draft_card(
ctx,
&existing,
@@ -4221,6 +4221,11 @@ fn upsert_world_foundation_card(
Ok(())
}
fn build_world_foundation_card_id(session_id: &str) -> String {
// `custom_world_draft_card.card_id` 是全局主键,世界底稿卡必须带上会话维度,避免多会话写入时触发唯一键冲突。
format!("custom-world:{session_id}:world-foundation")
}
fn sync_session_draft_profile_from_card_update(
session: &CustomWorldAgentSession,
card: &CustomWorldDraftCard,

View File

@@ -384,10 +384,11 @@ function CatalogCard({
if (layout === 'compact') {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
<div
role="button"
tabIndex={disabled ? -1 : 0}
onClick={disabled ? undefined : onClick}
aria-disabled={disabled}
className={`w-full rounded-[1.3rem] border p-2.5 text-left transition-colors ${
isSelected
? 'border-rose-300/35 bg-rose-500/10'
@@ -416,15 +417,16 @@ function CatalogCard({
{actions ? <div className="mt-2 flex flex-wrap gap-2">{actions}</div> : null}
</div>
</div>
</button>
</div>
);
}
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
<div
role="button"
tabIndex={disabled ? -1 : 0}
onClick={disabled ? undefined : onClick}
aria-disabled={disabled}
className={`w-full rounded-[1.4rem] border p-3 text-left transition-colors ${
isSelected
? 'border-rose-300/35 bg-rose-500/10'
@@ -451,7 +453,7 @@ function CatalogCard({
</div>
{actions ? <div className="flex flex-wrap gap-2">{actions}</div> : null}
</div>
</button>
</div>
);
}