| name | hai-app-create |
| description | 以 TDD 方式在应用中创建新功能:先编写测试定义行为,再编码实现直至测试通过;当需求涉及新增页面/路由、API、服务、数据模型、组件时使用。 |
hai-app-create — TDD 驱动的应用功能创建规范
能力契约
| 项目 | 契约 |
|---|
| 能力 | 以 TDD 方式在应用中创建新功能:先编写测试定义行为,再编码实现直至测试通过;当需求涉及新增页面/路由、API、服务、数据模型、组件时使用。 |
| 适用场景 | 当任务与 hai-app-create 的能力描述匹配,并且需要遵循本 Skill 的流程和边界时 |
| 输入 | 明确的功能需求、目标路径、现有实现、仓库规范与验收条件 |
| 输出 | 最小必要的代码、类型、测试和同步文档,以及实际验证结果 |
| 限制 | 不为假设需求增加抽象,不绕过生命周期/HaiResult/i18n 约定,不覆盖用户已有改动 |
面向 AI 助手的应用功能创建指南。必须遵循 TDD:先写测试(Red)→ 再实现(Green)→ 再重构(Refactor)。
若目标是纯 API service workspace,优先围绕 apps/*-contract、apps/*-service 与测试组织改动,而不是套用页面/路由脚手架。
适用场景
- 新增页面(路由)
- 新增 API 端点
- 新增服务层模块(
$lib/server/services/)
- 新增数据库表 / 数据模型
- 新增业务组件
TDD 开发流程(强制)
创建任何新功能时,必须按以下顺序执行:
Step 1:需求分析与测试设计
- 明确功能的输入、输出、边界条件、错误场景
- 确定需要创建的文件(服务、路由、Schema、组件等)
- 设计测试用例清单(参照
hai-app-tests 技能的覆盖范围要求)
Step 2:编写测试(Red 阶段)
- 创建单元测试:在
tests/ 下为服务层、Schema 编写 Vitest 测试
- 创建 E2E 测试:在
e2e/ 下为 API 端点、页面交互编写 Playwright 测试
- 创建空的实现文件:仅包含函数签名和类型定义,不写实际逻辑(确保 import 不报错)
- 运行测试确认全部失败:
pnpm --filter <app-name> test
Step 3:编写实现(Green 阶段)
按照下文的「创建指南」编写实际功能代码,每完成一个功能点立即运行测试确认通过。
Step 4:重构与验证(Refactor 阶段)
- 参照
hai-app-review 审查代码规范
- 运行质量门禁:
pnpm typecheck && pnpm lint && pnpm test
- 运行 E2E:
pnpm --filter <app-name> test:e2e
目录结构约定
src/
hooks.server.ts # `handle` hook 入口
app.css # 全局样式(TailwindCSS 4)
lib/
server/
init.ts # 应用初始化(模块 init 顺序)
services/ # 业务服务层(Server-only)
index.ts # 服务聚合导出
user.ts # 示例:用户服务
schemas/ # 请求校验 Schema(Zod)
components/ # 业务组件(非 @h-ai/ui 通用组件)
stores/ # 客户端 Store(Svelte 5 Runes)
paraglide/ # i18n 生成文件(禁止手动修改)
routes/
+layout.svelte # 根布局
+page.svelte # 首页
(auth)/ # 认证分组路由
login/+page.svelte
admin/ # 管理区域
users/
+page.svelte # 列表页
+page.server.ts # 列表数据加载
[id]/
+page.svelte # 详情页
+page.server.ts
api/ # API 端点
auth/+server.ts
health/+server.ts
iam/
users/+server.ts
messages/ # i18n 消息文件
en-US.json
zh-CN.json
config/ # 模块配置文件
_core.yml
_db.yml
_cache.yml
_iam.yml
创建页面
页面路由规则
使用 SvelteKit 文件路由,参考 src/routes/ 约定:
| 类型 | 路径示例 | 说明 |
|---|
| 静态页面 | routes/about/+page.svelte | 常规页面 |
| 动态路由 | routes/users/[id]/+page.svelte | URL 参数 |
| 分组路由 | routes/(auth)/login/+page.svelte | 共享布局,不影响 URL |
| API 端点 | routes/api/users/+server.ts | RESTful API |
页面文件模板
<!-- +page.svelte -->
<script lang="ts">
// Svelte 5 Runes 语法
let { data } = $props()
let loading = $state(false)
async function handleAction() {
loading = true
try {
// 业务逻辑
} finally {
loading = false
}
}
</script>
<div class="container mx-auto p-4">
<!-- 使用 @h-ai/ui 组件 -->
</div>
数据加载(+page.server.ts)
import type { PageServerLoad } from './$types'
import { kit } from '@h-ai/kit'
export const load: PageServerLoad = async (event) => {
const guard = kit.guard.requireAuth(event)
if (!guard.success)
return kit.response.redirect('/login')
const result = await someService.list()
if (!result.success)
return kit.response.error(500, result.error.message)
return { items: result.data }
}
创建 API 端点
RESTful 端点模板
import type { RequestHandler } from './$types'
import { kit } from '@h-ai/kit'
import { z } from 'zod'
const CreateSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
})
export const GET = kit.handler(async ({ locals }) => {
kit.guard.requirePermission(locals.session, 'resource:read')
const result = await service.list()
if (!result.success)
return kit.response.error(500, result.error.message)
return kit.response.ok(result.data)
})
export const POST = kit.handler(async ({ request, locals }) => {
kit.guard.requirePermission(locals.session, 'resource:create')
const data = await kit.validate.formOrFail(request, CreateSchema)
const result = await service.create(data)
if (!result.success)
return kit.response.error(500, result.error.message)
return kit.response.created(result.data)
})
要点
- 所有端点必须用
kit.handler(async ({ locals, request, ... }) => { ... }) 包裹,由 handler 统一处理 throw 的 Response
- 权限守卫使用
kit.guard.requirePermission(locals.session, 'xxx:yyy'),它本身就是 throw 模式(未通过时 throw 403 Response),无需检查返回值
- 输入校验使用
await kit.validate.formOrFail(request, Schema),校验失败时 throw 400 Response,无需手动判断 valid
- 返回统一使用
kit.response.*
- HaiResult 错误直接透传,不重新包装
- 框架模块 API 不抛异常,不要用
try/catch 处理模块返回的错误,直接检查 result.success
创建服务层
服务层位于 $lib/server/services/,处理业务逻辑。
import type { HaiResult } from '@h-ai/core'
import { core } from '@h-ai/core'
import { reldb } from '@h-ai/reldb'
const logger = core.logger
export async function createArticle(input: CreateArticleInput): Promise<HaiResult<Article>> {
logger.debug('Creating article', { title: input.title })
const result = await reldb.crud.create('articles', {
id: crypto.randomUUID(),
...input,
created_at: new Date().toISOString(),
})
if (!result.success)
return result
logger.info('Article created', { id: result.data.id })
return result
}
export async function listArticles(params: ListParams): Promise<HaiResult<PaginatedResult<Article>>> {
logger.debug('Listing articles', { page: params.page })
return reldb.crud.paginate('articles', params)
}
服务层规则
- 统一返回
HaiResult<T>
- 写操作需
debug(进入)+ info(成功)日志
- 读操作仅
debug 日志
- 禁止
console.log,使用 core.logger
- 服务层不处理 HTTP 响应格式,只返回 HaiResult
创建数据模型
Schema 定义(SQL DDL)
数据库表 Schema 定义在 $lib/server/init.ts 中:
const BUSINESS_SCHEMA = `
CREATE TABLE IF NOT EXISTS articles (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
author_id TEXT NOT NULL,
status TEXT DEFAULT 'draft',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_articles_author_id ON articles(author_id);
CREATE INDEX IF NOT EXISTS idx_articles_status ON articles(status);
`
类型定义
interface Article {
id: string
title: string
content: string | null
authorId: string
status: 'draft' | 'published' | 'archived'
createdAt: string
updatedAt: string
}
interface CreateArticleInput {
title: string
content?: string
authorId: string
}
创建业务组件
业务组件放在 $lib/components/,通用 UI 使用 @h-ai/ui。
<!-- src/lib/components/ArticleCard.svelte -->
<script lang="ts">
import type { Article } from '$lib/server/services/article'
let { article, onEdit }: { article: Article, onEdit?: () => void } = $props()
</script>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title">{article.title}</h2>
<p>{article.content ?? ''}</p>
{#if onEdit}
<div class="card-actions justify-end">
<button class="btn btn-primary" onclick={onEdit}>编辑</button>
</div>
{/if}
</div>
</div>
组件规则
- 使用 Svelte 5 Runes 语法(
$props()、$state()、$derived()、$effect())
- 事件回调通过 props 传入(非
createEventDispatcher)
- 样式使用 TailwindCSS 4 + DaisyUI 5 class
- 通用 UI 组件优先使用
@h-ai/ui,不重复实现
- i18n 文本使用
$lib/paraglide/messages.js 中的 key
初始化顺序
在 $lib/server/init.ts 中管理模块初始化顺序:
core.init() → reldb.init() → cache.init() → iam.init() → createBusinessTables()
新增模块时需在此文件中按依赖顺序添加初始化调用。
检查清单
相关 Skills
hai-build:项目架构与模块初始化顺序
hai-app-tests:TDD 测试规范(Red 阶段详细指引)
hai-app-review:代码审查规范(Refactor 阶段参照)
hai-kit:SvelteKit 集成的完整 API
hai-ui:UI 组件库使用
hai-reldb:数据库操作详细 API
hai-iam:认证与权限管理