@h-ai/cli 0.1.0-alpha.51 → 0.1.0-alpha.54
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/README.md +14 -0
- package/dist/{chunk-U4PA632G.js → chunk-FVDXJYMO.js} +34 -67
- package/dist/chunk-FVDXJYMO.js.map +1 -0
- package/dist/cli-config-templates-WGN2I23J.js +5 -0
- package/dist/{cli-config-templates-GSXKLGU5.js.map → cli-config-templates-WGN2I23J.js.map} +1 -1
- package/dist/index.js +299 -127
- package/dist/index.js.map +1 -1
- package/dist/{src-QNEJJ3BV.js → src-6ZBWDBUB.js} +127 -87
- package/dist/{src-QNEJJ3BV.js.map → src-6ZBWDBUB.js.map} +1 -1
- package/package.json +13 -4
- package/templates/apps/api/Dockerfile.hbs +6 -3
- package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +1 -0
- package/templates/apps/api/apps/{{projectName}}-service/data/.gitkeep.hbs +1 -0
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -0
- package/templates/apps/api/package.json.hbs +1 -0
- package/templates/apps/fullstack/.dockerignore.hbs +2 -0
- package/templates/apps/fullstack/Dockerfile.hbs +23 -36
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +2 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/public/runtime-config.js.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/tests/api.test.ts.hbs +27 -0
- package/templates/apps/fullstack/docker/19-runtime-config.envsh.hbs +5 -0
- package/templates/apps/fullstack/docker/nginx.conf.hbs +6 -0
- package/templates/apps/fullstack/docker/runtime-config.js.template.hbs +3 -0
- package/templates/apps/fullstack/package.json.hbs +3 -4
- package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +3 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/data/.gitkeep.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +9 -1
- package/templates/base/.gitignore.hbs +2 -1
- package/templates/base/Dockerfile.hbs +10 -4
- package/templates/base/eslint.config.js.hbs +1 -0
- package/templates/base/package.json.hbs +3 -1
- package/templates/skills/hai-ai/SKILL.md +4 -0
- package/templates/skills/hai-ai/reference.md +5 -1
- package/templates/skills/hai-app-create/SKILL.md +3 -1
- package/templates/skills/hai-app-review/SKILL.md +6 -0
- package/templates/skills/hai-build/SKILL.md +8 -0
- package/templates/skills/hai-cache/SKILL.md +2 -0
- package/templates/skills/hai-capacitor/SKILL.md +2 -0
- package/templates/skills/hai-ci/SKILL.md +12 -0
- package/templates/skills/hai-core/SKILL.md +2 -0
- package/templates/skills/hai-crypto/SKILL.md +2 -0
- package/templates/skills/hai-deploy/SKILL.md +12 -0
- package/templates/skills/hai-iam/SKILL.md +6 -0
- package/templates/skills/hai-scheduler/SKILL.md +12 -0
- package/templates/skills/hai-ui/SKILL.md +4 -0
- package/dist/chunk-U4PA632G.js.map +0 -1
- package/dist/cli-config-templates-GSXKLGU5.js +0 -5
- package/templates/apps/fullstack/docker/fullstack-server.mjs.hbs +0 -194
|
@@ -7,6 +7,8 @@ description: 使用 @h-ai/deploy 进行自动化部署:Vercel 部署 + 基础
|
|
|
7
7
|
|
|
8
8
|
## 能力契约
|
|
9
9
|
|
|
10
|
+
`hai deploy` 在模块/配置缺失或凭证、扫描、初始化、部署、关闭失败时返回非零退出码。自动化流程必须以退出码判断成功,不要只判断命令是否完成或是否输出日志。
|
|
11
|
+
|
|
10
12
|
| 项目 | 契约 |
|
|
11
13
|
| --- | --- |
|
|
12
14
|
| 能力 | 使用 @h-ai/deploy 进行自动化部署:Vercel 部署 + 基础设施开通(Neon/Upstash/R2/Resend/Aliyun);当需求涉及应用部署、环境配置、凭证管理或云服务开通时使用。 |
|
|
@@ -57,13 +59,17 @@ services:
|
|
|
57
59
|
provisioner: cloudflare-r2
|
|
58
60
|
accountId: ${HAI_DEPLOY_CF_ACCOUNT_ID}
|
|
59
61
|
apiToken: ${HAI_DEPLOY_CF_API_TOKEN}
|
|
62
|
+
accessKeyId: ${HAI_DEPLOY_R2_ACCESS_KEY_ID}
|
|
63
|
+
secretAccessKey: ${HAI_DEPLOY_R2_SECRET_ACCESS_KEY}
|
|
60
64
|
email:
|
|
61
65
|
provisioner: resend
|
|
62
66
|
apiKey: ${HAI_DEPLOY_RESEND_API_KEY}
|
|
67
|
+
from: ${HAI_DEPLOY_RESEND_FROM}
|
|
63
68
|
sms:
|
|
64
69
|
provisioner: aliyun
|
|
65
70
|
accessKeyId: ${HAI_DEPLOY_ALIYUN_ACCESS_KEY_ID}
|
|
66
71
|
accessKeySecret: ${HAI_DEPLOY_ALIYUN_ACCESS_KEY_SECRET}
|
|
72
|
+
signName: ${HAI_DEPLOY_ALIYUN_SIGN_NAME}
|
|
67
73
|
```
|
|
68
74
|
|
|
69
75
|
这些凭证名由 `deploy.credentials` 管理,属于显式特殊映射;若同时设置
|
|
@@ -230,3 +236,9 @@ const result = await deploy.deployApp('./apps/my-app')
|
|
|
230
236
|
- **hai-core** — 配置加载、日志、HaiResult 类型
|
|
231
237
|
- **hai-reldb** — 数据库(Neon PostgreSQL)
|
|
232
238
|
- **hai-cache** — 缓存(Upstash Redis)
|
|
239
|
+
|
|
240
|
+
失败与降级提示也必须国际化:官网 API 按请求 locale 生成 AI 降级/邮件送达状态;未送达不能称为已接收。CLI 部署状态使用 cliM,Gallery 加密能力限制使用双语消息。中文与英文分别验证未配置、失败和成功路径,无需调用付费服务。
|
|
241
|
+
|
|
242
|
+
云资源配置消费:Neon 输出 HAI_DB(PostgreSQL JSON),Upstash 输出 HAI_CACHE(Redis rediss URL JSON),R2 输出 HAI_STORAGE(扁平 S3 JSON)。邮件/短信输出 HAI_REACH_PROVIDERS 数组,deployApp 合并两个渠道并保留 YAML 中的消息模板;此覆盖替换原 providers 数组。Core 支持整体对象/数组覆盖,父节点优先于叶子变量,仍执行 Schema 校验;构建子进程与目标运行环境使用相同开通结果。R2 必须提供已有 S3 accessKeyId/secretAccessKey;Resend 必须提供已验证域名的 from;阿里云必须提供已审核 signName/模板。应用须已有相应 _db/_cache/_storage/_reach.yml 并在启动时加载,网络客户端需要 Node 运行时。
|
|
243
|
+
|
|
244
|
+
部署失败恢复:`ProvisionResult.resourceStatus` 标记 created/reused;`getDeployRecovery(result.error)` 读取 projectName、stage、platformProjectId 和资源清单,unknown 表示请求结果不确定,必须先在服务商控制台核对。失败资源已返回 ID 时保留该 ID。恢复结果不含 envVars、原始异常或连接凭证,CLI 会展示清单和建议。同名重试复用现有资源;不自动删除复用资源,新建资源仅在人工确认未被使用后清理。
|
|
@@ -7,6 +7,12 @@ description: 使用 @h-ai/iam 进行身份认证(密码/OTP/LDAP/API Key)、
|
|
|
7
7
|
|
|
8
8
|
## 能力契约
|
|
9
9
|
|
|
10
|
+
角色成员数使用 `getRoleUserCounts(roleIds)`,一次聚合返回 `HaiResult<Map<string, number>>`;成功时无关联为 0,失败必须展示错误,不能伪造零值。
|
|
11
|
+
|
|
12
|
+
角色资料和权限必须通过 `createRole({ ..., permissionIds })` / `updateRole(id, { ..., permissionIds })` 一次事务保存,禁止在应用层并行增删权限模拟替换。空数组清空、未传保持、未知权限失败、重复 ID 去重;并发以最后提交的完整集合为准。自管事务提交后同步会话,外部事务由调用方提交后同步。
|
|
13
|
+
|
|
14
|
+
管理界面自动创建角色时使用 `role_${core.id.generate()}` 生成 code,显示名称独立保存;不要通过移除中文、表情或折叠大小写生成标识。重命名不重建 code。
|
|
15
|
+
|
|
10
16
|
| 项目 | 契约 |
|
|
11
17
|
| --- | --- |
|
|
12
18
|
| 能力 | 使用 @h-ai/iam 进行身份认证(密码/OTP/LDAP/API Key)、统一 Bearer Token 管理(TokenPair/refresh/revoke)与 RBAC 授权;当需求涉及登录、注册、权限检查、角色管理、Token 认证或通过 @h-ai/api-contract 暴露 IAM API 时使用。 |
|
|
@@ -7,6 +7,16 @@ description: 使用 @h-ai/scheduler 进行统一定时任务管理、任务持
|
|
|
7
7
|
|
|
8
8
|
## 能力契约
|
|
9
9
|
|
|
10
|
+
持久化动态任务每个 `tickInterval`(默认 1000ms)在运行节点刷新;API 操作及获锁后的每次尝试重读定义,读取失败或任务已禁用/删除/改期时中断执行。`tasks` 仅表示最近成功刷新快照,已开始的外部副作用不会回滚。跨节点任务使用 `register()` 与共享数据库、Redis;`init({ tasks })` 是节点本地配置,memory cache 不支持跨进程防重。
|
|
11
|
+
|
|
12
|
+
关闭必须 `await scheduler.close()` 后再关闭数据库和缓存:关闭会拒绝新任务、取消 HTTP/Worker/重试并等待执行链退出。生命周期回调通过事件 `signal` 协作取消,必须自行停止外部副作用,禁止在 Hook 阻塞主线程。旧任务取消后不重试、不删除新实例同 ID 的任务。
|
|
13
|
+
|
|
14
|
+
JS 任务每次调用独立 Worker,默认 30000 毫秒期限包含线程启动;超时后等待线程终止才返回失败或重试,同步死循环不会占用主事件循环。上下文和结果须支持结构化克隆。Worker/vm 不构成安全沙箱,仍只接受受信任的服务端代码。
|
|
15
|
+
|
|
16
|
+
初始化为全有或全无:必须检查 `init()` 的 HaiResult。空 ID、重复 ID(包括与数据库记录重复)、无效 cron、持久化读取或解析失败均使初始化失败并清理状态;修正后重试,不会静默跳过任务。
|
|
17
|
+
|
|
18
|
+
API 任务 `timeout` 默认 30000 毫秒,覆盖请求及完整响应体读取;`maxResponseBytes` 指定实际接收字节上限(正安全整数,默认 1048576 即 1 MiB),例如 `maxResponseBytes: 10 * 1024 * 1024` 允许最大 10 MiB;非法值在请求前失败。超时、超限和非成功 HTTP 状态均失败,重试由任务 retry 策略控制。
|
|
19
|
+
|
|
10
20
|
| 项目 | 契约 |
|
|
11
21
|
| --- | --- |
|
|
12
22
|
| 能力 | 使用 @h-ai/scheduler 进行统一定时任务管理、任务持久化、手工触发、触发来源审计、全局生命周期回调与执行日志查询;当需求涉及 cron 调度、JS 函数字符串任务、API 任务、分布式锁或任务事件回调时使用。 |
|
|
@@ -198,3 +208,5 @@ await scheduler.init({
|
|
|
198
208
|
| `HaiSchedulerError.LOCK_ACQUIRE_FAILED` | `hai:scheduler:029` | 锁获取失败 |
|
|
199
209
|
| `HaiSchedulerError.JS_COMPILE_FAILED` | `hai:scheduler:030` | JS 编译失败 |
|
|
200
210
|
| `HaiSchedulerError.HOOK_EXECUTION_FAILED` | `hai:scheduler:031` | Hook 执行失败 |
|
|
211
|
+
|
|
212
|
+
调度循环按分钟时间点匹配 cron,并用同一分钟生成分布式锁键;启动或 tick 延迟至非零秒仍检查当前分钟,同一轮运行不会每个 tick 重复调度。
|
|
@@ -41,6 +41,10 @@ description: 使用 @h-ai/ui 构建多端应用界面,包含三层组件架构
|
|
|
41
41
|
|
|
42
42
|
## 使用边界
|
|
43
43
|
|
|
44
|
+
- 加密输入变更会清空密文,乱序/清空/卸载后的旧结果不会回填;失败显示错误。提交前确认当前密文非空,不缓存上次密文。
|
|
45
|
+
|
|
46
|
+
- `EncryptedInput` 的 `onencrypt` 必须接入真实加密服务;`algorithm` 仅标注算法。缺少回调时显示不可用,不生成密文,不允许用编码代替加密。
|
|
47
|
+
|
|
44
48
|
- **优先复用 `@h-ai/ui` 现有组件**,不要在 app 中重复实现同类按钮、表格、表单、弹层或场景页面。
|
|
45
49
|
- **页面级文案与业务逻辑放在应用层**;`@h-ai/ui` 负责展示与交互,不承接业务服务、数据库访问或页面级 i18n 管理。
|
|
46
50
|
- **场景组件自带内置中英文文案**(如 IAM / CRUD / AI / Storage 场景),除非组件显式提供覆盖 prop,否则不要把页面翻译样板再传进去。
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/cli-config-templates.ts"],"names":[],"mappings":"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","file":"chunk-U4PA632G.js","sourcesContent":["/**\n * @h-ai/cli — 配置文件模板\n *\n * 为各模块生成默认配置文件内容。\n * @module cli-config-templates\n */\n\nimport type { AiModuleConfig, CacheModuleConfig, CoreModuleConfig, DbModuleConfig, FeatureId, IamModuleConfig, ModuleConfigs, StorageModuleConfig } from '../cli-types.js'\n\n/**\n * 生成模块配置文件内容\n *\n * @param moduleKey - 模块标识(core/serv/db/cache/iam/storage/ai)\n * @param configs - 用户自定义配置值\n * @returns YAML 格式的配置内容\n */\nexport function generateConfigFile(moduleKey: string, configs?: ModuleConfigs): string {\n switch (moduleKey) {\n case 'core':\n return generateCoreConfig(configs?.core)\n case 'serv':\n return generateServConfig()\n case 'db':\n return generateDbConfig(configs?.db)\n case 'cache':\n return generateCacheConfig(configs?.cache)\n case 'iam':\n return generateIamConfig(configs?.iam)\n case 'storage':\n return generateStorageConfig(configs?.storage)\n case 'ai':\n return generateAiConfig(configs?.ai)\n case 'deploy':\n return generateDeployConfig()\n case 'vecdb':\n return generateVecdbConfig()\n case 'reach':\n return generateReachConfig()\n case 'scheduler':\n return generateSchedulerConfig()\n case 'audit':\n return generateAuditConfig()\n case 'payment':\n return generatePaymentConfig()\n default:\n return `# ${moduleKey} 配置\\n`\n }\n}\n\nfunction generateCoreConfig(cfg?: CoreModuleConfig): string {\n const name = cfg?.name ?? 'my-app'\n const locale = cfg?.defaultLocale ?? 'zh-CN'\n const supportedLocales = locale === 'en-US'\n ? ` - en-US\\n - zh-CN`\n : ` - zh-CN\\n - en-US`\n\n return `# =============================================================================\n# 应用配置\n# =============================================================================\n\nname: ${name}\nversion: 0.1.0\nenv: development\ndebug: false\n\ndefaultLocale: ${locale}\nsupportedLocales:\n${supportedLocales}\n`\n}\n\nfunction generateServConfig(): string {\n return `# =============================================================================\n# Service / HTTP 入口配置\n# =============================================================================\n\nhttp:\n apiPrefix: /api/v1\n openapi:\n path: /openapi.json\n docs:\n path: /docs\n health:\n path: /health\n readyPath: /ready\n rpc: false\n\n# 开发期默认关闭 transport;启用 @h-ai/crypto 后可按需改为对象配置\ntransport: false\n`\n}\n\nfunction generateDbConfig(cfg?: DbModuleConfig): string {\n const dbType = cfg?.type ?? 'sqlite'\n const database = cfg?.database ?? (dbType === 'sqlite' ? './data/app.db' : 'hai')\n const host = cfg?.host ?? 'localhost'\n const port = cfg?.port ?? (dbType === 'postgresql' ? 5432 : 3306)\n\n if (dbType === 'sqlite') {\n return `# =============================================================================\n# 数据库配置\n# =============================================================================\n\n# 数据库类型: sqlite | postgresql | mysql\ntype: sqlite\n\n# SQLite 数据库路径\ndatabase: ${database}\n\n# PostgreSQL/MySQL 配置(可选)\n# host: localhost\n# port: 5432\n# user: postgres\n# password: ''\n`\n }\n\n const user = dbType === 'postgresql' ? 'postgres' : 'root'\n\n return `# =============================================================================\n# 数据库配置\n# =============================================================================\n\n# 数据库类型: sqlite | postgresql | mysql\ntype: ${dbType}\n\n# 数据库名称\ndatabase: ${database}\n\n# 连接配置\nhost: ${host}\nport: ${port}\nuser: ${user}\npassword: ''\n`\n}\n\nfunction generateCacheConfig(cfg?: CacheModuleConfig): string {\n const cacheType = cfg?.type ?? 'memory'\n\n if (cacheType === 'memory') {\n return `# =============================================================================\n# 缓存配置\n# =============================================================================\n\n# 缓存类型: memory | redis\ntype: memory\n\n# Redis 配置(当 type 为 redis 时使用)\n# url: redis://localhost:6379/0\n# host: localhost\n# port: 6379\n# password: ''\n# keyPrefix: 'hai:'\n`\n }\n\n const host = cfg?.host ?? 'localhost'\n const port = cfg?.port ?? 6379\n\n return `# =============================================================================\n# 缓存配置\n# =============================================================================\n\n# 缓存类型: memory | redis\ntype: redis\n\n# Redis 配置\nhost: ${host}\nport: ${port}\npassword: ''\nkeyPrefix: 'hai:'\n`\n}\n\nfunction generateIamConfig(cfg?: IamModuleConfig): string {\n const loginPassword = cfg?.loginPassword ?? true\n const loginOtp = cfg?.loginOtp ?? false\n\n return `# =============================================================================\n# IAM 配置\n# =============================================================================\n\n# 登录配置\nlogin:\n password: ${loginPassword}\n otp: ${loginOtp}\n ldap: false\n\n# 密码策略\npassword:\n minLength: 8\n maxLength: 128\n requireUppercase: false\n requireLowercase: false\n requireNumber: false\n requireSpecialChar: false\n\n# 会话配置\nsession:\n maxAge: 86400 # 会话超时(秒),默认 24 小时\n sliding: true # 滑动窗口(每次请求刷新过期时间)\n singleDevice: false # 单设备登录\n refreshTokenMaxAge: 604800 # refreshToken 过期(秒),默认 7 天\n\n# RBAC 配置\nrbac:\n defaultRole: user\n\n# 是否自动创建默认数据\nseedDefaultData: true\n`\n}\n\nfunction generateStorageConfig(cfg?: StorageModuleConfig): string {\n const storageType = cfg?.type ?? 'local'\n\n if (storageType === 'local') {\n const localPath = cfg?.localPath ?? './data/uploads'\n return `# =============================================================================\n# 存储配置\n# =============================================================================\n\n# 默认 Provider\ndefaultProvider: local\n\n# Provider 配置\nproviders:\n local:\n type: local\n root: ${localPath}\n maxFileSize: 10485760 # 10MB\n\n # S3 配置(可选)\n # s3:\n # type: s3\n # bucket: my-bucket\n # region: us-east-1\n # accessKeyId: ''\n # secretAccessKey: ''\n # endpoint: ''\n # forcePathStyle: false\n`\n }\n\n return `# =============================================================================\n# 存储配置\n# =============================================================================\n\n# 默认 Provider\ndefaultProvider: s3\n\n# Provider 配置\nproviders:\n s3:\n type: s3\n bucket: ''\n region: us-east-1\n accessKeyId: ''\n secretAccessKey: ''\n # endpoint: ''\n # forcePathStyle: false\n\n # 本地存储(可选)\n # local:\n # type: local\n # root: ./data/uploads\n # maxFileSize: 10485760 # 10MB\n`\n}\n\nfunction generateAiConfig(cfg?: AiModuleConfig): string {\n const model = cfg?.model ?? 'gpt-4o-mini'\n\n // AIConfigSchema 结构:llm(必填)+ 可选子模块\n // LLMConfigSchema 支持顶层 apiKey/model,以及多模型 models 数组\n return `# =============================================================================\n# AI 配置\n# =============================================================================\n\n# LLM 配置(必填)\nllm:\n # API Key(约定环境变量 HAI_AI_LLM_APIKEY 优先)\n apiKey: ''\n # Base URL(约定环境变量 HAI_AI_LLM_BASEURL 优先)\n # baseUrl: https://api.openai.com/v1\n # 默认模型\n model: ${model}\n # 全局最大 Token 数(默认 4096)\n # maxTokens: 4096\n # 采样温度(0-2,默认 0.7)\n # temperature: 0.7\n # 请求超时(毫秒,默认 60000)\n # timeout: 60000\n\n # 多模型配置(可选,用于场景路由)\n # models:\n # - id: fast\n # model: gpt-4o-mini\n # - id: smart\n # model: gpt-4o\n # apiKey: ''\n\n # 场景模型映射(可选)\n # scenarios:\n # default: fast\n # analysis: smart\n`\n}\n\n/**\n * 根据选中的功能模块生成 .env.example 内容\n *\n * @param features - 选中的功能模块列表\n * @param configs - 用户自定义配置值\n * @returns .env.example 文件内容\n */\nexport function generateEnvExample(features: FeatureId[], configs?: ModuleConfigs): string {\n const sections: string[] = []\n\n // 文件头\n sections.push(`# =============================================================================\n# Environment Variables\n# =============================================================================\n#\n# Copy this file to .env and fill in actual values.\n# Config files use literal defaults; matching HAI variables override YAML leaves.\n#\n# Naming convention: HAI_<config name>_<YAML path>\n# =============================================================================`)\n\n // 应用基础(始终生成)\n sections.push(`\n# =============================================================================\n# Application\n# =============================================================================\n# Environment: development | production | test\nHAI_CORE_ENV=development\n# Enable debug mode\nHAI_CORE_DEBUG=false`)\n\n // 数据库\n if (features.includes('db')) {\n const dbType = configs?.db?.type ?? 'sqlite'\n const dbDatabase = configs?.db?.database ?? (dbType === 'sqlite' ? './data/app.db' : 'hai')\n const isSqlite = dbType === 'sqlite'\n sections.push(`\n# =============================================================================\n# Database (@h-ai/reldb)\n# =============================================================================\n# Database type: sqlite | postgresql | mysql\nHAI_DB_TYPE=${dbType}\n# SQLite: file path; PostgreSQL/MySQL: database name\nHAI_DB_DATABASE=${dbDatabase}${isSqlite\n ? `\n# PostgreSQL/MySQL connection (uncomment when not using sqlite)\n# HAI_DB_HOST=localhost\n# HAI_DB_PORT=5432\n# HAI_DB_USER=postgres\n# HAI_DB_PASSWORD=`\n : `\n# Connection\nHAI_DB_HOST=${configs?.db?.host ?? 'localhost'}\nHAI_DB_PORT=${configs?.db?.port ?? (dbType === 'postgresql' ? 5432 : 3306)}\nHAI_DB_USER=${dbType === 'postgresql' ? 'postgres' : 'root'}\nHAI_DB_PASSWORD=`}`)\n }\n\n // IAM / Session\n if (features.includes('iam')) {\n sections.push(`\n# =============================================================================\n# IAM (@h-ai/iam)\n# =============================================================================\n# HAI_IAM_PASSWORD_MINLENGTH=8\n# HAI_IAM_SESSION_MAXAGE=86400\n# HAI_IAM_SESSION_REFRESHTOKENMAXAGE=604800\n# HAI_KIT_COOKIE_KEY= # 32-char hex for SM4-CBC cookie encryption\n`)\n }\n\n // 缓存\n if (features.includes('cache')) {\n const cacheType = configs?.cache?.type ?? 'memory'\n if (cacheType === 'redis') {\n sections.push(`\n# =============================================================================\n# Cache (@h-ai/cache)\n# =============================================================================\n# Cache type: memory | redis\nHAI_CACHE_TYPE=redis\n# Redis connection\nHAI_CACHE_HOST=${configs?.cache?.host ?? 'localhost'}\nHAI_CACHE_PORT=${configs?.cache?.port ?? 6379}\nHAI_CACHE_PASSWORD=\n# HAI_CACHE_URL=redis://localhost:6379/0\n# HAI_CACHE_DB=0\n# HAI_CACHE_CONNECT_TIMEOUT=10000\n# HAI_CACHE_COMMAND_TIMEOUT=5000\n# HAI_CACHE_KEY_PREFIX=hai:`)\n }\n else {\n sections.push(`\n# =============================================================================\n# Cache (@h-ai/cache)\n# =============================================================================\n# Cache type: memory | redis\nHAI_CACHE_TYPE=memory\n# Redis connection (uncomment when type=redis)\n# HAI_CACHE_URL=redis://localhost:6379/0\n# HAI_CACHE_HOST=localhost\n# HAI_CACHE_PORT=6379\n# HAI_CACHE_PASSWORD=\n# HAI_CACHE_DB=0\n# HAI_CACHE_CONNECT_TIMEOUT=10000\n# HAI_CACHE_COMMAND_TIMEOUT=5000\n# HAI_CACHE_KEY_PREFIX=hai:`)\n }\n }\n\n // 存储\n if (features.includes('storage')) {\n const storageType = configs?.storage?.type ?? 'local'\n if (storageType === 's3') {\n sections.push(`\n# =============================================================================\n# Storage (@h-ai/storage)\n# =============================================================================\n# Default storage provider: local | s3\nHAI_STORAGE_DEFAULTPROVIDER=s3\n# S3 / S3-compatible\nHAI_STORAGE_PROVIDERS_S3_BUCKET=\nHAI_STORAGE_PROVIDERS_S3_REGION=us-east-1\nHAI_STORAGE_PROVIDERS_S3_ACCESSKEYID=\nHAI_STORAGE_PROVIDERS_S3_SECRETACCESSKEY=\n# HAI_STORAGE_PROVIDERS_S3_ENDPOINT= # Custom endpoint for MinIO / Aliyun OSS etc.\n# HAI_STORAGE_PROVIDERS_S3_FORCEPATHSTYLE=false\n# Local storage (uncomment when type=local)\n# HAI_STORAGE_PROVIDERS_LOCAL_ROOT=./data/uploads`)\n }\n else {\n sections.push(`\n# =============================================================================\n# Storage (@h-ai/storage)\n# =============================================================================\n# Default storage provider: local | s3\nHAI_STORAGE_DEFAULTPROVIDER=local\n# Local storage root path\nHAI_STORAGE_PROVIDERS_LOCAL_ROOT=${configs?.storage?.localPath ?? './data/uploads'}\n# S3 / S3-compatible (uncomment when type=s3)\n# HAI_STORAGE_PROVIDERS_S3_BUCKET=\n# HAI_STORAGE_PROVIDERS_S3_REGION=us-east-1\n# HAI_STORAGE_PROVIDERS_S3_ACCESSKEYID=\n# HAI_STORAGE_PROVIDERS_S3_SECRETACCESSKEY=\n# HAI_STORAGE_PROVIDERS_S3_ENDPOINT= # Custom endpoint for MinIO / Aliyun OSS etc.\n# HAI_STORAGE_PROVIDERS_S3_FORCEPATHSTYLE=false`)\n }\n }\n\n // AI\n if (features.includes('ai')) {\n const provider = configs?.ai?.defaultProvider ?? 'openai'\n if (provider === 'anthropic') {\n sections.push(`\n# =============================================================================\n# AI (@h-ai/ai)\n# =============================================================================\n# OpenAI-compatible API Key (Anthropic models via OpenAI-compatible proxy)\nHAI_AI_LLM_APIKEY=\n# HAI_AI_LLM_BASEURL= # Custom base URL\n# Optional compatibility fallback\n# OPENAI_API_KEY=`)\n }\n else if (provider === 'openai') {\n sections.push(`\n# =============================================================================\n# AI (@h-ai/ai)\n# =============================================================================\n# OpenAI / OpenAI-compatible\nHAI_AI_LLM_APIKEY=\n# HAI_AI_LLM_BASEURL= # Custom base URL (e.g. local proxy, Azure)\n# Optional compatibility fallback\n# OPENAI_API_KEY=`)\n }\n else {\n sections.push(`\n# =============================================================================\n# AI (@h-ai/ai)\n# =============================================================================\n# OpenAI-compatible provider\nHAI_AI_LLM_APIKEY=\n# HAI_AI_LLM_BASEURL= # Custom base URL\n# Optional compatibility fallback\n# OPENAI_API_KEY=`)\n }\n }\n\n // VecDB\n if (features.includes('vecdb')) {\n sections.push(`\n# =============================================================================\n# Vector Database (@h-ai/vecdb)\n# =============================================================================\n# Backend type: lancedb | pgvector | qdrant\nHAI_VECDB_TYPE=lancedb\n# LanceDB data path\nHAI_VECDB_PATH=./data/vecdb\n# Qdrant (uncomment when type=qdrant)\n# HAI_VECDB_URL=http://localhost:6333\n# HAI_VECDB_APIKEY=\n`)\n }\n\n // Reach\n if (features.includes('reach')) {\n sections.push(`\n# =============================================================================\n# Reach (@h-ai/reach)\n# =============================================================================\n# SMTP (uncomment when enabling email provider)\n# HAI_REACH_SMTP_HOST=smtp.example.com\n# HAI_REACH_SMTP_PORT=465\n# HAI_REACH_SMTP_USER=\n# HAI_REACH_SMTP_PASS=\n# HAI_REACH_SMTP_FROM=noreply@example.com\n`)\n }\n\n // Payment\n if (features.includes('payment')) {\n sections.push(`\n# =============================================================================\n# Payment (@h-ai/payment)\n# =============================================================================\n# WeChat Pay\n# HAI_PAYMENT_WECHAT_MCHID=\n# HAI_PAYMENT_WECHAT_APIV3KEY=\n# HAI_PAYMENT_WECHAT_SERIALNO=\n# HAI_PAYMENT_WECHAT_PRIVATEKEY=\n# HAI_PAYMENT_WECHAT_APPID=\n# Alipay\n# HAI_PAYMENT_ALIPAY_APPID=\n# HAI_PAYMENT_ALIPAY_PRIVATEKEY=\n# HAI_PAYMENT_ALIPAY_ALIPAYPUBLICKEY=\n# Stripe\n# HAI_PAYMENT_STRIPE_SECRETKEY=\n# HAI_PAYMENT_STRIPE_WEBHOOKSECRET=\n`)\n }\n\n // 测试(始终生成)\n sections.push(`\n# =============================================================================\n# Testing\n# =============================================================================\n# E2E test mode (relaxes rate limiting etc.)\n# HAI_E2E=1\n`)\n\n return sections.join('\\n')\n}\n\nfunction generateDeployConfig(): string {\n return `# =============================================================================\n# 部署配置 (@h-ai/deploy)\n# =============================================================================\n# 部署平台凭证通过 ~/.hai/credentials.yml 管理\n# 环境变量引用格式: \\${VAR_NAME:default_value}\n\n# 部署平台\nprovider:\n type: vercel\n token: \\${HAI_DEPLOY_VERCEL_TOKEN}\n\n# 基础设施服务(按需开启)\nservices:\n # PostgreSQL 数据库 (Neon)\n # db:\n # provisioner: neon\n # apiKey: \\${HAI_DEPLOY_NEON_API_KEY}\n\n # Redis 缓存 (Upstash)\n # cache:\n # provisioner: upstash\n # email: \\${HAI_DEPLOY_UPSTASH_EMAIL}\n # apiKey: \\${HAI_DEPLOY_UPSTASH_API_KEY}\n\n # S3 存储 (Cloudflare R2)\n # storage:\n # provisioner: cloudflare-r2\n # accountId: \\${HAI_DEPLOY_CF_ACCOUNT_ID}\n # apiToken: \\${HAI_DEPLOY_CF_API_TOKEN}\n\n # 邮件 (Resend)\n # email:\n # provisioner: resend\n # apiKey: \\${HAI_DEPLOY_RESEND_API_KEY}\n\n # 短信 (阿里云)\n # sms:\n # provisioner: aliyun\n # accessKeyId: \\${HAI_DEPLOY_ALIYUN_ACCESS_KEY_ID}\n # accessKeySecret: \\${HAI_DEPLOY_ALIYUN_ACCESS_KEY_SECRET}\n`\n}\n\nfunction generateVecdbConfig(): string {\n return `# =============================================================================\n# 向量数据库配置 (@h-ai/vecdb)\n# =============================================================================\n\n# 数据库类型: lancedb | pgvector | qdrant\ntype: lancedb\n\n# LanceDB 数据目录\npath: ./data/vecdb\n\n# 距离度量: cosine | euclidean | dot\n# metric: cosine\n\n# pgvector 配置(当 type 为 pgvector 时使用)\n# type: pgvector\n# host: localhost\n# port: 5432\n# database: hai\n# user: postgres\n# password: ''\n# indexType: hnsw\n# tablePrefix: vec_\n\n# Qdrant 配置(当 type 为 qdrant 时使用)\n# type: qdrant\n# url: http://localhost:6333\n# apiKey: ''\n`\n}\n\nfunction generateReachConfig(): string {\n return `# =============================================================================\n# 消息推送配置 (@h-ai/reach)\n# =============================================================================\n\nproviders:\n # 控制台输出(开发/测试用)\n - name: console_dev\n type: console\n\n # SMTP 邮件(按需开启)\n # - name: email\n # type: smtp\n # host: \\${HAI_REACH_SMTP_HOST:smtp.example.com}\n # port: \\${HAI_REACH_SMTP_PORT:465}\n # secure: true\n # user: \\${HAI_REACH_SMTP_USER:}\n # pass: \\${HAI_REACH_SMTP_PASS:}\n # from: \\${HAI_REACH_SMTP_FROM:noreply@example.com}\n\n # 阿里云短信(按需开启)\n # - name: sms\n # type: aliyun-sms\n # accessKeyId: \\${HAI_REACH_SMS_ACCESS_KEY}\n # accessKeySecret: \\${HAI_REACH_SMS_SECRET_KEY}\n # signName: MyApp\n\n# 模板(可选)\n# templates:\n# - name: verification_code\n# provider: email\n# subject: \"验证码: {code}\"\n# body: \"您的验证码是 {code},有效期 {minutes} 分钟。\"\n\n# 免打扰(可选)\n# dnd:\n# enabled: true\n# strategy: delay\n# start: \"22:00\"\n# end: \"08:00\"\n`\n}\n\nfunction generateSchedulerConfig(): string {\n return `# =============================================================================\n# 任务调度配置 (@h-ai/scheduler)\n# =============================================================================\n\n# 是否启用数据库日志(需要 @h-ai/reldb)\nenableDb: true\n\n# 执行日志表名\ntableName: hai_scheduler_logs\n\n# 任务定义表名\ntaskTableName: hai_scheduler_tasks\n\n# 分布式锁表名\nlockTableName: hai_scheduler_locks\n\n# 检查间隔(毫秒)\ntickInterval: 1000\n\n# 锁过期时间(毫秒)\nlockExpireMs: 300000\n`\n}\n\nfunction generateAuditConfig(): string {\n return `# =============================================================================\n# 审计日志配置 (@h-ai/audit)\n# =============================================================================\n\n# 审计日志表名\ntableName: hai_audit_logs\n\n# 用户表名(用于 JOIN 查询)\nuserTable: hai_iam_users\n\n# 用户表主键列名\nuserIdColumn: id\n\n# 用户名列名\nuserNameColumn: username\n`\n}\n\nfunction generatePaymentConfig(): string {\n return `# =============================================================================\n# 支付配置 (@h-ai/payment)\n# =============================================================================\n\n# 微信支付(按需开启)\n# wechat:\n# mchId: ''\n# apiV3Key: ''\n# serialNo: ''\n# privateKey: ''\n# appId: ''\n\n# 支付宝(按需开启)\n# alipay:\n# appId: ''\n# privateKey: ''\n# alipayPublicKey: ''\n# signType: RSA2\n# sandbox: false\n\n# Stripe(按需开启)\n# stripe:\n# secretKey: ''\n# webhookSecret: ''\n`\n}\n"]}
|
|
@@ -1,194 +0,0 @@
|
|
|
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}}
|