4 Commits

Author SHA1 Message Date
湛兮 8415291772 feat: 补齐员工端 agent skill 规范
- .agents: 新增中文提交与注释同步 skill 及项目索引

- AGENTS.md: 增加本地 skill 入口并保留 RTK 引用

- README.md: 同步 Agent 文档入口
2026-06-11 17:48:08 +08:00
湛兮 f8386d7b02 feat: 完善环境标识配置 2026-06-05 15:35:51 +08:00
湛兮 5fe2e2c75c chore: move Jenkins pipeline config out of repo 2026-06-05 15:08:11 +08:00
湛兮 3e1789e124 ci: harden production tag selection 2026-06-05 14:45:44 +08:00
16 changed files with 236 additions and 406 deletions
+21
View File
@@ -0,0 +1,21 @@
# role-user Agent Skills
本目录存放 `role-user` 的项目级 Agent skill。规则参考 SeaCloud 项目的 agent skill 组织方式,并按当前员工端 Next.js、TypeScript、移动端工作台场景保留最小必要规范。
## 使用入口
1. 进入仓库后先读 `AGENTS.md``RTK.md`
2. 创建或修改代码文件时,使用 `header-comment-sync` 保持中文文件头、导出声明和复杂逻辑注释同步。
3. 提交或推送代码时,使用 `chinese-commit-message` 生成英文 type 前缀加中文摘要的 commit message。
4. 所有 skill 的触达文件补齐规则只处理本次任务相关文件,不要求为了单次任务全仓扫描。
## 当前项目事实
- 应用类型:员工端 C 端移动优先工作台。
- 技术栈:Next.js App Router、React、TypeScript。
- 关键边界:BFF Route Handlers 负责会话与后端转发,前端不持久化 JWT 或明文密码。
## Skill 索引
- `header-comment-sync`:中文文件头、导出声明、复杂逻辑和风险边界注释。
- `chinese-commit-message`:中文提交信息格式。
@@ -0,0 +1,54 @@
---
name: chinese-commit-message
description: 在本仓库执行 git commit、整理提交说明或准备推送时使用,保持提交信息使用英文类型前缀加中文描述。
---
# 中文提交信息
## 何时使用
- 用户要求提交、推送或整理 commit message。
- 你准备执行 `git commit`
- 需要总结当前 diff 的提交说明。
## 核心要求
1. 提交信息必须使用英文 type 加中文摘要,例如 `feat: 补齐员工端 agent skill 规范`
2. 常用 type 优先使用 `feat``fix``refactor``docs``chore`
3. 不强制 scope;只有模块边界非常清楚时才使用 `feat(auth): ...`
4. 标题部分使用简洁中文,直接说明改动,不写“修改一下”“更新代码”这类空泛描述。
5. 非 trivial 改动建议补中文正文,用扁平 bullet 按文件或内容块说明关键动作。
6. 正文与标题之间空一行;正文每条 bullet 对应一个独立文件或明确内容块。
7. 如果用户指定提交信息,优先尊重用户原意,只做必要格式整理。
## 触达文件补齐
- 不要求为了提交信息单独全仓扫描。
- 提交前如果 diff 中的触达文件明显违反对应 skill 的触达补齐要求,应先修正当前相关链路,再整理 commit message。
- commit 正文应概括本次触达补齐,例如“补齐触达文件注释”“同步 README 目录说明”。
## 推荐结构
```text
feat: 补齐员工端 agent skill 规范
- .agents/README.md: 新增项目级 skill 索引和使用入口
- .agents/skills: 补充中文提交与注释同步规则
- AGENTS.md: 增加本地 skill 入口
```
## 不推荐写法
- `新增规则`
- `feat: add skills`
- `fix bug`
- `update`
- 复杂改动只有标题没有正文。
- 正文写成长段流水账或嵌套列表。
## 落地检查
- type 是否合理。
- 中文摘要是否覆盖主改动。
- 是否需要正文。
- 正文是否按文件或内容块归纳。
@@ -0,0 +1,68 @@
---
name: header-comment-sync
description: 在本仓库创建或修改 ts、tsx、js、jsx、mjs、cjs、vue、astro 文件时使用,保持中文文件头、导出声明、复杂逻辑和风险边界注释准确。
---
# 注释规范与同步
## 目标
让下一次进入文件的维护者能快速理解当前职责、关键约束和风险边界。注释解释“为什么”和“边界”,不复述代码表面行为。
## 适用场景
- 新增或修改页面、组件、Hook、store、service、utils、类型、配置或脚本文件。
- 修改 BFF Route Handler、鉴权、Cookie、权限、后端协议转换、缓存或错误兜底逻辑。
- 发现旧注释与当前代码行为不一致。
## 核心原则
- 注释使用中文,描述当前事实。
- 简单局部变量不强行注释。
- 文件头说明文件当前职责,1 到 2 行即可。
- 导出的函数、组件、Hook、类型、配置对象、service method 应有用途说明。
- 复杂兼容逻辑、后端协议、竞态锁、缓存、权限映射和会话安全边界需要短注释。
- TODO/FIXME 必须说明触发条件、剩余动作和可删除条件。
## 文件头规则
- 每个适用文件都要有准确文件头。
- 如果文件必须以 `"use client"``"use server"` 开头,文件头注释放在指令之后、导入之前。
- Vue 文件不为了补头注释重排模板结构;在 `<script>``<script setup>` 顶部补当前职责说明。
- 文件头不要写“本文件用于...”这类空话,直接说明业务角色。
```ts
"use client";
/**
* 员工端任务详情页,负责加载任务、发起流转动作并展示备注时间线。
*/
```
## JSDoc 与局部注释
- 导出的函数、类、组件、Hook、类型、常量和配置对象优先使用 JSDoc。
- 非导出但复杂的解析、格式化、请求构造、状态派生、回调工厂也要补。
- 只在局部逻辑确实有约束时使用行内注释,例如竞态锁、兼容字段、后端协议和临时迁移。
- 不要在每一行、每个 JSX 片段、每个短生命周期变量上堆注释。
## 触达文件补齐
- 不要求为了注释规范单独全仓扫描。
- 只要本次任务修改了适用文件,就顺手补齐明显缺失或过时的文件头、导出声明、共享类型、复杂回调和协议注释。
- 大文件可按触达区域优先,但同文件内裸露的顶层导出和共享类型应一起补。
- 如果一次补齐整个大文件会明显超出需求范围,至少补齐当前需求链路和顶层声明,并在交付说明里说明剩余范围。
## 不推荐的写法
- 注释只写“处理数据”“点击事件”这种无信息内容。
- 注释描述旧方案,和当前代码矛盾。
- 为每个 JSX 片段、简单赋值、短生命周期变量写注释。
- 用英文口号、emoji 或情绪化标记替代项目内中文说明。
## 落地检查
- 修改后的文件头是否准确。
- 新增/修改的导出声明是否有必要说明。
- 复杂逻辑是否解释了约束而不是复述代码。
- 旧注释是否仍然可信。
+6
View File
@@ -0,0 +1,6 @@
ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api
ROLE_USER_SESSION_COOKIE=role_user_session_develop
APP_ENV=develop
APP_ENV_LABEL=测试环境
PORT=3211
HOSTNAME=0.0.0.0
+5 -1
View File
@@ -1,2 +1,6 @@
ACCESS_MANAGE_API_BASE_URL=http://localhost:3500/api ACCESS_MANAGE_API_BASE_URL=http://localhost:3500/api
ROLE_USER_SESSION_COOKIE=role_user_session ROLE_USER_SESSION_COOKIE=role_user_session_local
APP_ENV=local
APP_ENV_LABEL=本地环境
PORT=3210
HOSTNAME=0.0.0.0
+2 -2
View File
@@ -1,6 +1,6 @@
ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api
ROLE_USER_SESSION_COOKIE=role_user_session_test ROLE_USER_SESSION_COOKIE=role_user_session_develop
APP_ENV=test APP_ENV=develop
APP_ENV_LABEL=测试环境 APP_ENV_LABEL=测试环境
PORT=3211 PORT=3211
HOSTNAME=0.0.0.0 HOSTNAME=0.0.0.0
+13
View File
@@ -0,0 +1,13 @@
@RTK.md
# role-user Agent 入口
## 必用 Skill
- 创建或修改 `ts``tsx``js``jsx``mjs``cjs``vue``astro` 文件时,使用 `./.agents/skills/header-comment-sync/SKILL.md`
- 执行 `git commit`、整理提交说明或准备推送时,使用 `./.agents/skills/chinese-commit-message/SKILL.md`
## 本地规则
- 先读 `RTK.md`,再按任务读取 `.agents/README.md` 中匹配的 skill。
- skill 的触达文件补齐只处理本次修改相关文件,不为单次任务全仓扫描。
Vendored
-138
View File
@@ -1,138 +0,0 @@
def isProductionDeploy() {
return params.DEPLOY_ENV == "production"
}
def isTestDeploy() {
def branch = env.BRANCH_NAME ?: ""
return params.DEPLOY_ENV == "test" && !params.SKIP_DEPLOY && branch == env.TEST_BRANCH
}
@NonCPS
boolean isUserTriggeredBuild() {
return currentBuild.rawBuild.getCauses().any { cause ->
cause.class.name == "hudson.model.Cause\$UserIdCause"
}
}
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: "30", artifactNumToKeepStr: "10"))
}
parameters {
choice(
name: "DEPLOY_ENV",
choices: ["test", "production"],
description: "test: develop 合并后自动部署测试环境;production: 仅允许手动输入 Tag 部署"
)
string(name: "RELEASE_TAG", defaultValue: "", description: "生产部署必填,必须是 Gitea 中已存在的 Tag")
booleanParam(name: "SKIP_DEPLOY", defaultValue: false, description: "只构建检查,不执行部署")
}
environment {
PROJECT_NAME = "role-user"
TEST_BRANCH = "develop"
DEPLOY_BASE_DIR = "/srv/www"
}
stages {
stage("Validate deploy policy") {
steps {
script {
if (isProductionDeploy()) {
if (!isUserTriggeredBuild()) {
error("生产环境禁止自动触发,只能在 Jenkins 手动 Build With Parameters。")
}
if (!params.RELEASE_TAG?.trim()) {
error("生产环境部署必须填写 RELEASE_TAG,且只能从项目 Tag 部署。")
}
}
if (params.DEPLOY_ENV == "test" && env.BRANCH_NAME && env.BRANCH_NAME != env.TEST_BRANCH) {
echo "当前分支 ${env.BRANCH_NAME} 不是 ${env.TEST_BRANCH},本次只构建检查,不自动部署测试环境。"
}
}
}
}
stage("Checkout production tag") {
when {
expression { isProductionDeploy() }
}
steps {
sh '''
set -euo pipefail
git fetch --tags --force origin '+refs/tags/*:refs/tags/*'
tag_commit="$(git rev-parse -q --verify "refs/tags/${RELEASE_TAG}^{commit}")"
if [ -z "${tag_commit}" ]; then
echo "Tag not found: ${RELEASE_TAG}" >&2
exit 1
fi
git checkout -f "${tag_commit}"
git log -1 --oneline
'''
}
}
stage("Install") {
steps {
sh '''
corepack enable || true
pnpm install --frozen-lockfile
'''
}
}
stage("Verify") {
steps {
sh '''
pnpm typecheck
pnpm lint
'''
}
}
stage("Build") {
steps {
script {
sh isProductionDeploy() ? "pnpm build:prod" : "pnpm build:test"
}
}
}
stage("Package standalone") {
steps {
sh '''
set -euo pipefail
rm -rf .deploy/role-user
mkdir -p .deploy/role-user/.next
cp -R .next/standalone/. .deploy/role-user/
cp -R .next/static .deploy/role-user/.next/static
cp -R public .deploy/role-user/public
'''
}
}
stage("Deploy test") {
when {
expression { isTestDeploy() }
}
steps {
sh "bash deploy/jenkins/deploy-standalone.sh test .deploy/role-user"
}
}
stage("Deploy production") {
when {
expression { isProductionDeploy() && !params.SKIP_DEPLOY }
}
steps {
sh "bash deploy/jenkins/deploy-standalone.sh production .deploy/role-user"
}
}
}
}
-97
View File
@@ -1,97 +0,0 @@
pipeline {
agent any
parameters {
gitParameter(
name: 'RELEASE_TAG',
type: 'PT_TAG',
tagFilter: 'v*',
branchFilter: 'origin/(.*)',
sortMode: 'DESCENDING_SMART',
selectedValue: 'TOP',
useRepository: 'http://127.0.0.1:3001/my-project/role-user.git',
quickFilterEnabled: true,
listSize: '10',
requiredParameter: true,
description: '请选择要部署到生产环境的 Git Tag。列表自动来自当前项目仓库,生产只能从 Tag 发布。'
)
}
environment {
CI = 'true'
}
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Checkout Tag') {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: '*/master']],
userRemoteConfigs: [[
url: 'http://127.0.0.1:3001/my-project/role-user.git'
]]
])
sh '''
set -eu
test -n "$RELEASE_TAG"
NORMALIZED_RELEASE_TAG="$(printf '%s' "$RELEASE_TAG" | sed 's/\\^{}$//')"
echo "Deploying production tag: $NORMALIZED_RELEASE_TAG"
git fetch --tags --force
git checkout -f "refs/tags/$NORMALIZED_RELEASE_TAG"
git describe --tags --exact-match HEAD
'''
}
}
stage('Env') {
steps {
sh '''
git --version
node -v
corepack --version
corepack enable
corepack prepare pnpm@11.5.0 --activate
pnpm -v
'''
}
}
stage('Install') {
steps {
sh '''
pnpm install --frozen-lockfile
'''
}
}
stage('Check') {
steps {
sh '''
pnpm typecheck
'''
}
}
stage('Build') {
steps {
sh '''
pnpm build:prod
test -f .next/standalone/server.js
test -d .next/static
'''
}
}
stage('Deploy Production') {
steps {
sh 'sudo /usr/local/bin/deploy-role-user-from-jenkins "$WORKSPACE"'
}
}
}
}
+21 -1
View File
@@ -26,11 +26,23 @@ pnpm dev
```bash ```bash
ACCESS_MANAGE_API_BASE_URL=http://localhost:3500/api ACCESS_MANAGE_API_BASE_URL=http://localhost:3500/api
ROLE_USER_SESSION_COOKIE=role_user_session ROLE_USER_SESSION_COOKIE=role_user_session_local
APP_ENV=local
APP_ENV_LABEL=本地环境
PORT=3210
HOSTNAME=0.0.0.0
``` ```
`ACCESS_MANAGE_API_BASE_URL` 指向 `access-manage` 服务端 API 根路径。员工登录会调用后端 `POST /api/auth/employee/login`,登录成功后只把 JWT 写入 Next.js 服务端 HttpOnly Cookie,不写入 `localStorage` `ACCESS_MANAGE_API_BASE_URL` 指向 `access-manage` 服务端 API 根路径。员工登录会调用后端 `POST /api/auth/employee/login`,登录成功后只把 JWT 写入 Next.js 服务端 HttpOnly Cookie,不写入 `localStorage`
环境标识按 `APP_ENV` 区分:
- `local`: 本地环境,默认使用 `.env.example` 复制出的 `.env.local`
- `develop`: 测试环境,参考 `.env.develop.example``.env.test.example` 保留为兼容入口。
- `production`: 生产环境,参考 `.env.production.example`
`APP_ENV_LABEL` 可覆盖页面右上角显示文案;未配置时会按 `APP_ENV` 自动显示“本地环境”“测试环境”或“生产环境”。Cookie 只在 `APP_ENV=production` 时设置 `Secure`,避免测试环境 HTTP 访问时登录后被浏览器丢弃 Cookie。
## 可用命令 ## 可用命令
```bash ```bash
@@ -38,6 +50,10 @@ pnpm dev
pnpm typecheck pnpm typecheck
pnpm lint pnpm lint
pnpm build pnpm build
pnpm build:develop
pnpm build:prod
pnpm start:develop
pnpm start:prod
``` ```
## 当前实现范围 ## 当前实现范围
@@ -83,8 +99,12 @@ pnpm build
## 文档 ## 文档
- `.agents/README.md`: 本项目 Agent skill 索引和使用入口。
- `.agents/skills/chinese-commit-message/SKILL.md`: 中文提交信息规范。
- `.agents/skills/header-comment-sync/SKILL.md`: 中文文件头和注释同步规范。
- `docs/C_EMPLOYEE_APP_REQUIREMENTS.md`: C 端员工工作台需求文档。 - `docs/C_EMPLOYEE_APP_REQUIREMENTS.md`: C 端员工工作台需求文档。
- `docs/FULLSTACK_BACKEND_GAP_ANALYSIS.md`: C 端员工工作台三端缺口与改动范围分析。 - `docs/FULLSTACK_BACKEND_GAP_ANALYSIS.md`: C 端员工工作台三端缺口与改动范围分析。
- `AGENTS.md`: Codex/Agent 入口规则,转到 `RTK.md` 并登记本地 skill。
- `RTK.md`: 本项目 Codex/Agent 协作规则。 - `RTK.md`: 本项目 Codex/Agent 协作规则。
## 关联项目 ## 关联项目
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
deploy_env="${1:?Usage: deploy-standalone.sh test|production [artifact_dir]}"
artifact_dir="${2:-.deploy/role-user}"
case "${deploy_env}" in
test)
env_file=".env.test"
;;
production)
env_file=".env.production"
;;
*)
echo "Unknown deploy environment: ${deploy_env}" >&2
exit 1
;;
esac
if [[ ! -f "${artifact_dir}/server.js" ]]; then
echo "Standalone server.js not found in ${artifact_dir}" >&2
exit 1
fi
project_name="${PROJECT_NAME:-role-user}"
base_dir="${DEPLOY_BASE_DIR:-/srv/www}"
target_dir="${DEPLOY_TARGET_DIR:-${base_dir}/${deploy_env}/${project_name}}"
deploy_remote="${DEPLOY_REMOTE:-}"
release_id="${BUILD_NUMBER:-manual}-$(git rev-parse --short=12 HEAD 2>/dev/null || date +%Y%m%d%H%M%S)"
remote_shell() {
if [[ -n "${deploy_remote}" ]]; then
ssh "${deploy_remote}" "$@"
else
bash -lc "$*"
fi
}
remote_copy() {
local source_dir="$1"
local dest_dir="$2"
if [[ -n "${deploy_remote}" ]]; then
rsync -az --delete "${source_dir}/" "${deploy_remote}:${dest_dir}/"
else
mkdir -p "${dest_dir}"
rsync -az --delete "${source_dir}/" "${dest_dir}/"
fi
}
release_dir="${target_dir}/releases/${release_id}"
shared_dir="${target_dir}/shared"
remote_shell "mkdir -p '${release_dir}' '${shared_dir}' '${target_dir}/logs'"
remote_copy "${artifact_dir}" "${release_dir}"
remote_shell "
set -euo pipefail
if [ -f '${shared_dir}/${env_file}' ]; then
ln -sfn '../../shared/${env_file}' '${release_dir}/${env_file}'
else
echo 'Missing ${shared_dir}/${env_file}; create it before starting the service.' >&2
fi
ln -sfn '${release_dir}' '${target_dir}/current'
"
if [[ -n "${DEPLOY_POST_DEPLOY_CMD:-}" ]]; then
remote_shell "cd '${target_dir}/current' && ${DEPLOY_POST_DEPLOY_CMD}"
else
echo "Deployed ${project_name} ${deploy_env} to ${target_dir}/current"
echo "Start with: set -a; source ${target_dir}/shared/${env_file}; set +a; node ${target_dir}/current/server.js"
fi
-91
View File
@@ -1,91 +0,0 @@
# 环境拆分与流水线规则
`role-user` 是 Next.js standalone 服务。测试环境和生产环境通过独立部署目录、独立运行时环境变量和独立后端 API 地址拆分。
## 环境约定
| 环境 | 触发方式 | 代码依据 | 构建命令 | 默认部署目录 | 默认端口 | 后端 API |
| --- | --- | --- | --- | --- | --- | --- |
| 测试环境 | `develop` 合并后自动触发 Jenkins | `develop` 最新提交 | `pnpm build:test` | `/srv/www/test/role-user/current` | `3211` | `http://127.0.0.1:3501/api` |
| 生产环境 | Jenkins 手动触发 | Gitea 项目 Tag | `pnpm build:prod` | `/srv/www/production/role-user/current` | `3210` | `http://127.0.0.1:3500/api` |
生产环境禁止因代码合并自动部署。生产部署时必须在 Jenkins 参数中选择 `DEPLOY_ENV=production` 并填写已存在的 `RELEASE_TAG`
## Jenkins 参数
| 参数 | 说明 |
| --- | --- |
| `DEPLOY_ENV` | `test``production`。默认 `test`。 |
| `RELEASE_TAG` | 生产环境必填,必须是 Gitea 仓库中已存在的 Tag。 |
| `SKIP_DEPLOY` | 为 `true` 时只执行安装、检查、构建,不部署。 |
`Jenkinsfile` 会强制校验:
- 测试环境只在 `develop` 分支自动部署。
- 生产环境必须手动触发。
- 生产环境必须填写 `RELEASE_TAG`
- 生产环境构建会先 checkout 到该 Tag 对应的提交,再部署。
## 运行时环境变量
真实环境变量必须留在服务器,不提交到仓库。
部署脚本默认读取:
```text
/srv/www/test/role-user/shared/.env.test
/srv/www/production/role-user/shared/.env.production
```
示例文件:
```text
.env.test.example
.env.production.example
```
`ACCESS_MANAGE_API_BASE_URL` 是服务端 BFF 使用的运行时变量,不加 `NEXT_PUBLIC_`,不会暴露到浏览器 bundle。Cookie 名测试和生产应分开,避免同域下会话互相覆盖。
## standalone 产物
`next.config.ts` 已启用 `output: "standalone"`。Jenkins 构建后会打包:
```text
.next/standalone
.next/static
public
```
部署脚本会发布到:
```text
${DEPLOY_BASE_DIR}/${DEPLOY_ENV}/role-user/releases/<build>-<commit>
```
并更新:
```text
${DEPLOY_BASE_DIR}/${DEPLOY_ENV}/role-user/current
```
如果 Jenkins 不在目标服务器上运行,可以配置:
```text
DEPLOY_REMOTE=user@server
```
## 生产发布流程
1. 在需要发布的提交上创建 Tag。
2. 推送 Tag 到 Gitea。
3. Jenkins 手动 Build With Parameters。
4. 选择 `DEPLOY_ENV=production`
5. 填写 `RELEASE_TAG`
6. 执行构建部署。
示例:
```bash
git tag -a v2026.06.05-1 -m "role-user production release 2026-06-05"
git push origin v2026.06.05-1
```
+6 -3
View File
@@ -3,11 +3,14 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3210", "dev": "APP_ENV=local APP_ENV_LABEL=本地环境 ACCESS_MANAGE_API_BASE_URL=http://localhost:3500/api ROLE_USER_SESSION_COOKIE=role_user_session_local next dev -p 3210",
"build": "next build", "build": "next build",
"build:test": "next build", "build:develop": "APP_ENV=develop APP_ENV_LABEL=测试环境 ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api ROLE_USER_SESSION_COOKIE=role_user_session_develop next build",
"build:prod": "next build", "build:test": "pnpm build:develop",
"build:prod": "APP_ENV=production APP_ENV_LABEL=生产环境 ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3500/api ROLE_USER_SESSION_COOKIE=role_user_session next build",
"start": "next start", "start": "next start",
"start:develop": "APP_ENV=develop APP_ENV_LABEL=测试环境 ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api ROLE_USER_SESSION_COOKIE=role_user_session_develop next start -p 3211",
"start:prod": "APP_ENV=production APP_ENV_LABEL=生产环境 ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3500/api ROLE_USER_SESSION_COOKIE=role_user_session next start -p 3210",
"lint": "eslint", "lint": "eslint",
"typecheck": "tsc --noEmit --incremental false" "typecheck": "tsc --noEmit --incremental false"
}, },
+2 -1
View File
@@ -1,6 +1,7 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { connection } from "next/server"; import { connection } from "next/server";
import { getAppEnvLabel } from "@/lib/environment";
import "./globals.css"; import "./globals.css";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -28,7 +29,7 @@ export const viewport: Viewport = {
export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
await connection(); await connection();
const environmentLabel = process.env.APP_ENV_LABEL || "生产环境"; const environmentLabel = getAppEnvLabel();
return ( return (
<html lang="zh-CN"> <html lang="zh-CN">
+35
View File
@@ -0,0 +1,35 @@
import "server-only";
export type AppEnv = "local" | "develop" | "production";
export function getAppEnv(): AppEnv {
const appEnv = process.env.APP_ENV;
if (appEnv === "production") {
return "production";
}
if (appEnv === "develop" || appEnv === "test") {
return "develop";
}
if (appEnv === "local") {
return "local";
}
return process.env.NODE_ENV === "production" ? "production" : "local";
}
export function getAppEnvLabel() {
if (process.env.APP_ENV_LABEL) {
return process.env.APP_ENV_LABEL;
}
const labels: Record<AppEnv, string> = {
local: "本地环境",
develop: "测试环境",
production: "生产环境"
};
return labels[getAppEnv()];
}
+3 -1
View File
@@ -2,6 +2,8 @@ import "server-only";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { getAppEnv } from "@/lib/environment";
const DEFAULT_COOKIE_NAME = "role_user_session"; const DEFAULT_COOKIE_NAME = "role_user_session";
export function getSessionCookieName() { export function getSessionCookieName() {
@@ -17,7 +19,7 @@ export async function setSessionToken(token: string) {
cookieStore.set(getSessionCookieName(), token, { cookieStore.set(getSessionCookieName(), token, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: getAppEnv() === "production",
sameSite: "lax", sameSite: "lax",
path: "/", path: "/",
maxAge: 60 * 60 * 8 maxAge: 60 * 60 * 8