refactor: 将 n8n 工作流外部化并简化数据库 schema
- 移除 n8n-workflows 目录下的工作流配置文件 - 从 Project 模型移除 embedding 和 embeddingUpdatedAt 字段 - 添加 N8N_AI_SEARCH_WEBHOOK 环境变量配置 Co-Authored-By: Claude (glm-4.7) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,9 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/agent_park"
|
||||
# Webhook API - Generate a secure key for production
|
||||
WEBHOOK_API_KEY="sk_live_your_secure_api_key_min_32_chars"
|
||||
|
||||
# n8n AI Search Webhook
|
||||
N8N_AI_SEARCH_WEBHOOK="https://n8n.mzaxd.fun/webhook/ai-search"
|
||||
|
||||
# Internationalization
|
||||
NEXT_INTL_DEFAULT_LOCALE="zh"
|
||||
NEXT_INTL_SUPPORTED_LOCALES="zh,en"
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
# n8n 工作流导入指南
|
||||
|
||||
本指南将帮助您导入和配置 AI 智能搜索系统的两个 n8n 工作流。
|
||||
|
||||
## 前置条件
|
||||
|
||||
确保您已经完成:
|
||||
- [ ] n8n 实例已运行
|
||||
- [ ] 已配置 `OpenAI Embeddings` 凭证
|
||||
- [ ] 已配置 `Neon Database` 凭证
|
||||
- [ ] Neon 数据库已应用迁移(添加 embedding 字段)
|
||||
|
||||
---
|
||||
|
||||
## 工作流 1: Project Vectorization(项目向量化)
|
||||
|
||||
### 功能说明
|
||||
每 5 分钟自动执行一次,查询未向量化的项目,生成 OpenAI embeddings 并存储到数据库。
|
||||
|
||||
### 导入步骤
|
||||
|
||||
1. **导入工作流**
|
||||
- 打开 n8n 实例
|
||||
- 点击右上角 **+** → **Import from File**
|
||||
- 选择 `project-vectorization.json`
|
||||
- 点击 **Import**
|
||||
|
||||
2. **配置凭证**
|
||||
- 点击 **查询未向量化项目** 节点
|
||||
- 在 **Credentials** 下拉框中选择 `Neon Database`
|
||||
- 点击 **Save**
|
||||
|
||||
- 点击 **OpenAI Embeddings** 节点
|
||||
- 在 **Credentials** 下拉框中选择 `OpenAI Embeddings`
|
||||
- 点击 **Save**
|
||||
|
||||
- 点击 **更新 Embedding** 节点
|
||||
- 在 **Credentials** 下拉框中选择 `Neon Database`
|
||||
- 点击 **Save**
|
||||
|
||||
3. **测试工作流**
|
||||
- 点击工作流右上角 **Test Workflow**
|
||||
- 手动点击 **Cron** 节点的执行按钮
|
||||
- 查看每个节点的输出:
|
||||
- `查询未向量化项目` 应返回项目列表(或空数组)
|
||||
- `构造文本内容` 应添加 `textContent` 字段
|
||||
- `OpenAI Embeddings` 应返回向量数组
|
||||
- `更新 Embedding` 应成功更新数据库
|
||||
|
||||
4. **激活工作流**
|
||||
- 点击左上角 **Inactive** 开关,变为 **Active**
|
||||
- 工作流将每 5 分钟自动执行
|
||||
|
||||
### 节点说明
|
||||
|
||||
| 节点 | 功能 |
|
||||
|------|------|
|
||||
| Cron | 定时触发器(每 5 分钟) |
|
||||
| 查询未向量化项目 | 查询 embedding 为空的 ACTIVE 项目 |
|
||||
| 构造文本内容 | 合并项目字段生成用于向量化的文本 |
|
||||
| Split in Batches | 分批处理(每批 5 个,避免 API 限流) |
|
||||
| OpenAI Embeddings | 调用 OpenAI API 生成向量 |
|
||||
| 更新 Embedding | 将向量写入数据库 |
|
||||
|
||||
---
|
||||
|
||||
## 工作流 2: AI Semantic Search(AI 语义搜索)
|
||||
|
||||
### 功能说明
|
||||
接收 Webhook 请求,生成查询向量,执行向量相似度搜索,返回排序结果。
|
||||
|
||||
### 导入步骤
|
||||
|
||||
1. **导入工作流**
|
||||
- 打开 n8n 实例
|
||||
- 点击右上角 **+** → **Import from File**
|
||||
- 选择 `ai-semantic-search.json`
|
||||
- 点击 **Import**
|
||||
|
||||
2. **配置凭证**
|
||||
- 依次配置以下节点的凭证为 `OpenAI Embeddings`:
|
||||
- **生成查询向量** 节点
|
||||
|
||||
- 依次配置以下节点的凭证为 `Neon Database`:
|
||||
- **向量相似度搜索** 节点
|
||||
- **查询标签** 节点
|
||||
|
||||
3. **获取 Webhook URL**
|
||||
- 点击 **Webhook** 节点
|
||||
- 复制 **Production URL**(格式类似:`https://your-n8n.com/webhook/ai-search`)
|
||||
- 将此 URL 更新到 `.env.local` 的 `N8N_AI_SEARCH_WEBHOOK`
|
||||
|
||||
4. **测试工作流**
|
||||
- 点击工作流右上角 **Test Workflow**
|
||||
- 在 **Webhook** 节点中点击 **Listen for Test Event**
|
||||
- 使用以下命令测试:
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-n8n.com/webhook/ai-search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"视频生成工具","locale":"zh","limit":5}'
|
||||
```
|
||||
|
||||
- 预期响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"project": { /* 项目数据 */ },
|
||||
"similarity": 0.89,
|
||||
"matchReason": "相似度: 89%"
|
||||
}
|
||||
],
|
||||
"total": 5,
|
||||
"searchTime": 1234
|
||||
}
|
||||
```
|
||||
|
||||
5. **激活工作流**
|
||||
- 点击左上角 **Inactive** 开关,变为 **Active**
|
||||
|
||||
### 节点说明
|
||||
|
||||
| 节点 | 功能 |
|
||||
|------|------|
|
||||
| Webhook | 接收搜索请求(POST /webhook/ai-search) |
|
||||
| 生成查询向量 | 将查询文本转换为向量 |
|
||||
| 向量相似度搜索 | 使用 pgvector 执行余弦相似度搜索 |
|
||||
| 准备标签查询 | 准备项目 ID 列表 |
|
||||
| 查询标签 | 查询每个项目的标签 |
|
||||
| 合并标签 | 将标签合并到搜索结果 |
|
||||
| 格式化响应 | 生成最终的 JSON 响应 |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 节点连接错误?
|
||||
导入后如果节点连接线丢失,手动按以下顺序连接:
|
||||
|
||||
**工作流 1 连接顺序:**
|
||||
```
|
||||
Cron → 查询未向量化项目 → 构造文本内容 → Split in Batches → OpenAI Embeddings → 更新 Embedding → (循环回) Split in Batches
|
||||
```
|
||||
|
||||
**工作流 2 连接顺序:**
|
||||
```
|
||||
Webhook → 生成查询向量 → 向量相似度搜索 → 准备标签查询 → 查询标签 → 合并标签 → 格式化响应
|
||||
```
|
||||
|
||||
### Q2: 凭证选择框为空?
|
||||
- 确保已在 n8n 中创建了 `OpenAI Embeddings` 和 `Neon Database` 凭证
|
||||
- 如果凭证已创建但不可见,重新导入工作流
|
||||
|
||||
### Q3: OpenAI API 错误?
|
||||
- 检查 API Key 是否有效
|
||||
- 确认 API Key 有足够的配额
|
||||
- 检查网络连接
|
||||
|
||||
### Q4: 数据库连接错误?
|
||||
- 验证 Neon 数据库凭证配置正确
|
||||
- 检查数据库是否已应用迁移
|
||||
- 确认 pgvector 扩展已安装
|
||||
|
||||
---
|
||||
|
||||
## 更新环境变量
|
||||
|
||||
将获取的 Webhook URL 更新到项目的 `.env.local` 文件:
|
||||
|
||||
```bash
|
||||
# n8n AI Search Webhook
|
||||
N8N_AI_SEARCH_WEBHOOK="https://your-n8n.com/webhook/ai-search"
|
||||
```
|
||||
|
||||
然后重启开发服务器:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证完整流程
|
||||
|
||||
1. **启动向量化**
|
||||
- 确保 Project Vectorization 工作流已激活
|
||||
- 等待 5 分钟或手动执行
|
||||
- 在 Neon SQL Editor 中检查:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*) FROM "projects" WHERE "embedding" IS NOT NULL;
|
||||
```
|
||||
|
||||
2. **测试 AI 搜索**
|
||||
- 访问 `http://localhost:3000/zh/projects`
|
||||
- 点击 ✨ 按钮切换到 AI 模式
|
||||
- 输入查询:"帮我找能生成视频的 AI 工具"
|
||||
- 验证返回相关结果
|
||||
|
||||
---
|
||||
|
||||
## 完成后
|
||||
|
||||
所有工作流配置完成后,您的 AI 智能搜索系统就已就绪!
|
||||
|
||||
- 向量化工作流会在后台自动运行
|
||||
- AI 搜索 API 可供前端调用
|
||||
- 用户可以使用自然语言查询项目
|
||||
@@ -1,180 +0,0 @@
|
||||
{
|
||||
"name": "AI Semantic Search",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "ai-search",
|
||||
"responseMode": "whenLastNodeFinishes",
|
||||
"options": {}
|
||||
},
|
||||
"id": "webhook-node",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [250, 300],
|
||||
"webhookId": "ai-search-webhook"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "embedding",
|
||||
"model": "text-embedding-3-small",
|
||||
"input": "={{ $json.query }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "openai-embeddings",
|
||||
"name": "生成查询向量",
|
||||
"type": "@n8n/n8n-nodes-langchain.openai",
|
||||
"typeVersion": 1.4,
|
||||
"position": [470, 300],
|
||||
"credentials": {
|
||||
"openaiApi": {
|
||||
"id": "OPENAI_CREDENTIAL_ID",
|
||||
"name": "OpenAI Embeddings"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "=SELECT\n p.id,\n p.name,\n p.\"nameEn\",\n p.slug,\n p.description,\n p.\"descriptionEn\",\n p.status,\n p.\"createdAt\",\n 1 - (p.\"embedding\" <=> '{{ $json.data[0].embedding }}'::vector) as similarity\nFROM \"projects\" p\nWHERE p.\"embedding\" IS NOT NULL\n AND p.status = 'ACTIVE'\nORDER BY p.\"embedding\" <=> '{{ $json.data[0].embedding }}'::vector\nLIMIT {{ $json.limit || 20 }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "postgres-vector-search",
|
||||
"name": "向量相似度搜索",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [690, 300],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "NEON_DATABASE_CREDENTIAL_ID",
|
||||
"name": "Neon Database"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// 为每个项目查询标签\nconst results = $input.all();\n\nreturn results.map(item => {\n return {\n json: {\n ...item.json,\n projectId: item.json.id\n }\n };\n});"
|
||||
},
|
||||
"id": "prepare-tag-query",
|
||||
"name": "准备标签查询",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [910, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "=SELECT\n t.id,\n t.name,\n t.\"nameEn\",\n t.slug,\n pt.\"projectId\"\nFROM \"tags\" t\nINNER JOIN \"project_tags\" pt ON t.id = pt.\"tagId\"\nWHERE pt.\"projectId\" IN (SELECT UNNEST(STRING_TO_ARRAY('{{ $json.projectIds }}', ','))::INTEGER)",
|
||||
"options": {}
|
||||
},
|
||||
"id": "query-tags",
|
||||
"name": "查询标签",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [1130, 300],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "NEON_DATABASE_CREDENTIAL_ID",
|
||||
"name": "Neon Database"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// 合并标签到项目\nconst projects = $('向量相似度搜索').all();\nconst tags = $('查询标签').all();\n\n// 合并标签到项目\nconst results = projects.map(project => {\n const projectTags = tags\n .filter(t => t.json.projectId === project.json.id)\n .map(t => ({\n id: t.json.id,\n name: t.json.name,\n nameEn: t.json.nameEn,\n slug: t.json.slug\n }));\n\n return {\n json: {\n project: {\n ...project.json,\n tags: projectTags\n },\n similarity: project.json.similarity,\n matchReason: `相似度: ${(project.json.similarity * 100).toFixed(0)}%`\n }\n };\n});\n\nreturn results;"
|
||||
},
|
||||
"id": "merge-tags",
|
||||
"name": "合并标签",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1350, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// 格式化最终响应\nconst results = $input.all();\n\nreturn {\n json: {\n results: results.map(r => r.json),\n total: results.length,\n searchTime: Date.now() - $('Webhook').item.json.startTime\n }\n};"
|
||||
},
|
||||
"id": "format-response",
|
||||
"name": "格式化响应",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1570, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "生成查询向量",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"生成查询向量": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "向量相似度搜索",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"向量相似度搜索": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "准备标签查询",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"准备标签查询": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "查询标签",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"查询标签": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "合并标签",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"合并标签": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "格式化响应",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": [],
|
||||
"triggerCount": 0,
|
||||
"updatedAt": "2026-01-26T00:00:00.000Z",
|
||||
"versionId": "1"
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
{
|
||||
"name": "Project Vectorization",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [
|
||||
{
|
||||
"field": "minutes",
|
||||
"minutesInterval": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "cron-node",
|
||||
"name": "Cron",
|
||||
"type": "n8n-nodes-base.cron",
|
||||
"typeVersion": 1.2,
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "SELECT id, name, \"nameEn\", description, \"descriptionEn\", content, \"contentEn\"\nFROM \"projects\"\nWHERE \"embedding\" IS NULL\n AND \"status\" = 'ACTIVE'\nLIMIT 20",
|
||||
"options": {}
|
||||
},
|
||||
"id": "postgres-query",
|
||||
"name": "查询未向量化项目",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [470, 300],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "NEON_DATABASE_CREDENTIAL_ID",
|
||||
"name": "Neon Database"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// 为每个项目构造用于向量化的文本内容\nconst projects = $input.all();\n\nreturn projects.map(item => {\n const project = item.json;\n\n // 合并字段,按权重构造\n const parts = [\n project.name || '',\n project.nameEn || '',\n project.description || '',\n project.descriptionEn || '',\n (project.content || '').substring(0, 500),\n (project.contentEn || '').substring(0, 500)\n ].filter(Boolean);\n\n const textContent = parts.join('\\n\\n');\n\n return {\n json: {\n ...project,\n textContent: textContent\n }\n };\n});"
|
||||
},
|
||||
"id": "construct-content",
|
||||
"name": "构造文本内容",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [690, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"batchSize": 5,
|
||||
"options": {}
|
||||
},
|
||||
"id": "split-batches",
|
||||
"name": "Split in Batches",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"position": [910, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "embedding",
|
||||
"model": "text-embedding-3-small",
|
||||
"input": "={{ $json.textContent }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "openai-embeddings",
|
||||
"name": "OpenAI Embeddings",
|
||||
"type": "@n8n/n8n-nodes-langchain.openai",
|
||||
"typeVersion": 1.4,
|
||||
"position": [1130, 300],
|
||||
"credentials": {
|
||||
"openaiApi": {
|
||||
"id": "OPENAI_CREDENTIAL_ID",
|
||||
"name": "OpenAI Embeddings"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "=UPDATE \"projects\"\nSET\n \"embedding\" = '{{ $json.data[0].embedding }}'::vector,\n \"embeddingUpdatedAt\" = NOW()\nWHERE \"id\" = {{ $json.id }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "postgres-update",
|
||||
"name": "更新 Embedding",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [1350, 300],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "NEON_DATABASE_CREDENTIAL_ID",
|
||||
"name": "Neon Database"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Cron": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "查询未向量化项目",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"查询未向量化项目": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "构造文本内容",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"构造文本内容": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split in Batches",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Split in Batches": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "OpenAI Embeddings",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"OpenAI Embeddings": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "更新 Embedding",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"更新 Embedding": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split in Batches",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": [],
|
||||
"triggerCount": 0,
|
||||
"updatedAt": "2026-01-26T00:00:00.000Z",
|
||||
"versionId": "1"
|
||||
}
|
||||
+15
-17
@@ -39,25 +39,23 @@ enum TaskStatus {
|
||||
// ================================
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
description String
|
||||
descriptionEn String?
|
||||
content String? @db.Text
|
||||
contentEn String? @db.Text
|
||||
status ProjectStatus @default(ACTIVE)
|
||||
source String?
|
||||
embedding Unsupported("vector(1536)")?
|
||||
embeddingUpdatedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
description String
|
||||
descriptionEn String?
|
||||
content String? @db.Text
|
||||
contentEn String? @db.Text
|
||||
status ProjectStatus @default(ACTIVE)
|
||||
source String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
tags ProjectTag[]
|
||||
links ExternalLink[]
|
||||
discoveryTasks ProjectDiscoveryTask[]
|
||||
tags ProjectTag[]
|
||||
links ExternalLink[]
|
||||
discoveryTasks ProjectDiscoveryTask[]
|
||||
|
||||
// Indexes
|
||||
@@index([status, createdAt], map: "idx_project_status_createdAt")
|
||||
|
||||
Reference in New Issue
Block a user