nodebbs 0.1.0 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,37 +3,22 @@ services:
3
3
  postgres:
4
4
  image: postgres:16-alpine
5
5
  container_name: nodebbs-postgres
6
- restart: always
6
+ restart: unless-stopped
7
7
  environment:
8
8
  POSTGRES_USER: postgres
9
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
9
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_password}
10
10
  POSTGRES_DB: ${POSTGRES_DB:-nodebbs}
11
- TZ: ${TZ:-Asia/Shanghai}
11
+ TZ: Asia/Shanghai
12
12
  volumes:
13
13
  - postgres_data:/var/lib/postgresql/data
14
- # 生产环境默认不暴露数据库端口到主机,只在 Docker 网络内部访问
15
- # 如果需要外部访问 (如开发调试),可以将下面两行取消注释
16
- # ports:
17
- # - "${POSTGRES_PORT:-5432}:5432"
14
+
15
+ ports:
16
+ - "${POSTGRES_PORT:-5432}:5432"
18
17
  healthcheck:
19
18
  test: ["CMD-SHELL", "pg_isready -U postgres"]
20
- interval: 30s
21
- timeout: 10s
19
+ interval: 10s
20
+ timeout: 5s
22
21
  retries: 5
23
- start_period: 40s
24
- deploy:
25
- resources:
26
- limits:
27
- cpus: '1'
28
- memory: 512M
29
- reservations:
30
- cpus: '0.25'
31
- memory: 256M
32
- logging:
33
- driver: "json-file"
34
- options:
35
- max-size: "10m"
36
- max-file: "3"
37
22
  networks:
38
23
  - nodebbs-network
39
24
 
@@ -41,62 +26,46 @@ services:
41
26
  redis:
42
27
  image: redis:7-alpine
43
28
  container_name: nodebbs-redis
44
- restart: always
45
- command: >
46
- redis-server
47
- --requirepass ${REDIS_PASSWORD}
48
- --appendonly yes
49
- --appendfsync everysec
50
- --maxmemory 256mb
51
- --maxmemory-policy allkeys-lru
29
+ restart: unless-stopped
30
+ command: redis-server --requirepass ${REDIS_PASSWORD:-redis_password} --appendonly yes
31
+ environment:
32
+ TZ: Asia/Shanghai
52
33
  volumes:
53
34
  - redis_data:/data
54
- # 生产环境默认不暴露 Redis 端口到主机
55
- # ports:
56
- # - "${REDIS_PORT:-6379}:6379"
35
+ ports:
36
+ - "${REDIS_PORT:-6379}:6379"
57
37
  healthcheck:
58
38
  test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
59
- interval: 30s
60
- timeout: 10s
39
+ interval: 10s
40
+ timeout: 5s
61
41
  retries: 5
62
- start_period: 20s
63
- deploy:
64
- resources:
65
- limits:
66
- cpus: '0.5'
67
- memory: 256M
68
- reservations:
69
- cpus: '0.1'
70
- memory: 128M
71
- logging:
72
- driver: "json-file"
73
- options:
74
- max-size: "10m"
75
- max-file: "3"
76
42
  networks:
77
43
  - nodebbs-network
78
44
 
79
45
  # API 服务
80
46
  api:
81
- image: ${API_IMAGE:-ghcr.io/aiprojecthub/nodebbs-api:latest}
47
+ build:
48
+ context: ../../
49
+ dockerfile: apps/api/Dockerfile
50
+ image: ${API_IMAGE:-nodebbs-api:local}
82
51
  container_name: nodebbs-api
83
- restart: always
52
+ restart: unless-stopped
84
53
  environment:
85
54
  NODE_ENV: production
86
55
  APP_NAME: ${APP_NAME:-nodebbs}
87
56
  HOST: 0.0.0.0
88
57
  PORT: 7100
89
- DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-nodebbs}
90
- REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379/0
91
- USER_CACHE_TTL: ${USER_CACHE_TTL:-300}
92
- JWT_SECRET: ${JWT_SECRET}
58
+ DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-postgres_password}@postgres:5432/${POSTGRES_DB:-nodebbs}
59
+ REDIS_URL: redis://default:${REDIS_PASSWORD:-redis_password}@redis:6379/0
60
+ USER_CACHE_TTL: ${USER_CACHE_TTL:-120}
61
+ JWT_SECRET: ${JWT_SECRET:-change-this-to-a-secure-random-string-in-production}
93
62
  JWT_ACCESS_TOKEN_EXPIRES_IN: ${JWT_ACCESS_TOKEN_EXPIRES_IN:-1y}
