@h-ai/cli 0.1.0-alpha.50 → 0.1.0-alpha.51
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.
- package/dist/index.js +52 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/apps/api/.dockerignore.hbs +14 -0
- package/templates/apps/api/Dockerfile.hbs +34 -0
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +6 -0
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +2 -2
- package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +4 -4
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +2 -0
- package/templates/apps/api/package.json.hbs +4 -0
- package/templates/apps/fullstack/.dockerignore.hbs +14 -0
- package/templates/apps/fullstack/Dockerfile.hbs +71 -0
- package/templates/apps/fullstack/docker/fullstack-server.mjs.hbs +194 -0
- package/templates/apps/fullstack/docker/nginx.conf.hbs +18 -0
- package/templates/apps/fullstack/package.json.hbs +8 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +2 -1
- package/templates/base/.dockerignore.hbs +17 -0
- package/templates/base/Dockerfile.hbs +56 -0
- package/templates/base/docker/nginx.conf.hbs +18 -0
- package/templates/base/package.json.hbs +16 -6
- package/templates/base/svelte.config.js.hbs +0 -8
- package/templates/skills/hai-app-create/SKILL.md +13 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.51",
|
|
5
5
|
"description": "Hai Framework CLI for project scaffolding and code generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"keywords": [
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"ora": "^9.1.0",
|
|
35
35
|
"prompts": "^2.4.2",
|
|
36
36
|
"yaml": "^2.7.0",
|
|
37
|
-
"@h-ai/core": "0.1.0-alpha.
|
|
37
|
+
"@h-ai/core": "0.1.0-alpha.51"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1.7
|
|
2
|
+
FROM node:22-bookworm-slim AS build
|
|
3
|
+
|
|
4
|
+
ENV PNPM_HOME=/pnpm
|
|
5
|
+
ENV PATH=$PNPM_HOME:$PATH
|
|
6
|
+
WORKDIR /workspace
|
|
7
|
+
|
|
8
|
+
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
9
|
+
COPY . .
|
|
10
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|
11
|
+
RUN pnpm build
|
|
12
|
+
RUN pnpm --filter {{api.servicePackageName}} --prod deploy /prod/app
|
|
13
|
+
|
|
14
|
+
FROM node:22-bookworm-slim AS runtime-base
|
|
15
|
+
|
|
16
|
+
ENV NODE_ENV=production
|
|
17
|
+
ENV HOST=0.0.0.0
|
|
18
|
+
ENV PORT=3000
|
|
19
|
+
WORKDIR /app
|
|
20
|
+
|
|
21
|
+
COPY --from=build --chown=node:node /prod/app ./
|
|
22
|
+
RUN mkdir -p /app/data && chown node:node /app/data
|
|
23
|
+
|
|
24
|
+
USER node
|
|
25
|
+
EXPOSE 3000
|
|
26
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
27
|
+
CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
28
|
+
CMD ["node", "dist/index.js"]
|
|
29
|
+
|
|
30
|
+
FROM runtime-base AS runtime-with-data
|
|
31
|
+
|
|
32
|
+
COPY --from=embedded-data --chown=node:node . /app/data
|
|
33
|
+
|
|
34
|
+
FROM runtime-base AS runtime
|
|
@@ -9,6 +9,12 @@ pnpm --filter {{api.servicePackageName}} dev
|
|
|
9
9
|
pnpm --filter {{api.servicePackageName}} test
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## 容器运行
|
|
13
|
+
|
|
14
|
+
- 根目录运行 `pnpm docker:build` / `pnpm podman:build`;镜像监听 `0.0.0.0:3000`。
|
|
15
|
+
- 配置默认打包自本目录 `config/`,可在运行时只读挂载到 `/app/config`,敏感值通过环境变量注入。
|
|
16
|
+
- `data/` 可作为 SQLite/本地存储目录;`pnpm docker:build:data` / `pnpm podman:build:data` 会将其固化为镜像初始数据,生产写入应挂载 `/app/data` volume。
|
|
17
|
+
|
|
12
18
|
## 关键目录
|
|
13
19
|
|
|
14
20
|
- `config/`:service 配置
|
|
@@ -18,10 +18,10 @@ http:
|
|
|
18
18
|
rpc: false
|
|
19
19
|
|
|
20
20
|
cors:
|
|
21
|
-
origin:
|
|
22
|
-
nativeOrigins: http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost
|
|
23
|
-
{{#if features.iam}} credentials: true
|
|
24
|
-
{{else}} credentials: false
|
|
21
|
+
origin: ${HAI_CORS_ORIGIN:*}
|
|
22
|
+
nativeOrigins: ${HAI_CORS_NATIVEORIGINS:http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
|
|
23
|
+
{{#if features.iam}} credentials: ${HAI_CORS_CREDENTIALS:true}
|
|
24
|
+
{{else}} credentials: ${HAI_CORS_CREDENTIALS:false}
|
|
25
25
|
{{/if}} allowedHeaders:
|
|
26
26
|
- Authorization
|
|
27
27
|
- Content-Type
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"dev": "pnpm --filter {{api.servicePackageName}} dev",
|
|
10
10
|
"compile": "pnpm typecheck && pnpm build",
|
|
11
11
|
"build": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present build",
|
|
12
|
+
"docker:build": "docker build --target runtime -t {{projectName}}:latest .",
|
|
13
|
+
"docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./apps/{{projectName}}-service/data -t {{projectName}}:latest .",
|
|
14
|
+
"podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest .",
|
|
15
|
+
"podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./apps/{{projectName}}-service/data -t {{projectName}}:latest .",
|
|
12
16
|
"typecheck": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present typecheck",
|
|
13
17
|
"lint": "pnpm -r --if-present lint",
|
|
14
18
|
"test": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present test",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1.7
|
|
2
|
+
FROM node:22-bookworm-slim AS build
|
|
3
|
+
|
|
4
|
+
ARG PUBLIC_API_BASE
|
|
5
|
+
ENV PNPM_HOME=/pnpm
|
|
6
|
+
ENV PATH=$PNPM_HOME:$PATH
|
|
7
|
+
ENV PUBLIC_API_BASE=$PUBLIC_API_BASE
|
|
8
|
+
WORKDIR /workspace
|
|
9
|
+
|
|
10
|
+
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
11
|
+
COPY . .
|
|
12
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|
13
|
+
RUN pnpm build
|
|
14
|
+
RUN pnpm --filter {{fullstack.servPackageName}} --prod deploy /prod/service
|
|
15
|
+
|
|
16
|
+
FROM node:22-bookworm-slim AS service
|
|
17
|
+
|
|
18
|
+
ENV NODE_ENV=production
|
|
19
|
+
ENV HOST=0.0.0.0
|
|
20
|
+
ENV PORT=3000
|
|
21
|
+
WORKDIR /app
|
|
22
|
+
|
|
23
|
+
COPY --from=build --chown=node:node /prod/service ./
|
|
24
|
+
RUN mkdir -p /app/data && chown node:node /app/data
|
|
25
|
+
|
|
26
|
+
USER node
|
|
27
|
+
EXPOSE 3000
|
|
28
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
29
|
+
CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
30
|
+
CMD ["node", "dist/index.js"]
|
|
31
|
+
|
|
32
|
+
{{#if fullstack.frontends.web}}
|
|
33
|
+
FROM nginxinc/nginx-unprivileged:1.29-alpine AS web
|
|
34
|
+
|
|
35
|
+
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
|
36
|
+
COPY --from=build /workspace/apps/{{projectName}}-web/dist /usr/share/nginx/html
|
|
37
|
+
|
|
38
|
+
EXPOSE 8080
|
|
39
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
40
|
+
CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
|
|
41
|
+
|
|
42
|
+
FROM node:22-bookworm-slim AS runtime-base
|
|
43
|
+
|
|
44
|
+
ENV NODE_ENV=production
|
|
45
|
+
ENV HAI_WEB_PORT=8080
|
|
46
|
+
ENV HAI_SERV_PORT=3000
|
|
47
|
+
WORKDIR /app
|
|
48
|
+
|
|
49
|
+
COPY --from=build --chown=node:node /prod/service ./service
|
|
50
|
+
COPY --from=build --chown=node:node /workspace/apps/{{projectName}}-web/dist ./web
|
|
51
|
+
COPY --chown=node:node docker/fullstack-server.mjs ./fullstack-server.mjs
|
|
52
|
+
RUN mkdir -p /app/service/data && chown node:node /app/service/data
|
|
53
|
+
|
|
54
|
+
USER node
|
|
55
|
+
EXPOSE 8080
|
|
56
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
57
|
+
CMD node -e "fetch('http://127.0.0.1:'+process.env.HAI_WEB_PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
58
|
+
CMD ["node", "fullstack-server.mjs"]
|
|
59
|
+
|
|
60
|
+
FROM runtime-base AS runtime-with-data
|
|
61
|
+
|
|
62
|
+
COPY --from=embedded-data --chown=node:node . /app/service/data
|
|
63
|
+
|
|
64
|
+
FROM runtime-base AS runtime
|
|
65
|
+
{{else}}
|
|
66
|
+
FROM service AS runtime-with-data
|
|
67
|
+
|
|
68
|
+
COPY --from=embedded-data --chown=node:node . /app/data
|
|
69
|
+
|
|
70
|
+
FROM service AS runtime
|
|
71
|
+
{{/if}}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
{{! @skipIfEmpty }}
|
|
2
|
+
{{#if fullstack.frontends.web}}
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { createReadStream } from 'node:fs'
|
|
5
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
6
|
+
import { createServer, request as createProxyRequest } from 'node:http'
|
|
7
|
+
import { extname, relative, resolve, sep } from 'node:path'
|
|
8
|
+
import process from 'node:process'
|
|
9
|
+
|
|
10
|
+
const serviceRoot = '/app/service'
|
|
11
|
+
const webRoot = '/app/web'
|
|
12
|
+
const publicPort = readPort('HAI_WEB_PORT', 8080)
|
|
13
|
+
const servicePort = readPort('HAI_SERV_PORT', 3000)
|
|
14
|
+
const indexHtml = await readFile(resolve(webRoot, 'index.html'), 'utf8')
|
|
15
|
+
const indexWithRuntimeApi = injectRuntimeApiOrigin(indexHtml)
|
|
16
|
+
let shuttingDown = false
|
|
17
|
+
|
|
18
|
+
// 服务进程只监听容器回环地址,所有外部流量统一经过同源 Web 入口。
|
|
19
|
+
const service = spawn(process.execPath, ['dist/index.js'], {
|
|
20
|
+
cwd: serviceRoot,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
HOST: '127.0.0.1',
|
|
24
|
+
PORT: String(servicePort),
|
|
25
|
+
},
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const server = createServer(async (request, response) => {
|
|
30
|
+
const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`)
|
|
31
|
+
|
|
32
|
+
if (isServicePath(requestUrl.pathname)) {
|
|
33
|
+
proxyToService(request, response)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
38
|
+
response.writeHead(405, { Allow: 'GET, HEAD' })
|
|
39
|
+
response.end()
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await serveWebAsset(requestUrl.pathname, request.method === 'HEAD', response)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
server.listen(publicPort, '0.0.0.0', () => {
|
|
47
|
+
process.stdout.write(`Fullstack container listening on 0.0.0.0:${publicPort}\n`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
service.once('error', (error) => {
|
|
51
|
+
process.stderr.write(`Service process failed to start: ${error.message}\n`)
|
|
52
|
+
void shutdown('service-error')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
service.once('exit', (code, signal) => {
|
|
56
|
+
if (shuttingDown)
|
|
57
|
+
return
|
|
58
|
+
process.stderr.write(`Service process exited unexpectedly (code=${String(code)}, signal=${String(signal)})\n`)
|
|
59
|
+
process.exitCode = code ?? 1
|
|
60
|
+
server.close()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
64
|
+
process.once(signal, () => {
|
|
65
|
+
void shutdown(signal)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readPort(name, fallback) {
|
|
70
|
+
const raw = process.env[name]
|
|
71
|
+
if (raw === undefined || raw === '')
|
|
72
|
+
return fallback
|
|
73
|
+
const value = Number(raw)
|
|
74
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535)
|
|
75
|
+
throw new Error(`${name} must be an integer between 1 and 65535`)
|
|
76
|
+
return value
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isServicePath(pathname) {
|
|
80
|
+
return pathname === '/health'
|
|
81
|
+
|| pathname === '/ready'
|
|
82
|
+
|| pathname === '/openapi.json'
|
|
83
|
+
|| pathname.startsWith('/api/')
|
|
84
|
+
|| pathname.startsWith('/docs')
|
|
85
|
+
|| pathname.startsWith('/_hai/')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function proxyToService(request, response) {
|
|
89
|
+
const proxyRequest = createProxyRequest({
|
|
90
|
+
host: '127.0.0.1',
|
|
91
|
+
port: servicePort,
|
|
92
|
+
method: request.method,
|
|
93
|
+
path: request.url,
|
|
94
|
+
headers: {
|
|
95
|
+
...request.headers,
|
|
96
|
+
host: `127.0.0.1:${servicePort}`,
|
|
97
|
+
'x-forwarded-host': request.headers.host ?? '',
|
|
98
|
+
'x-forwarded-proto': request.headers['x-forwarded-proto'] ?? 'http',
|
|
99
|
+
},
|
|
100
|
+
}, (proxyResponse) => {
|
|
101
|
+
response.writeHead(proxyResponse.statusCode ?? 502, proxyResponse.headers)
|
|
102
|
+
proxyResponse.pipe(response)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
proxyRequest.once('error', (error) => {
|
|
106
|
+
if (response.headersSent) {
|
|
107
|
+
response.destroy(error)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
response.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
|
|
111
|
+
response.end('Service unavailable')
|
|
112
|
+
})
|
|
113
|
+
request.pipe(proxyRequest)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function serveWebAsset(pathname, headOnly, response) {
|
|
117
|
+
let decodedPath
|
|
118
|
+
try {
|
|
119
|
+
decodedPath = decodeURIComponent(pathname)
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
response.writeHead(400)
|
|
123
|
+
response.end()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const candidate = resolve(webRoot, `.${decodedPath}`)
|
|
128
|
+
const relativePath = relative(webRoot, candidate)
|
|
129
|
+
if (relativePath.startsWith(`..${sep}`) || relativePath === '..') {
|
|
130
|
+
response.writeHead(403)
|
|
131
|
+
response.end()
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const file = await stat(candidate)
|
|
137
|
+
if (file.isFile()) {
|
|
138
|
+
response.writeHead(200, {
|
|
139
|
+
'content-length': file.size,
|
|
140
|
+
'content-type': contentType(candidate),
|
|
141
|
+
'x-content-type-options': 'nosniff',
|
|
142
|
+
})
|
|
143
|
+
if (headOnly)
|
|
144
|
+
response.end()
|
|
145
|
+
else
|
|
146
|
+
createReadStream(candidate).pipe(response)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// SPA 路由没有对应静态文件时回退到 index.html。
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const body = Buffer.from(indexWithRuntimeApi)
|
|
155
|
+
response.writeHead(200, {
|
|
156
|
+
'content-length': body.length,
|
|
157
|
+
'content-type': 'text/html; charset=utf-8',
|
|
158
|
+
'x-content-type-options': 'nosniff',
|
|
159
|
+
})
|
|
160
|
+
response.end(headOnly ? undefined : body)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function injectRuntimeApiOrigin(html) {
|
|
164
|
+
const script = `<script>(()=>{const p=new URLSearchParams(location.search);if(!p.has('apiBase')){p.set('apiBase',location.origin);history.replaceState(null,'',location.pathname+'?'+p+location.hash)}})()</script>`
|
|
165
|
+
return html.includes('<head>') ? html.replace('<head>', `<head>${script}`) : `${script}${html}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function contentType(file) {
|
|
169
|
+
return {
|
|
170
|
+
'.css': 'text/css; charset=utf-8',
|
|
171
|
+
'.html': 'text/html; charset=utf-8',
|
|
172
|
+
'.ico': 'image/x-icon',
|
|
173
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
174
|
+
'.json': 'application/json; charset=utf-8',
|
|
175
|
+
'.map': 'application/json; charset=utf-8',
|
|
176
|
+
'.png': 'image/png',
|
|
177
|
+
'.svg': 'image/svg+xml',
|
|
178
|
+
'.webp': 'image/webp',
|
|
179
|
+
'.woff': 'font/woff',
|
|
180
|
+
'.woff2': 'font/woff2',
|
|
181
|
+
}[extname(file).toLowerCase()] ?? 'application/octet-stream'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function shutdown(signal) {
|
|
185
|
+
if (shuttingDown)
|
|
186
|
+
return
|
|
187
|
+
shuttingDown = true
|
|
188
|
+
process.stdout.write(`Stopping fullstack container (${signal})\n`)
|
|
189
|
+
server.close()
|
|
190
|
+
service.kill('SIGTERM')
|
|
191
|
+
const forceExit = setTimeout(() => service.kill('SIGKILL'), 10_000)
|
|
192
|
+
forceExit.unref()
|
|
193
|
+
}
|
|
194
|
+
{{/if}}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{{! @skipIfEmpty }}
|
|
2
|
+
{{#if fullstack.frontends.web}}
|
|
3
|
+
server {
|
|
4
|
+
listen 8080;
|
|
5
|
+
server_name _;
|
|
6
|
+
root /usr/share/nginx/html;
|
|
7
|
+
|
|
8
|
+
location / {
|
|
9
|
+
try_files $uri $uri/ /index.html;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
location = /health {
|
|
13
|
+
access_log off;
|
|
14
|
+
add_header Content-Type text/plain;
|
|
15
|
+
return 200 'ok';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
{{/if}}
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
"dev": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev",
|
|
12
12
|
"compile": "pnpm typecheck && pnpm build",
|
|
13
13
|
"build": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present build",
|
|
14
|
+
"docker:build": "docker build --target runtime -t {{projectName}}:latest .",
|
|
15
|
+
"docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./packages/{{projectName}}-serv/data -t {{projectName}}:latest .",
|
|
16
|
+
"docker:build:service": "docker build --target service -t {{projectName}}-serv:latest ."{{#if fullstack.frontends.web}},
|
|
17
|
+
"docker:build:web": "docker build --target web -t {{projectName}}-web:latest ."{{/if}},
|
|
18
|
+
"podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest .",
|
|
19
|
+
"podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./packages/{{projectName}}-serv/data -t {{projectName}}:latest .",
|
|
20
|
+
"podman:build:service": "podman build --format docker --jobs=1 --target service -t {{projectName}}-serv:latest ."{{#if fullstack.frontends.web}},
|
|
21
|
+
"podman:build:web": "podman build --format docker --jobs=1 --target web -t {{projectName}}-web:latest ."{{/if}},
|
|
14
22
|
"package": "pnpm build{{#if fullstack.frontends.web}} && pnpm --filter {{projectName}}-web package{{/if}}{{#if fullstack.frontends.app}} && pnpm --filter {{projectName}}-app package{{/if}}{{#if fullstack.frontends.desktop}} && pnpm --filter {{projectName}}-desktop package{{/if}}",
|
|
15
23
|
"deploy": "pnpm package",
|
|
16
24
|
"typecheck": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present typecheck",
|
|
@@ -15,3 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
- `pnpm --filter {{fullstack.servPackageName}} dev`
|
|
17
17
|
- `pnpm --filter {{fullstack.servPackageName}} test`
|
|
18
|
+
|
|
19
|
+
## 容器运行
|
|
20
|
+
|
|
21
|
+
- 根目录 `pnpm docker:build` / `pnpm podman:build` 生成 Web + Service 同源镜像;`pnpm docker:build:service` / `pnpm podman:build:service` 只生成本服务镜像。
|
|
22
|
+
- 合并镜像的配置挂载点为 `/app/service/config`,数据目录为 `/app/service/data`;service-only 镜像对应 `/app/config` 与 `/app/data`。
|
|
23
|
+
- YAML 配置使用 `${ENV_NAME:default}` 接收运行时环境变量;不要把密钥写入镜像。
|
|
24
|
+
- 将 SQLite/本地存储放入本目录 `data/` 后运行 `pnpm docker:build:data` / `pnpm podman:build:data`,可生成带初始数据的镜像,持续写入仍应使用 volume。
|
|
@@ -21,9 +21,9 @@ http:
|
|
|
21
21
|
rpc: false
|
|
22
22
|
|
|
23
23
|
cors:
|
|
24
|
-
origin:
|
|
25
|
-
nativeOrigins: http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost
|
|
26
|
-
credentials: false
|
|
24
|
+
origin: ${HAI_CORS_ORIGIN:*}
|
|
25
|
+
nativeOrigins: ${HAI_CORS_NATIVEORIGINS:http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
|
|
26
|
+
credentials: ${HAI_CORS_CREDENTIALS:false}
|
|
27
27
|
allowedHeaders:
|
|
28
28
|
- Authorization
|
|
29
29
|
- Content-Type
|
package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs
CHANGED
|
@@ -38,7 +38,8 @@ export async function fetchAppInfo(options: FetchAppInfoOptions = {}): Promise<A
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function resolveDefaultApiOrigin(): string {
|
|
41
|
-
return import.meta.env.PUBLIC_API_BASE
|
|
41
|
+
return import.meta.env.PUBLIC_API_BASE
|
|
42
|
+
|| (typeof globalThis.location === 'undefined' ? DEFAULT_API_ORIGIN : globalThis.location.origin)
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function normalizeApiBaseUrl(value: string): string {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1.7
|
|
2
|
+
{{#if isSvelteOnlyApp}}
|
|
3
|
+
FROM node:22-bookworm-slim AS build
|
|
4
|
+
|
|
5
|
+
ENV PNPM_HOME=/pnpm
|
|
6
|
+
ENV PATH=$PNPM_HOME:$PATH
|
|
7
|
+
WORKDIR /workspace
|
|
8
|
+
|
|
9
|
+
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
10
|
+
COPY . .
|
|
11
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|
12
|
+
RUN pnpm build
|
|
13
|
+
|
|
14
|
+
FROM nginxinc/nginx-unprivileged:1.29-alpine AS runtime
|
|
15
|
+
|
|
16
|
+
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
|
17
|
+
COPY --from=build /workspace/dist /usr/share/nginx/html
|
|
18
|
+
|
|
19
|
+
EXPOSE 8080
|
|
20
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
21
|
+
CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
|
|
22
|
+
{{else}}
|
|
23
|
+
FROM node:22-bookworm-slim AS build
|
|
24
|
+
|
|
25
|
+
ENV PNPM_HOME=/pnpm
|
|
26
|
+
ENV PATH=$PNPM_HOME:$PATH
|
|
27
|
+
WORKDIR /workspace
|
|
28
|
+
|
|
29
|
+
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
30
|
+
COPY . .
|
|
31
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|
32
|
+
RUN pnpm build
|
|
33
|
+
RUN pnpm --filter {{projectName}} --prod deploy /prod/app
|
|
34
|
+
|
|
35
|
+
FROM node:22-bookworm-slim AS runtime-base
|
|
36
|
+
|
|
37
|
+
ENV NODE_ENV=production
|
|
38
|
+
ENV HOST=0.0.0.0
|
|
39
|
+
ENV PORT=3000
|
|
40
|
+
WORKDIR /app
|
|
41
|
+
|
|
42
|
+
COPY --from=build --chown=node:node /prod/app ./
|
|
43
|
+
RUN mkdir -p /app/data && chown node:node /app/data
|
|
44
|
+
|
|
45
|
+
USER node
|
|
46
|
+
EXPOSE 3000
|
|
47
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
48
|
+
CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
49
|
+
CMD ["node", "build"]
|
|
50
|
+
|
|
51
|
+
FROM runtime-base AS runtime-with-data
|
|
52
|
+
|
|
53
|
+
COPY --from=embedded-data --chown=node:node . /app/data
|
|
54
|
+
|
|
55
|
+
FROM runtime-base AS runtime
|
|
56
|
+
{{/if}}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{{! @skipIfEmpty }}
|
|
2
|
+
{{#if isSvelteOnlyApp}}
|
|
3
|
+
server {
|
|
4
|
+
listen 8080;
|
|
5
|
+
server_name _;
|
|
6
|
+
root /usr/share/nginx/html;
|
|
7
|
+
|
|
8
|
+
location / {
|
|
9
|
+
try_files $uri $uri/ /index.html;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
location = /health {
|
|
13
|
+
access_log off;
|
|
14
|
+
add_header Content-Type text/plain;
|
|
15
|
+
return 200 'ok';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
{{/if}}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "{{projectName}}",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.51",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "{{#if_eq appType 'admin'}}Enterprise management system built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'website'}}Enterprise website built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'h5'}}Mobile H5 app built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'api'}}REST API service built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'mobile-app'}}Mobile app built with hai Agent Framework{{/if_eq}}",
|
|
6
6
|
"keywords": ["hai", {{#unless isSvelteOnlyApp}}"sveltekit", {{/unless}}"svelte5", "{{appType}}"],
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"type": "module",
|
|
9
|
+
{{#unless isSvelteOnlyApp}}
|
|
10
|
+
"files": [
|
|
11
|
+
"build",
|
|
12
|
+
"config",
|
|
13
|
+
"data",
|
|
14
|
+
"package.json"
|
|
15
|
+
],
|
|
16
|
+
{{/unless}}
|
|
9
17
|
"scripts": {
|
|
10
18
|
{{#if hasI18n}}
|
|
11
19
|
"paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
|
|
@@ -19,7 +27,13 @@
|
|
|
19
27
|
"test": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vitest run",
|
|
20
28
|
"test:watch": "vitest",
|
|
21
29
|
"test:e2e": "playwright test",
|
|
22
|
-
"clean": "{{#if isSvelteOnlyApp}}rimraf dist build android ios .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}"
|
|
30
|
+
"clean": "{{#if isSvelteOnlyApp}}rimraf dist build android ios .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}",
|
|
31
|
+
"docker:build": "docker build --target runtime -t {{projectName}}:latest .",
|
|
32
|
+
{{#unless isSvelteOnlyApp}}
|
|
33
|
+
"docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./data -t {{projectName}}:latest .",
|
|
34
|
+
{{/unless}}
|
|
35
|
+
"podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest ."{{#unless isSvelteOnlyApp}},
|
|
36
|
+
"podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./data -t {{projectName}}:latest ."{{/unless}}{{#if isCapacitorApp}},
|
|
23
37
|
"cap:add:android": "cap add android",
|
|
24
38
|
"cap:add:ios": "cap add ios",
|
|
25
39
|
"cap:sync": "pnpm build && cap sync",
|
|
@@ -78,11 +92,7 @@
|
|
|
78
92
|
"@capacitor/push-notifications": "^8.0.1",
|
|
79
93
|
"@capacitor/status-bar": "^8.0.1",
|
|
80
94
|
{{else}}
|
|
81
|
-
{{#if_eq appType 'api'}}
|
|
82
95
|
"@sveltejs/adapter-node": "^5.5.4",
|
|
83
|
-
{{else}}
|
|
84
|
-
"@sveltejs/adapter-auto": "^7.0.1",
|
|
85
|
-
{{/if_eq}}
|
|
86
96
|
{{/if}}
|
|
87
97
|
{{#unless isSvelteOnlyApp}}
|
|
88
98
|
"@sveltejs/kit": "^2.61.1",
|
|
@@ -8,15 +8,7 @@ import process from 'node:process'
|
|
|
8
8
|
import { autoImportHaiUi } from '@h-ai/ui/auto-import'
|
|
9
9
|
{{/if}}
|
|
10
10
|
{{#unless isSvelteOnlyApp}}
|
|
11
|
-
{{#if isCapacitorApp}}
|
|
12
|
-
import adapter from '@sveltejs/adapter-static'
|
|
13
|
-
{{else}}
|
|
14
|
-
{{#if_eq appType 'api'}}
|
|
15
11
|
import adapter from '@sveltejs/adapter-node'
|
|
16
|
-
{{else}}
|
|
17
|
-
import adapter from '@sveltejs/adapter-auto'
|
|
18
|
-
{{/if_eq}}
|
|
19
|
-
{{/if}}
|
|
20
12
|
{{/unless}}
|
|
21
13
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
|
22
14
|
|