94
63
  CORS_ORIGIN: ${CORS_ORIGIN:-*}
95
64
  APP_URL: ${APP_URL:-http://localhost:3100}
96
- TZ: ${TZ:-Asia/Shanghai}
97
- NODE_OPTIONS: "--max-old-space-size=512"
65
+ TZ: Asia/Shanghai
98
66
  volumes:
99
67
  - api_uploads:/app/apps/api/uploads
68
+ # - ./apps/api/src:/app/apps/api/src:ro
100
69
  ports:
101
70
  - "${API_PORT:-7100}:7100"
102
71
  depends_on:
@@ -110,35 +79,23 @@ services:
110
79
  timeout: 10s
111
80
  retries: 3
112
81
  start_period: 40s
113
- deploy:
114
- resources:
115
- limits:
116
- cpus: '1'
117
- memory: 768M
118
- reservations:
119
- cpus: '0.3'
120
- memory: 384M
121
- logging:
122
- driver: "json-file"
123
- options:
124
- max-size: "20m"
125
- max-file: "5"
126
82
  networks:
127
83
  - nodebbs-network
128
84
 
129
85
  # Web 前端服务
130
86
  web:
131
- image: ${WEB_IMAGE:-ghcr.io/aiprojecthub/nodebbs-web:latest}
87
+ build:
88
+ context: ../../
89
+ dockerfile: apps/web/Dockerfile
90
+ image: ${WEB_IMAGE:-nodebbs-web:local}
132
91
  container_name: nodebbs-web
133
- restart: always
92
+ restart: unless-stopped
134
93
  environment:
135
94
  NODE_ENV: production
136
95
  APP_NAME: ${APP_NAME:-nodebbs}
137
96
  PORT: 3100
138
- NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:7100}
139
- NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3100}
140
- TZ: ${TZ:-Asia/Shanghai}
141
- NODE_OPTIONS: "--max-old-space-size=512"
97
+ SERVER_API_URL: ${SERVER_API_URL:-http://api:7100}
98
+ TZ: Asia/Shanghai
142
99
  ports:
143
100
  - "${WEB_PORT:-3100}:3100"
144
101
  depends_on:
@@ -150,19 +107,6 @@ services:
150
107
  timeout: 10s
151
108
  retries: 3
152
109
  start_period: 40s
153
- deploy:
154
- resources:
155
- limits:
156
- cpus: '1'
157
- memory: 768M
158
- reservations:
159
- cpus: '0.3'
160
- memory: 384M
161
- logging:
162
- driver: "json-file"
163
- options:
164
- max-size: "20m"
165
- max-file: "5"
166
110
  networks:
167
111
  - nodebbs-network
168
112
 
@@ -179,6 +123,3 @@ volumes:
179
123
  networks:
180
124
  nodebbs-network:
181
125
  driver: bridge
182
- ipam:
183
- config:
184
- - subnet: 172.28.0.0/16
@@ -0,0 +1,120 @@
1
+ # ============================================
2
+ # .dockerignore - Turborepo Monorepo
3
+ # ============================================
4
+ # 作用:优化 Docker 构建上下文
5
+ # 原则:只把源码和必要文件打包进镜像
6
+ # 构建缓存、依赖、日志、运行时数据等全部排除
7
+ # ============================================
8
+
9
+ # ========================
10
+ # Node / pnpm 依赖
11
+ # ========================
12
+ # node_modules 会在 Docker 内重新安装
13
+ node_modules
14
+ # pnpm 全局存储目录
15
+ .pnpm-store
16
+
17
+ # ========================
18
+ # Turborepo 缓存
19
+ # ========================
20
+ .turbo
21
+
22
+ # ========================
23
+ # 构建输出
24
+ # ========================
25
+ .next # Next.js 构建目录
26
+ dist # Fastify / TS 构建目录
27
+ build
28
+ out # Next.js 静态导出目录
29
+ coverage # 测试覆盖率报告
30
+
31
+ # ========================
32
+ # 日志文件
33
+ # ========================
34
+ *.log
35
+ npm-debug.log*
36
+ yarn-debug.log*
37
+ yarn-error.log*
38
+ pnpm-debug.log*
39
+ lerna-debug.log*
40
+
41
+ # ========================
42
+ # 环境变量文件(运行时通过 docker-compose 注入)
43
+ # ========================
44
+ .env* # 排除所有本地环境文件
45
+ !.env.example # 保留示例文件
46
+
47
+ # ========================
48
+ # 测试相关文件
49
+ # ========================
50
+ __tests__ # 单元测试目录
51
+ *.test.* # 测试文件
52
+ *.spec.* # 测试文件
53
+ .nyc_output # 测试覆盖率缓存
54
+
55
+ # ========================
56
+ # 版本控制相关
57
+ # ========================
58
+ .git
59
+ .gitignore
60
+ .gitattributes
61
+
62
+ # ========================
63
+ # 编辑器 / 操作系统临时文件
64
+ # ========================
65
+ .vscode
66
+ .idea
67
+ .DS_Store
68
+ Thumbs.db
69
+ desktop.ini
70
+ *.swp
71
+ *.swo
72
+ *.swn
73
+ .history
74
+
75
+ # ========================
76
+ # CI/CD 和文档
77
+ # ========================
78
+ .github
79
+ .gitlab-ci.yml
80
+ .travis.yml
81
+ .circleci
82
+ docs
83
+ *.md
84
+ !README.md # 保留 README
85
+
86
+ # ========================
87
+ # 缓存 / 临时文件
88
+ # ========================
89
+ .cache
90
+ tmp
91
+ temp
92
+ .temp
93
+ *.tsbuildinfo # TypeScript 构建缓存
94
+
95
+ # ========================
96
+ # 运行时数据(使用 Docker 卷挂载)
97
+ # ========================
98
+ apps/api/uploads/ # 用户上传文件
99
+
100
+ # ========================
101
+ # 本地数据库文件(开发用)
102
+ # ========================
103
+ *.db
104
+ *.sqlite
105
+
106
+ # ========================
107
+ # 数据库 dump(非 migration)
108
+ # ========================
109
+ dump/
110
+ backups/
111
+
112
+ # ========================
113
+ # 进程管理器
114
+ # ========================
115
+ .pm2
116
+
117
+ # ========================
118
+ # docker-compose 文件(不打包进镜像)
119
+ # ========================
120
+ docker-compose*.yml
@@ -1,27 +1,13 @@
1
1
  # ========================================
2
- # NodeBBS 部署环境变量配置示例
2
+ # NodeBBS Docker Compose 环境变量配置
3
3
  # ========================================
4
4
 
5
- # ========================================
6
- # 基础配置
7
- # ========================================
8
- # 应用名称(用于 Docker 容器前缀)
5
+ # 应用名称
9
6
  APP_NAME=nodebbs
10
7
 
11
- # 部署环境 (prod, dev)
12
- NODE_ENV=production
13
-
14
- # 镜像配置
15
- # 默认为 GitHub Container Registry
16
- # 注意:使用 `nodebbs start` 时,可以选择版本 tag 覆盖此处的默认 latest tag
17
- # 如果需要固定特定版本 (如 v1.0),请修改以下配置或在部署时输入对应的 tag
18
- API_IMAGE=ghcr.io/aiprojecthub/nodebbs-api:latest
19
- WEB_IMAGE=ghcr.io/aiprojecthub/nodebbs-web:latest
20
-
21
8
  # ========================================
22
9
  # 数据库配置
23
10
  # ========================================
24
- # 务必修改为强密码!
25
11
  POSTGRES_PASSWORD=your_secure_postgres_password_here
26
12
  POSTGRES_DB=nodebbs
27
13
  POSTGRES_PORT=5432
@@ -29,7 +15,6 @@ POSTGRES_PORT=5432
29
15
  # ========================================
30
16
  # Redis 配置
31
17
  # ========================================
32
- # 务必修改为强密码!
33
18
  REDIS_PASSWORD=your_secure_redis_password_here
34
19
  REDIS_PORT=6379
35
20
 
@@ -39,8 +24,8 @@ REDIS_PORT=6379
39
24
  API_PORT=7100
40
25
 
41
26
  # 用户缓存 TTL(秒)
42
- # 生产环境建议: 120-300
43
- USER_CACHE_TTL=300
27
+ # 开发环境: 30-60, 生产环境: 120-300
28
+ USER_CACHE_TTL=120
44
29
 
45
30
  # JWT 配置
46
31
  # 使用 `openssl rand -base64 32` 生成安全的密钥
@@ -52,7 +37,6 @@ JWT_ACCESS_TOKEN_EXPIRES_IN=1y
52
37
  CORS_ORIGIN=*
53
38
 
54
39
  # 应用 URL(OAuth 回调使用)
55
- # 生产环境请修改为公网域名
56
40
  APP_URL=http://localhost:3100
57
41
 
58
42
  # ========================================
@@ -60,17 +44,14 @@ APP_URL=http://localhost:3100
60
44
  # ========================================
61
45
  WEB_PORT=3100
62
46
 
63
- # API 地址(公网访问地址)
64
- # 生产环境请修改为公网域名,例如: https://api.yourdomain.com
65
- # 或者是 Nginx 反向代理后的地址,例如: https://yourdomain.com/api
66
- NEXT_PUBLIC_API_URL=http://localhost:7100
67
-
68
- # 应用 URL(公网访问地址)
69
- # 生产环境请修改为公网域名,例如: https://yourdomain.com
70
- NEXT_PUBLIC_APP_URL=http://localhost:3100
47
+ # Web 服务内部访问 API 的地址
48
+ # 通常保持默认 http://api:7100 即可,走 Docker 内部网络
49
+ SERVER_API_URL=http://api:7100
71
50
 
72
51
  # ========================================
73
- # 其他配置
52
+ # 时区配置
74
53
  # ========================================
75
- # 时区
76
54
  TZ=Asia/Shanghai
55
+
56
+ API_IMAGE=ghcr.io/aiprojecthub/nodebbs-api:latest
57
+ WEB_IMAGE=ghcr.io/aiprojecthub/nodebbs-web:latest
@@ -5,7 +5,7 @@
5
5
  * - 如果存在,则优先使用项目根目录的配置。
6
6
  * - 如果不存在,则使用 CLI 内置的模板文件。
7
7
  *
8
- * @param env - 运行环境 ('production' | 'lowmem')
8
+ * @param env - 运行环境 ('production' | 'lowmem' | 'basic')
9
9
  * @returns 对象包含文件路径数组和是否使用内置模板的标志
10
10
  */
11
11
  export declare function getComposeFiles(env: string): Promise<{
@@ -4,7 +4,17 @@ import path from 'node:path';
4
4
  import { exists } from 'node:fs';
5
5
  import { promisify } from 'node:util';
6
6
  import { getTemplateDir, getTemplatePath } from './template.js';
7
+ import { getDeploymentMode } from './selection.js';
7
8
  const fileExists = promisify(exists);
9
+ async function getDockerEnv() {
10
+ const mode = await getDeploymentMode();
11
+ const envs = { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' };
12
+ if (mode === 'source') {
13
+ envs['API_IMAGE'] = 'nodebbs-api:local';
14
+ envs['WEB_IMAGE'] = 'nodebbs-web:local';
15
+ }
16
+ return envs;
17
+ }
8
18
  /**
9
19
  * 获取 Docker Compose 文件路径配置
10
20
  *
@@ -12,27 +22,52 @@ const fileExists = promisify(exists);
12
22
  * - 如果存在,则优先使用项目根目录的配置。
13
23
  * - 如果不存在,则使用 CLI 内置的模板文件。
14
24
  *
15
- * @param env - 运行环境 ('production' | 'lowmem')
25
+ * @param env - 运行环境 ('production' | 'lowmem' | 'basic')
16
26
  * @returns 对象包含文件路径数组和是否使用内置模板的标志
17
27
  */
18
28
  export async function getComposeFiles(env) {
19
29
  const workDir = process.cwd();
20
30
  let isBuiltIn = false;
31
+ // 1. 确定 Base 文件
21
32
  // 检查当前目录是否存在 docker-compose.yml
22
33
  let baseFile = path.join(workDir, 'docker-compose.yml');
23
- let templateDir = workDir;
34
+ const templateDir = getTemplateDir();
24
35
  if (!await fileExists(baseFile)) {
25
36
  // 使用内置模板
26
37
  isBuiltIn = true;
27
- templateDir = getTemplateDir();
28
38
  baseFile = getTemplatePath('docker-compose.yml');
29
39
  }
30
40
  const files = [baseFile];
31
- if (env === 'production') {
32
- // Production now uses the default file (merged config)
41
+ // 2. 确定环境 Override 文件 (production / lowmem)
42
+ if (env === 'production' || env === 'lowmem') {
43
+ const fileName = `docker-compose.${env}.yml`;
44
+ const localOverride = path.join(workDir, fileName);
45
+ const templateOverride = path.join(templateDir, fileName);
46
+ if (await fileExists(localOverride)) {
47
+ files.push(localOverride);
48
+ }
49
+ else if (await fileExists(templateOverride)) {
50
+ // 即使 Base 是本地的,如果本地没有 override,我们也尝试用内置的 override 补全吗?
51
+ // 通常为了混用方便,是可以的。但如果 Base 是本地,通常期望全套本地。
52
+ // 策略:如果 Base 是内置,则一定用内置 Override。如果 Base 是本地,仅当本地也有 Override 时才用?
53
+ // 不,为了方便用户只覆盖 Base 而复用我们的资源配置,如果本地没 Override,可以用内置的。
54
+ files.push(templateOverride);
55
+ }
33
56
  }
34
- else if (env === 'lowmem') {
35
- files.push(path.join(templateDir, 'docker-compose.lowmem.yml'));
57
+ // 3. 确定构建配置 (仅源码模式)
58
+ const mode = await getDeploymentMode();
59
+ if (mode === 'source') {
60
+ const buildFileName = 'docker-compose.build.yml';
61
+ const localBuild = path.join(workDir, buildFileName);
62
+ const templateBuild = path.join(templateDir, buildFileName);
63
+ if (await fileExists(localBuild)) {
64
+ files.push(localBuild);
65
+ }
66
+ else if (isBuiltIn && await fileExists(templateBuild)) {
67
+ // 只有在使用内置 Base 时,才默认加载内置 Build。
68
+ // 如果用户自定义了 Base,通常他们会自己把 build 写进去,或者自己提供 build.yml
69
+ files.push(templateBuild);
70
+ }
36
71
  }
37
72
  return { files, isBuiltIn };
38
73
  }
@@ -74,7 +109,7 @@ export async function runCompose(files, args, isBuiltIn = false) {
74
109
  // 设置 COMPOSE_LOG_LEVEL=ERROR 抑制变量未设置的警告
75
110
  await execa('docker', ['compose', ...composeArgs], {
76
111
  stdio: 'inherit',
77
- env: { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' }
112
+ env: await getDockerEnv()
78
113
  });
79
114
  }
80
115
  /**
@@ -96,7 +131,7 @@ export async function execCompose(files, service, command, isBuiltIn = false) {
96
131
  composeArgs.push('exec', service, ...command);
97
132
  await execa('docker', ['compose', ...composeArgs], {
98
133
  stdio: 'inherit',
99
- env: { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' }
134
+ env: await getDockerEnv()
100
135
  });
101
136
  }
102
137
  /**
@@ -118,10 +153,11 @@ export async function waitForHealth(files, isBuiltIn = false) {
118
153
  composeArgs.push('--project-directory', process.cwd());
119
154
  }
120
155
  const pgArgs = [...composeArgs, 'exec', '-T', 'postgres', 'pg_isready', '-U', 'postgres'];
156
+ const envs = await getDockerEnv();
121
157
  let retries = 15;
122
158
  while (retries > 0) {
123
159
  try {
124
- await execa('docker', ['compose', ...pgArgs]);
160
+ await execa('docker', ['compose', ...pgArgs], { env: envs });
125
161
  logger.success('PostgreSQL 已就绪');
126
162
  break;
127
163
  }
@@ -1,2 +1,7 @@
1
+ export declare function getEnvValue(key: string): Promise<string | undefined>;
2
+ export declare function setEnvValue(key: string, value: string): Promise<void>;
3
+ export declare function unsetEnvValue(key: string): Promise<void>;
1
4
  export declare function initEnv(): Promise<void>;
2
- export declare function checkEnv(envType: 'production' | 'lowmem'): Promise<void>;
5
+ export declare function initDockerIgnore(): Promise<void>;
6
+ export declare function initImageEnv(): Promise<void>;
7
+ export declare function checkEnv(envType: 'production' | 'lowmem' | 'basic'): Promise<void>;
package/dist/utils/env.js CHANGED
@@ -6,12 +6,52 @@ import { confirm } from '@inquirer/prompts';
6
6
  import dotenv from 'dotenv';
7
7
  import { getTemplatePath } from './template.js';
8
8
  const fileExists = promisify(exists);
9
- export async function initEnv() {
9
+ export async function getEnvValue(key) {
10
+ if (!await fileExists('.env'))
11
+ return undefined;
12
+ try {
13
+ const content = await fs.readFile('.env', 'utf-8');
14
+ const config = dotenv.parse(content);
15
+ return config[key];
16
+ }
17
+ catch (e) {
18
+ return undefined;
19
+ }
20
+ }
21
+ export async function setEnvValue(key, value) {
22
+ let content = '';
10
23
  if (await fileExists('.env')) {
11
- logger.info('.env 文件已存在,跳过创建');
24
+ content = await fs.readFile('.env', 'utf-8');
25
+ }
26
+ const regex = new RegExp(`^${key}=.*`, 'm');
27
+ if (regex.test(content)) {
28
+ content = content.replace(regex, `${key}=${value}`);
29
+ }
30
+ else {
31
+ if (content && !content.endsWith('\n'))
32
+ content += '\n';
33
+ content += `${key}=${value}\n`;
34
+ }
35
+ await fs.writeFile('.env', content, 'utf-8');
36
+ }
37
+ export async function unsetEnvValue(key) {
38
+ if (!await fileExists('.env'))
12
39
  return;
40
+ let content = await fs.readFile('.env', 'utf-8');
41
+ const regex = new RegExp(`^${key}=.*\n?`, 'm');
42
+ if (regex.test(content)) {
43
+ content = content.replace(regex, '');
44
+ await fs.writeFile('.env', content, 'utf-8');
13
45
  }
14
- logger.info('正在创建 .env 文件...');
46
+ }
47
+ export async function initEnv() {
48
+ const currentEnv = await fileExists('.env') ? dotenv.parse(await fs.readFile('.env', 'utf-8')) : {};
49
+ // 如果已存在关键配置(如 POSTGRES_PASSWORD),则视为无需初始化
50
+ if (currentEnv.POSTGRES_PASSWORD) {
51
+ logger.info('.env 文件已配置,跳过创建');
52
+ return;
53
+ }
54
+ logger.info('正在创建/更新 .env 文件...');
15
55
  // 检查模板文件
16
56
  let templateContent = '';
17
57
  let sourceFile = '';
@@ -54,14 +94,10 @@ export async function initEnv() {
54
94
  message: '设置 API_PORT (后端端口):',
55
95
  default: '7100'
56
96
  });
57
- const defaultApiUrl = `http://localhost:${apiPort}`;
58
- const nextPublicApiUrl = await input({
59
- message: '设置 NEXT_PUBLIC_API_URL (前端访问后端的地址):',
60
- default: defaultApiUrl
61
- });
97
+ const serverApiUrl = `http://api:${apiPort}`;
62
98
  const defaultAppUrl = `http://localhost:${webPort}`;
63
- const nextPublicAppUrl = await input({
64
- message: '设置 NEXT_PUBLIC_APP_URL (应用访问地址):',
99
+ const appUrl = await input({
100
+ message: '设置 APP_URL (应用访问地址):',
65
101
  default: defaultAppUrl
66
102
  });
67
103
  const corsOrigin = await input({
@@ -75,8 +111,8 @@ export async function initEnv() {
75
111
  console.log(`JWT_SECRET: ${jwtSecret.substring(0, 3)}******`);
76
112
  console.log(`WEB_PORT: ${webPort}`);
77
113
  console.log(`API_PORT: ${apiPort}`);
78
- console.log(`NEXT_PUBLIC_API_URL: ${nextPublicApiUrl}`);
79
- console.log(`NEXT_PUBLIC_APP_URL: ${nextPublicAppUrl}`);
114
+ console.log(`APP_URL: ${appUrl}`);
115
+ console.log(`SERVER_API_URL: ${serverApiUrl}`);
80
116
  console.log(`CORS_ORIGIN: ${corsOrigin}`);
81
117
  console.log('==========================================\n');
82
118
  // 3. 确认生成
@@ -98,11 +134,9 @@ export async function initEnv() {
98
134
  'JWT_SECRET': jwtSecret,
99
135
  'WEB_PORT': webPort,
100
136
  'API_PORT': apiPort,
101
- 'NEXT_PUBLIC_API_URL': nextPublicApiUrl,
102
- 'NEXT_PUBLIC_APP_URL': nextPublicAppUrl,
137
+ 'SERVER_API_URL': serverApiUrl,
138
+ 'APP_URL': appUrl,
103
139
  'CORS_ORIGIN': corsOrigin,
104
- // 如果 API_PORT 变了,模板里可能还需要联动修改 APP_URL 用于 OAuth 回调,这里简单处理
105
- 'APP_URL': nextPublicAppUrl
106
140
  };
107
141
  // 针对每个 Key 进行替换。
108
142
  // 策略:查找 `KEY=...` 并替换为 `KEY=newValue`
@@ -118,6 +152,11 @@ export async function initEnv() {
118
152
  }
119
153
  }
120
154
  // 5. 写入文件
155
+ // 如果原文件中有 DEPLOY_ENV 等其他配置,保留它们
156
+ // 简单策略:如果 currentEnv 有 DEPLOY_ENV,确保 newEnv 里也有
157
+ if (currentEnv.DEPLOY_ENV && !newEnv.includes('DEPLOY_ENV=')) {
158
+ newEnv += `\nDEPLOY_ENV=${currentEnv.DEPLOY_ENV}`;
159
+ }
121
160
  await fs.writeFile('.env', newEnv, 'utf-8');
122
161
  // isBuiltIn is not defined in the new logic, need to determine it
123
162
  // based on whether sourceFile was .env.docker.example or a template path
@@ -129,6 +168,41 @@ export async function initEnv() {
129
168
  logger.success(`已从 ${sourceFile} 复制并填充配置生成 .env`);
130
169
  }
131
170
  }
171
+ export async function initDockerIgnore() {
172
+ if (await fileExists('.dockerignore')) {
173
+ return;
174
+ }
175
+ logger.info('正在创建 .dockerignore 文件...');
176
+ const templatePath = getTemplatePath('dockerignore');
177
+ const content = await fs.readFile(templatePath, 'utf-8');
178
+ await fs.writeFile('.dockerignore', content, 'utf-8');
179
+ logger.success('已创建 .dockerignore');
180
+ }
181
+ export async function initImageEnv() {
182
+ const { getDeploymentMode } = await import('./selection.js');
183
+ const mode = await getDeploymentMode();
184
+ logger.info(`当前部署模式: ${mode === 'image' ? '镜像部署' : '源码部署'}`);
185
+ if (mode !== 'image')
186
+ return;
187
+ logger.info('检测到镜像部署模式,请配置镜像地址...');
188
+ // initEnv 已经执行,尝试从 .env 获取默认值
189
+ const currentApiImage = await getEnvValue('API_IMAGE') || 'ghcr.io/aiprojecthub/nodebbs-api:latest';
190
+ const currentWebImage = await getEnvValue('WEB_IMAGE') || 'ghcr.io/aiprojecthub/nodebbs-web:latest';
191
+ let defaultVersion = 'latest';
192
+ const match = currentApiImage.match(/:([^:]+)$/);
193
+ if (match)
194
+ defaultVersion = match[1];
195
+ const { input } = await import('@inquirer/prompts');
196
+ const version = await input({
197
+ message: '设置镜像版本 (默认为 latest):',
198
+ default: defaultVersion
199
+ });
200
+ // 仅替换版本号部分
201
+ const newApiImage = currentApiImage.replace(/:[^:]+$/, `:${version}`);
202
+ const newWebImage = currentWebImage.replace(/:[^:]+$/, `:${version}`);
203
+ await setEnvValue('API_IMAGE', newApiImage);
204
+ await setEnvValue('WEB_IMAGE', newWebImage);
205
+ }
132
206
  export async function checkEnv(envType) {
133
207
  logger.info('正在检查环境配置...');
134
208
  // 加载 .env
@@ -1,6 +1,8 @@
1
- export type EnvType = 'production' | 'lowmem';
1
+ export type EnvType = 'production' | 'lowmem' | 'basic';
2
+ export type DeploymentMode = 'source' | 'image';
2
3
  export declare const EnvFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
3
4
  export declare function setStoredEnv(env: EnvType): Promise<void>;
5
+ export declare function getDeploymentMode(): Promise<DeploymentMode>;
4
6
  export declare function clearStoredEnv(): Promise<void>;
5
7
  export declare function selectEnvironment(env?: string, options?: {
6
8
  prompt?: string;