Compare commits
No commits in common. "b4af88e730d20a23f79fa7e625c1140c0420e9da" and "b6022e824d4fd42ef14fe98d16c1df2537470f77" have entirely different histories.
b4af88e730
...
b6022e824d
43
.github/pull_request_template.md
vendored
43
.github/pull_request_template.md
vendored
@ -1,43 +0,0 @@
|
|||||||
## Description
|
|
||||||
|
|
||||||
<!-- What does this PR do? What problem does it solve? -->
|
|
||||||
|
|
||||||
## Type of Change
|
|
||||||
|
|
||||||
- [ ] **feat** — New feature
|
|
||||||
- [ ] **fix** — Bug fix
|
|
||||||
- [ ] **docs** — Documentation changes only
|
|
||||||
- [ ] **refactor** — Code refactoring (no functional change)
|
|
||||||
- [ ] **perf** — Performance improvement
|
|
||||||
- [ ] **test** — Adding or updating tests
|
|
||||||
- [ ] **chore** — Build process, auxiliary tools, or CI/CD
|
|
||||||
|
|
||||||
## Related Issue
|
|
||||||
|
|
||||||
<!-- Link to the related issue, e.g., "Closes #123", "Fixes #456" -->
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
<!-- How has this been tested? What cases does it cover? -->
|
|
||||||
|
|
||||||
- [ ] Unit tests added/updated
|
|
||||||
- [ ] Integration tests added/updated
|
|
||||||
- [ ] Manual testing performed
|
|
||||||
|
|
||||||
## Screenshots (if applicable)
|
|
||||||
|
|
||||||
<!-- For UI changes, include before/after screenshots -->
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] Code follows the project's coding conventions
|
|
||||||
- [ ] Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/)
|
|
||||||
- [ ] Self-review completed
|
|
||||||
- [ ] Comments added for complex logic
|
|
||||||
- [ ] Documentation updated (if needed)
|
|
||||||
- [ ] No new warnings or errors
|
|
||||||
- [ ] Breaking changes clearly documented
|
|
||||||
|
|
||||||
## Additional Notes
|
|
||||||
|
|
||||||
<!-- Any other context reviewers should know about -->
|
|
||||||
96
.github/workflows/ci.yml
vendored
96
.github/workflows/ci.yml
vendored
@ -1,96 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, develop ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main, develop ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rust-check:
|
|
||||||
name: Rust Lint & Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust@stable
|
|
||||||
with:
|
|
||||||
components: clippy, rustfmt
|
|
||||||
|
|
||||||
- name: Cache cargo
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: apps/* libs/*
|
|
||||||
|
|
||||||
- name: Check formatting
|
|
||||||
run: cargo fmt --workspace -- --check
|
|
||||||
|
|
||||||
- name: Run clippy
|
|
||||||
run: cargo clippy --workspace -- -D warnings
|
|
||||||
|
|
||||||
rust-test:
|
|
||||||
name: Rust Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust@stable
|
|
||||||
|
|
||||||
- name: Cache cargo
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: apps/* libs/*
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --workspace
|
|
||||||
|
|
||||||
frontend-check:
|
|
||||||
name: Frontend Lint & Type Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: ESLint
|
|
||||||
run: pnpm lint
|
|
||||||
|
|
||||||
- name: Type check
|
|
||||||
run: pnpm tsc --noEmit
|
|
||||||
|
|
||||||
frontend-build:
|
|
||||||
name: Frontend Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: frontend-check
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Upload dist
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dist
|
|
||||||
path: dist/
|
|
||||||
@ -59,7 +59,7 @@ actix-csrf = "0.8.0"
|
|||||||
actix-rt = "2.11.0"
|
actix-rt = "2.11.0"
|
||||||
actix = "0.13"
|
actix = "0.13"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
|
async-nats = "0.47.0"
|
||||||
actix-service = "2.0.3"
|
actix-service = "2.0.3"
|
||||||
actix-utils = "3.0.1"
|
actix-utils = "3.0.1"
|
||||||
redis = "1.1.0"
|
redis = "1.1.0"
|
||||||
|
|||||||
77
README.md
77
README.md
@ -4,8 +4,7 @@
|
|||||||
|
|
||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
Code API 是一个全栈 monorepo 项目,采用 Rust 后端 + React 前端的技术栈。项目实现了类似 GitHub 的 Issue 追踪、Pull Request
|
Code API 是一个全栈 monorepo 项目,采用 Rust 后端 + React 前端的技术栈。项目实现了类似 GitHub 的 Issue 追踪、Pull Request 代码审查、Git 仓库管理,以及类似 Slack 的实时聊天 Room 功能。
|
||||||
代码审查、Git 仓库管理,以及类似 Slack 的实时聊天 Room 功能。
|
|
||||||
|
|
||||||
### 核心功能
|
### 核心功能
|
||||||
|
|
||||||
@ -20,33 +19,33 @@ Code API 是一个全栈 monorepo 项目,采用 Rust 后端 + React 前端的
|
|||||||
|
|
||||||
### 后端 (Rust)
|
### 后端 (Rust)
|
||||||
|
|
||||||
| 类别 | 技术 |
|
| 类别 | 技术 |
|
||||||
|--------|----------------------|
|
|------|------|
|
||||||
| 语言 | Rust 2024 Edition |
|
| 语言 | Rust 2024 Edition |
|
||||||
| Web 框架 | Actix-web |
|
| Web 框架 | Actix-web |
|
||||||
| ORM | SeaORM |
|
| ORM | SeaORM |
|
||||||
| 数据库 | PostgreSQL |
|
| 数据库 | PostgreSQL |
|
||||||
| 缓存 | Redis |
|
| 缓存 | Redis |
|
||||||
| 实时通信 | WebSocket (actix-ws) |
|
| 实时通信 | WebSocket (actix-ws) |
|
||||||
| 消息队列 | Redis |
|
| 消息队列 | NATS |
|
||||||
| 向量数据库 | Qdrant |
|
| 向量数据库 | Qdrant |
|
||||||
| Git 操作 | git2 / git2-ext |
|
| Git 操作 | git2 / git2-ext |
|
||||||
| 认证 | JWT + Session |
|
| 认证 | JWT + Session |
|
||||||
| API 文档 | utoipa (OpenAPI) |
|
| API 文档 | utoipa (OpenAPI) |
|
||||||
|
|
||||||
### 前端 (TypeScript/React)
|
### 前端 (TypeScript/React)
|
||||||
|
|
||||||
| 类别 | 技术 |
|
| 类别 | 技术 |
|
||||||
|----------|----------------------------|
|
|------|------|
|
||||||
| 语言 | TypeScript 5.9 |
|
| 语言 | TypeScript 5.9 |
|
||||||
| 框架 | React 19 |
|
| 框架 | React 19 |
|
||||||
| 路由 | React Router v7 |
|
| 路由 | React Router v7 |
|
||||||
| 构建工具 | Vite 8 + SWC |
|
| 构建工具 | Vite 8 + SWC |
|
||||||
| UI 组件 | shadcn/ui + Tailwind CSS 4 |
|
| UI 组件 | shadcn/ui + Tailwind CSS 4 |
|
||||||
| 状态管理 | TanStack Query |
|
| 状态管理 | TanStack Query |
|
||||||
| HTTP 客户端 | Axios + OpenAPI 生成 |
|
| HTTP 客户端 | Axios + OpenAPI 生成 |
|
||||||
| Markdown | react-markdown + Shiki |
|
| Markdown | react-markdown + Shiki |
|
||||||
| 拖拽 | dnd-kit |
|
| 拖拽 | dnd-kit |
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@ -136,8 +135,8 @@ code/
|
|||||||
```
|
```
|
||||||
|
|
||||||
7. **访问应用**
|
7. **访问应用**
|
||||||
- 前端: http://localhost:5173
|
- 前端: http://localhost:5173
|
||||||
- 后端 API: http://localhost:8080
|
- 后端 API: http://localhost:8080
|
||||||
|
|
||||||
## 开发指南
|
## 开发指南
|
||||||
|
|
||||||
@ -193,21 +192,21 @@ cargo run -p migrate
|
|||||||
|
|
||||||
### 必需配置项
|
### 必需配置项
|
||||||
|
|
||||||
| 变量名 | 说明 | 示例 |
|
| 变量名 | 说明 | 示例 |
|
||||||
|--------------------|---------------|---------------------------------------|
|
|--------|------|------|
|
||||||
| `APP_DATABASE_URL` | PostgreSQL 连接 | `postgresql://user:pass@localhost/db` |
|
| `APP_DATABASE_URL` | PostgreSQL 连接 | `postgresql://user:pass@localhost/db` |
|
||||||
| `APP_REDIS_URL` | Redis 连接 | `redis://localhost:6379` |
|
| `APP_REDIS_URL` | Redis 连接 | `redis://localhost:6379` |
|
||||||
| `APP_AI_API_KEY` | AI 服务 API Key | `sk-xxxxx` |
|
| `APP_AI_API_KEY` | AI 服务 API Key | `sk-xxxxx` |
|
||||||
| `APP_SMTP_*` | SMTP 邮件配置 | 见 `.env.example` |
|
| `APP_SMTP_*` | SMTP 邮件配置 | 见 `.env.example` |
|
||||||
|
|
||||||
### 可选配置项
|
### 可选配置项
|
||||||
|
|
||||||
| 变量名 | 默认值 | 说明 |
|
| 变量名 | 默认值 | 说明 |
|
||||||
|--------------------------------|-------------|------------|
|
|--------|--------|------|
|
||||||
| `APP_DATABASE_MAX_CONNECTIONS` | 10 | 数据库连接池大小 |
|
| `APP_DATABASE_MAX_CONNECTIONS` | 10 | 数据库连接池大小 |
|
||||||
| `APP_LOG_LEVEL` | info | 日志级别 |
|
| `APP_LOG_LEVEL` | info | 日志级别 |
|
||||||
| `APP_QDRANT_URL` | - | 向量数据库地址 |
|
| `APP_QDRANT_URL` | - | 向量数据库地址 |
|
||||||
| `APP_REPOS_ROOT` | /data/repos | Git 仓库存储路径 |
|
| `APP_REPOS_ROOT` | /data/repos | Git 仓库存储路径 |
|
||||||
|
|
||||||
完整配置请参考 `.env.example`。
|
完整配置请参考 `.env.example`。
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,6 @@ data:
|
|||||||
APP_DATABASE_HEALTH_CHECK_INTERVAL: "30"
|
APP_DATABASE_HEALTH_CHECK_INTERVAL: "30"
|
||||||
APP_DATABASE_RETRY_ATTEMPTS: "3"
|
APP_DATABASE_RETRY_ATTEMPTS: "3"
|
||||||
APP_DATABASE_RETRY_DELAY: "1"
|
APP_DATABASE_RETRY_DELAY: "1"
|
||||||
APP_SESSION_SECRET: "571e53736a57f16c870b755e80dace66f7fd7f8a788c9aeaff879e5dc3c3d79a"
|
|
||||||
APP_REDIS_URL: "redis://default:redis123@valkey-cluster.valkey-cluster.svc.cluster.local:6379"
|
APP_REDIS_URL: "redis://default:redis123@valkey-cluster.valkey-cluster.svc.cluster.local:6379"
|
||||||
APP_REDIS_POOL_SIZE: "16"
|
APP_REDIS_POOL_SIZE: "16"
|
||||||
APP_REDIS_CONNECT_TIMEOUT: "5"
|
APP_REDIS_CONNECT_TIMEOUT: "5"
|
||||||
|
|||||||
@ -1,127 +0,0 @@
|
|||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
|
||||||
name: apps.code.dev
|
|
||||||
annotations:
|
|
||||||
controller-gen.kubebuilder.io/version: v0.16.0
|
|
||||||
spec:
|
|
||||||
group: code.dev
|
|
||||||
names:
|
|
||||||
kind: App
|
|
||||||
listKind: AppList
|
|
||||||
plural: apps
|
|
||||||
singular: app
|
|
||||||
shortNames:
|
|
||||||
- app
|
|
||||||
scope: Namespaced
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
||||||
additionalPrinterColumns:
|
|
||||||
- name: Replicas
|
|
||||||
jsonPath: .spec.replicas
|
|
||||||
type: integer
|
|
||||||
- name: Ready
|
|
||||||
jsonPath: .status.phase
|
|
||||||
type: string
|
|
||||||
- name: Age
|
|
||||||
jsonPath: .metadata.creationTimestamp
|
|
||||||
type: date
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
required: [spec]
|
|
||||||
properties:
|
|
||||||
apiVersion:
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
type: string
|
|
||||||
metadata:
|
|
||||||
type: object
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
required: []
|
|
||||||
properties:
|
|
||||||
image:
|
|
||||||
type: string
|
|
||||||
default: myapp/app:latest
|
|
||||||
replicas:
|
|
||||||
type: integer
|
|
||||||
default: 3
|
|
||||||
env:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
required: [name]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
valueFrom:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
secretRef:
|
|
||||||
type: object
|
|
||||||
required: [name, secretName, secretKey]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
secretName:
|
|
||||||
type: string
|
|
||||||
secretKey:
|
|
||||||
type: string
|
|
||||||
resources:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
requests:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cpu:
|
|
||||||
type: string
|
|
||||||
memory:
|
|
||||||
type: string
|
|
||||||
limits:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cpu:
|
|
||||||
type: string
|
|
||||||
memory:
|
|
||||||
type: string
|
|
||||||
livenessProbe:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
port:
|
|
||||||
type: integer
|
|
||||||
default: 8080
|
|
||||||
path:
|
|
||||||
type: string
|
|
||||||
default: /health
|
|
||||||
initialDelaySeconds:
|
|
||||||
type: integer
|
|
||||||
default: 5
|
|
||||||
readinessProbe:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
port:
|
|
||||||
type: integer
|
|
||||||
default: 8080
|
|
||||||
path:
|
|
||||||
type: string
|
|
||||||
default: /health
|
|
||||||
initialDelaySeconds:
|
|
||||||
type: integer
|
|
||||||
default: 5
|
|
||||||
imagePullPolicy:
|
|
||||||
type: string
|
|
||||||
default: IfNotPresent
|
|
||||||
status:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
readyReplicas:
|
|
||||||
type: integer
|
|
||||||
phase:
|
|
||||||
type: string
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
|
||||||
name: emailworkers.code.dev
|
|
||||||
annotations:
|
|
||||||
controller-gen.kubebuilder.io/version: v0.16.0
|
|
||||||
spec:
|
|
||||||
group: code.dev
|
|
||||||
names:
|
|
||||||
kind: EmailWorker
|
|
||||||
listKind: EmailWorkerList
|
|
||||||
plural: emailworkers
|
|
||||||
singular: emailworker
|
|
||||||
shortNames:
|
|
||||||
- ew
|
|
||||||
scope: Namespaced
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
||||||
additionalPrinterColumns:
|
|
||||||
- name: Age
|
|
||||||
jsonPath: .metadata.creationTimestamp
|
|
||||||
type: date
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
required: [spec]
|
|
||||||
properties:
|
|
||||||
apiVersion:
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
type: string
|
|
||||||
metadata:
|
|
||||||
type: object
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
required: []
|
|
||||||
properties:
|
|
||||||
image:
|
|
||||||
type: string
|
|
||||||
default: myapp/email-worker:latest
|
|
||||||
env:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
required: [name]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
valueFrom:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
secretRef:
|
|
||||||
type: object
|
|
||||||
required: [name, secretName, secretKey]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
secretName:
|
|
||||||
type: string
|
|
||||||
secretKey:
|
|
||||||
type: string
|
|
||||||
resources:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
requests:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cpu:
|
|
||||||
type: string
|
|
||||||
memory:
|
|
||||||
type: string
|
|
||||||
limits:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cpu:
|
|
||||||
type: string
|
|
||||||
memory:
|
|
||||||
type: string
|
|
||||||
imagePullPolicy:
|
|
||||||
type: string
|
|
||||||
default: IfNotPresent
|
|
||||||
status:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
readyReplicas:
|
|
||||||
type: integer
|
|
||||||
phase:
|
|
||||||
type: string
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
|
||||||
name: githooks.code.dev
|
|
||||||
annotations:
|
|
||||||
controller-gen.kubebuilder.io/version: v0.16.0
|
|
||||||
spec:
|
|
||||||
group: code.dev
|
|
||||||
names:
|
|
||||||
kind: GitHook
|
|
||||||
listKind: GitHookList
|
|
||||||
plural: githooks
|
|
||||||
singular: githook
|
|
||||||
shortNames:
|
|
||||||
- ghk
|
|
||||||
scope: Namespaced
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
||||||
additionalPrinterColumns:
|
|
||||||
- name: Age
|
|
||||||
jsonPath: .metadata.creationTimestamp
|
|
||||||
type: date
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
required: [spec]
|
|
||||||
properties:
|
|
||||||
apiVersion:
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
type: string
|
|
||||||
metadata:
|
|
||||||
type: object
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
required: []
|
|
||||||
properties:
|
|
||||||
image:
|
|
||||||
type: string
|
|
||||||
default: myapp/git-hook:latest
|
|
||||||
env:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
required: [name]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
valueFrom:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
secretRef:
|
|
||||||
type: object
|
|
||||||
required: [name, secretName, secretKey]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
secretName:
|
|
||||||
type: string
|
|
||||||
secretKey:
|
|
||||||
type: string
|
|
||||||
resources:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
requests:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cpu:
|
|
||||||
type: string
|
|
||||||
memory:
|
|
||||||
type: string
|
|
||||||
limits:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cpu:
|
|
||||||
type: string
|
|
||||||
memory:
|
|
||||||
type: string
|
|
||||||
imagePullPolicy:
|
|
||||||
type: string
|
|
||||||
default: IfNotPresent
|
|
||||||
workerId:
|
|
||||||
type: string
|
|
||||||
status:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
readyReplicas:
|
|
||||||
type: integer
|
|
||||||
phase:
|
|
||||||
type: string
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
|
||||||
name: gitservers.code.dev
|
|
||||||
annotations:
|
|
||||||
controller-gen.kubebuilder.io/version: v0.16.0
|
|
||||||
spec:
|
|
||||||
group: code.dev
|
|
||||||
names:
|
|
||||||
kind: GitServer
|
|
||||||
listKind: GitServerList
|
|
||||||
plural: gitservers
|
|
||||||
singular: gitserver
|
|
||||||
shortNames:
|
|
||||||
- gs
|
|
||||||
scope: Namespaced
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
||||||
additionalPrinterColumns:
|
|
||||||
- name: Age
|
|
||||||
jsonPath: .metadata.creationTimestamp
|
|
||||||
type: date
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
required: [spec]
|
|
||||||
properties:
|
|
||||||
apiVersion:
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
type: string
|
|
||||||
metadata:
|
|
||||||
type: object
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
required: []
|
|
||||||
properties:
|
|
||||||
image:
|
|
||||||
type: string
|
|
||||||
default: myapp/gitserver:latest
|
|
||||||
env:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
required: [name]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
valueFrom:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
secretRef:
|
|
||||||
type: object
|
|
||||||
required: [name, secretName, secretKey]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
secretName:
|
|
||||||
type: string
|
|
||||||
secretKey:
|
|
||||||
type: string
|
|
||||||
resources:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
requests:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cpu:
|
|
||||||
type: string
|
|
||||||
memory:
|
|
||||||
type: string
|
|
||||||
limits:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cpu:
|
|
||||||
type: string
|
|
||||||
memory:
|
|
||||||
type: string
|
|
||||||
sshServiceType:
|
|
||||||
type: string
|
|
||||||
default: NodePort
|
|
||||||
storageSize:
|
|
||||||
type: string
|
|
||||||
default: 10Gi
|
|
||||||
imagePullPolicy:
|
|
||||||
type: string
|
|
||||||
default: IfNotPresent
|
|
||||||
sshDomain:
|
|
||||||
type: string
|
|
||||||
sshPort:
|
|
||||||
type: integer
|
|
||||||
default: 22
|
|
||||||
httpPort:
|
|
||||||
type: integer
|
|
||||||
default: 8022
|
|
||||||
status:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
readyReplicas:
|
|
||||||
type: integer
|
|
||||||
phase:
|
|
||||||
type: string
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
|
||||||
name: migrates.code.dev
|
|
||||||
annotations:
|
|
||||||
controller-gen.kubebuilder.io/version: v0.16.0
|
|
||||||
spec:
|
|
||||||
group: code.dev
|
|
||||||
names:
|
|
||||||
kind: Migrate
|
|
||||||
listKind: MigrateList
|
|
||||||
plural: migrates
|
|
||||||
singular: migrate
|
|
||||||
shortNames:
|
|
||||||
- mig
|
|
||||||
scope: Namespaced
|
|
||||||
versions:
|
|
||||||
- name: v1
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
||||||
additionalPrinterColumns:
|
|
||||||
- name: Status
|
|
||||||
jsonPath: .status.phase
|
|
||||||
type: string
|
|
||||||
- name: Age
|
|
||||||
jsonPath: .metadata.creationTimestamp
|
|
||||||
type: date
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
required: [spec]
|
|
||||||
properties:
|
|
||||||
apiVersion:
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
type: string
|
|
||||||
metadata:
|
|
||||||
type: object
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
required: []
|
|
||||||
properties:
|
|
||||||
image:
|
|
||||||
type: string
|
|
||||||
default: myapp/migrate:latest
|
|
||||||
env:
|
|
||||||
type: array
|
|
||||||
description: "Must include APP_DATABASE_URL"
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
required: [name]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
valueFrom:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
secretRef:
|
|
||||||
type: object
|
|
||||||
required: [name, secretName, secretKey]
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
secretName:
|
|
||||||
type: string
|
|
||||||
secretKey:
|
|
||||||
type: string
|
|
||||||
command:
|
|
||||||
type: string
|
|
||||||
default: up
|
|
||||||
description: "Migration command: up, down, fresh, refresh, reset"
|
|
||||||
backoffLimit:
|
|
||||||
type: integer
|
|
||||||
default: 3
|
|
||||||
status:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
phase:
|
|
||||||
type: string
|
|
||||||
startTime:
|
|
||||||
type: string
|
|
||||||
completionTime:
|
|
||||||
type: string
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
# 分支保护规则配置
|
|
||||||
|
|
||||||
以下规则需要在 GitHub 仓库设置界面手动配置,或通过 Terraform/Ansible 等基础设施即代码工具自动化。
|
|
||||||
|
|
||||||
## main 分支保护规则
|
|
||||||
|
|
||||||
路径:**Settings → Branches → Branch protection rules → Add rule**
|
|
||||||
|
|
||||||
### 必填项
|
|
||||||
|
|
||||||
| 配置项 | 值 | 说明 |
|
|
||||||
|--------|-----|------|
|
|
||||||
| Branch name pattern | `main` | 匹配 main 分支 |
|
|
||||||
| Protect matching branches | ✅ 启用 | 开启分支保护 |
|
|
||||||
| **Require pull request reviews** | ✅ 要求 | 合并前至少 1 人 review |
|
|
||||||
| **Require approvals** | `1` | 最少审批数量 |
|
|
||||||
| Dismiss stale approvals | ✅ 启用 | PR 更新后需重新审批 |
|
|
||||||
| Require review from Code Owners | ☐ 可选 | 建议开启 |
|
|
||||||
| **Require status checks to pass before merging** | ✅ 要求 | 必须通过 CI |
|
|
||||||
| Required status checks | `rust-check`, `rust-test`, `frontend-check` | CI job 名称 |
|
|
||||||
| **Require branches to be up to date before merging** | ☐ 可选 | 建议不启用,避免复杂 |
|
|
||||||
| Do not allow bypassing the above settings | ✅ 启用 | 即使 admin 不能绕过 |
|
|
||||||
|
|
||||||
### 安全设置
|
|
||||||
|
|
||||||
| 配置项 | 值 |
|
|
||||||
|--------|-----|
|
|
||||||
| Lock branch | ☐ 可选(不勾选,CI 仍可推送) |
|
|
||||||
| Allow force pushes | ☐ 禁用(禁止 force push) |
|
|
||||||
| Allow deletions | ☐ 禁用(禁止删除分支) |
|
|
||||||
|
|
||||||
## develop 分支保护规则
|
|
||||||
|
|
||||||
路径:**Settings → Branches → Branch protection rules → Add rule**
|
|
||||||
|
|
||||||
| 配置项 | 值 |
|
|
||||||
|--------|-----|
|
|
||||||
| Branch name pattern | `develop` |
|
|
||||||
| Protect matching branches | ✅ 启用 |
|
|
||||||
| Require pull request reviews | ✅ 要求 |
|
|
||||||
| Require approvals | `1` |
|
|
||||||
| Dismiss stale approvals | ✅ 启用 |
|
|
||||||
| Require status checks to pass before merging | ✅ 要求 |
|
|
||||||
| Required status checks | `rust-check`, `rust-test`, `frontend-check` |
|
|
||||||
| Do not allow bypassing | ✅ 启用 |
|
|
||||||
| Allow force pushes | ☐ 禁用 |
|
|
||||||
|
|
||||||
## 自动清理已合并分支
|
|
||||||
|
|
||||||
建议安装 GitHub App [Branch Clean Up](https://github.com/apps/branch-cleanup) 或在 PR 合并后自动删除源分支:
|
|
||||||
|
|
||||||
- **Settings → General → Automatically delete head branches** → ✅ 启用
|
|
||||||
|
|
||||||
## 使用 GitHub CLI 配置(自动化)
|
|
||||||
|
|
||||||
如果需要通过代码自动化配置,可以使用 `gh` CLI:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 安装 gh
|
|
||||||
brew install gh
|
|
||||||
|
|
||||||
# 登录
|
|
||||||
gh auth login
|
|
||||||
|
|
||||||
# 创建 branch protection rule for main
|
|
||||||
gh api repos/{owner}/{repo}/branches/main/protection -X PUT \
|
|
||||||
-f required_status_checks='{"strict":true,"contexts":["rust-check","rust-test","frontend-check"]}' \
|
|
||||||
-f enforce_admins=true \
|
|
||||||
-f required_pull_request_reviews='{"required_approving_review_count":1,"dismiss_stale_reviews":true}' \
|
|
||||||
-f allow_force_pushes=false
|
|
||||||
```
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
# Commit Message 规范
|
|
||||||
|
|
||||||
本项目采用 [Conventional Commits](https://www.conventionalcommits.org/) 规范。
|
|
||||||
|
|
||||||
## 格式
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>(<scope>): <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer(s)]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 示例
|
|
||||||
|
|
||||||
```
|
|
||||||
feat(api): add workspace member invitation endpoint
|
|
||||||
fix(frontend): correct sidebar collapse state on resize
|
|
||||||
docs: update API documentation for /repos endpoints
|
|
||||||
refactor(service): extract auth logic into separate module
|
|
||||||
hotfix(k8s): add missing health probe for gitserver
|
|
||||||
```
|
|
||||||
|
|
||||||
## Type
|
|
||||||
|
|
||||||
| Type | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `feat` | 新功能 |
|
|
||||||
| `fix` | Bug 修复 |
|
|
||||||
| `docs` | 仅文档变更 |
|
|
||||||
| `style` | 代码格式(不影响功能) |
|
|
||||||
| `refactor` | 重构(不是修复也不是新功能) |
|
|
||||||
| `perf` | 性能优化 |
|
|
||||||
| `test` | 添加或修正测试 |
|
|
||||||
| `chore` | 构建或辅助工具变更 |
|
|
||||||
| `build` | 影响构建系统或依赖 |
|
|
||||||
| `ci` | CI/CD 配置 |
|
|
||||||
| `revert` | 回滚之前的提交 |
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
| Scope | 说明 |
|
|
||||||
|-------|------|
|
|
||||||
| `frontend` | React 前端 |
|
|
||||||
| `api` | REST API 路由 |
|
|
||||||
| `service` | 业务逻辑层 |
|
|
||||||
| `models` | 数据库模型 |
|
|
||||||
| `k8s` | Kubernetes 部署 |
|
|
||||||
| `deploy` | Docker/Helm 部署 |
|
|
||||||
| `git` | Git 仓库功能 |
|
|
||||||
| `room` | 实时聊天 |
|
|
||||||
| `agent` | AI Agent |
|
|
||||||
| `db` | 数据库迁移 |
|
|
||||||
|
|
||||||
## 规则
|
|
||||||
|
|
||||||
1. **每条提交应仅包含一个逻辑变更**
|
|
||||||
2. **description 首字母小写,使用祈使句**
|
|
||||||
3. **禁止句号结尾**
|
|
||||||
4. **Body 解释 what 和 why,不解释 how**
|
|
||||||
5. **Footer 引用相关 Issue**
|
|
||||||
|
|
||||||
## Git Alias
|
|
||||||
|
|
||||||
在 `~/.gitconfig` 中添加快捷别名:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[alias]
|
|
||||||
co = checkout
|
|
||||||
cm = commit -m
|
|
||||||
lg = log --oneline --graph --decorate
|
|
||||||
amend = commit --amend --no-edit
|
|
||||||
uncommit = reset --soft HEAD~1
|
|
||||||
```
|
|
||||||
|
|
||||||
或使用 commitlint 钩子自动验证(见 `.husky/commit-msg`)。
|
|
||||||
191
docs/GITFLOW.md
191
docs/GITFLOW.md
@ -1,191 +0,0 @@
|
|||||||
# Git Flow 分支策略
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本项目采用 [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) 分支模型,结合 GitHub Flow 的简洁性,适合本项目的发布节奏。
|
|
||||||
|
|
||||||
## 分支结构
|
|
||||||
|
|
||||||
```
|
|
||||||
main ← 生产环境,始终保持可发布状态
|
|
||||||
│
|
|
||||||
├── develop ← 开发集成分支,所有功能分支合并至此
|
|
||||||
│ │
|
|
||||||
│ ├── feature/xxx ← 功能分支,从 develop 创建
|
|
||||||
│ │
|
|
||||||
│ ├── release/0.3.0 ← 发布分支,从 develop 创建
|
|
||||||
│ │
|
|
||||||
│ └── hotfix/0.2.10 ← 热修复分支,从 main 创建
|
|
||||||
```
|
|
||||||
|
|
||||||
## 长期分支
|
|
||||||
|
|
||||||
| 分支 | 用途 | 保护状态 |
|
|
||||||
|------|------|----------|
|
|
||||||
| `main` | 生产环境代码 | 强制保护,禁止 force push |
|
|
||||||
| `develop` | 开发集成,所有功能最终汇入 | 建议保护 |
|
|
||||||
|
|
||||||
## 临时分支
|
|
||||||
|
|
||||||
| 前缀 | 用途 | 创建来源 | 合并目标 |
|
|
||||||
|------|------|----------|----------|
|
|
||||||
| `feature/` | 新功能开发 | `develop` | `develop` |
|
|
||||||
| `release/` | 发布准备 | `develop` | `main` + `develop` |
|
|
||||||
| `hotfix/` | 生产环境紧急修复 | `main` | `main` + `develop` |
|
|
||||||
|
|
||||||
## 工作流程
|
|
||||||
|
|
||||||
### 1. 功能开发 (feature)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 从 develop 创建功能分支
|
|
||||||
git checkout develop
|
|
||||||
git checkout -b feature/user-dashboard
|
|
||||||
|
|
||||||
# 开发完成后,提交 PR 到 develop
|
|
||||||
git push origin feature/user-dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 发布准备 (release)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 从 develop 创建发布分支
|
|
||||||
git checkout develop
|
|
||||||
git checkout -b release/0.3.0
|
|
||||||
|
|
||||||
# 在 release 分支上做最后的修复和版本号更新
|
|
||||||
# 修复完成后,合并到 main 并打标签
|
|
||||||
git checkout main
|
|
||||||
git merge release/0.3.0 --no-ff
|
|
||||||
git tag -a v0.3.0 -m "Release v0.3.0"
|
|
||||||
|
|
||||||
# 同时合并回 develop
|
|
||||||
git checkout develop
|
|
||||||
git merge release/0.3.0 --no-ff
|
|
||||||
|
|
||||||
# 删除发布分支
|
|
||||||
git branch -d release/0.3.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 热修复 (hotfix)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 从 main 创建热修复分支
|
|
||||||
git checkout main
|
|
||||||
git checkout -b hotfix/0.2.10
|
|
||||||
|
|
||||||
# 修复完成后,合并到 main 并打标签
|
|
||||||
git checkout main
|
|
||||||
git merge hotfix/0.2.10 --no-ff
|
|
||||||
git tag -a v0.2.10 -m "Hotfix v0.2.10"
|
|
||||||
|
|
||||||
# 同时合并回 develop
|
|
||||||
git checkout develop
|
|
||||||
git merge hotfix/0.2.10 --no-ff
|
|
||||||
|
|
||||||
# 删除热修复分支
|
|
||||||
git branch -d hotfix/0.2.10
|
|
||||||
```
|
|
||||||
|
|
||||||
## 分支命名规范
|
|
||||||
|
|
||||||
```
|
|
||||||
feature/<issue-id>-<简短描述>
|
|
||||||
release/<版本号>
|
|
||||||
hotfix/<版本号>
|
|
||||||
```
|
|
||||||
|
|
||||||
示例:
|
|
||||||
- `feature/123-user-dashboard`
|
|
||||||
- `feature/456-oauth-integration`
|
|
||||||
- `release/0.3.0`
|
|
||||||
- `hotfix/0.2.10`
|
|
||||||
|
|
||||||
## 合并 (Merge) 策略
|
|
||||||
|
|
||||||
- **功能分支 → develop**:使用 `git merge --no-ff`,保留功能分支历史
|
|
||||||
- **release → main + develop**:使用 `git merge --no-ff`
|
|
||||||
- **hotfix → main + develop**:使用 `git merge --no-ff`
|
|
||||||
- **PR 合并**:在 GitHub 界面使用 "Squash and merge" 或 "Create a merge commit"
|
|
||||||
|
|
||||||
## 提交信息规范
|
|
||||||
|
|
||||||
采用 [Conventional Commits](https://www.conventionalcommits.org/) 格式:
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>(<scope>): <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type 类型
|
|
||||||
|
|
||||||
| Type | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `feat` | 新功能 |
|
|
||||||
| `fix` | Bug 修复 |
|
|
||||||
| `docs` | 仅文档变更 |
|
|
||||||
| `style` | 代码格式(不影响功能) |
|
|
||||||
| `refactor` | 重构(不是修复也不是新功能) |
|
|
||||||
| `perf` | 性能优化 |
|
|
||||||
| `test` | 添加或修正测试 |
|
|
||||||
| `chore` | 构建或辅助工具变更 |
|
|
||||||
|
|
||||||
### Scope 范围
|
|
||||||
|
|
||||||
使用受影响的主要模块:
|
|
||||||
|
|
||||||
| Scope | 说明 |
|
|
||||||
|-------|------|
|
|
||||||
| `frontend` | 前端代码 |
|
|
||||||
| `backend` | 后端通用 |
|
|
||||||
| `api` | API 路由/Handler |
|
|
||||||
| `service` | 业务逻辑层 |
|
|
||||||
| `models` | 数据库模型 |
|
|
||||||
| `k8s` | Kubernetes 部署 |
|
|
||||||
| `deploy` | 部署配置 |
|
|
||||||
| `git` | Git 相关功能 |
|
|
||||||
| `room` | 实时聊天功能 |
|
|
||||||
| `agent` | AI Agent 功能 |
|
|
||||||
| `db` | 数据库/迁移 |
|
|
||||||
|
|
||||||
### 示例
|
|
||||||
|
|
||||||
```
|
|
||||||
feat(frontend): add user dashboard page
|
|
||||||
fix(api): handle nil pointer in workspace handler
|
|
||||||
docs: update API documentation for /repos endpoints
|
|
||||||
refactor(service): extract auth logic into separate module
|
|
||||||
hotfix(k8s): add missing health probe for gitserver
|
|
||||||
```
|
|
||||||
|
|
||||||
## 标签 (Tag) 策略
|
|
||||||
|
|
||||||
使用语义化版本号:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 发布标签
|
|
||||||
git tag -a v0.2.9 -m "Release v0.2.9"
|
|
||||||
|
|
||||||
# 带签名的标签(推荐)
|
|
||||||
git tag -s v0.2.9 -m "Release v0.2.9"
|
|
||||||
```
|
|
||||||
|
|
||||||
版本号格式:`v<major>.<minor>.<patch>`
|
|
||||||
- `major`:破坏性变更
|
|
||||||
- `minor`:新功能(向后兼容)
|
|
||||||
- `patch`:Bug 修复
|
|
||||||
|
|
||||||
## 分支清理
|
|
||||||
|
|
||||||
功能分支合并后应删除:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 删除本地分支
|
|
||||||
git branch -d feature/xxx
|
|
||||||
|
|
||||||
# 删除远程分支
|
|
||||||
git push origin --delete feature/xxx
|
|
||||||
```
|
|
||||||
@ -4,36 +4,10 @@ use thiserror::Error;
|
|||||||
pub enum AgentError {
|
pub enum AgentError {
|
||||||
#[error("openai error: {0}")]
|
#[error("openai error: {0}")]
|
||||||
OpenAi(String),
|
OpenAi(String),
|
||||||
|
|
||||||
#[error("qdrant error: {0}")]
|
#[error("qdrant error: {0}")]
|
||||||
Qdrant(String),
|
Qdrant(String),
|
||||||
|
|
||||||
#[error("internal error: {0}")]
|
#[error("internal error: {0}")]
|
||||||
Internal(String),
|
Internal(String),
|
||||||
|
|
||||||
/// The task exceeded its timeout limit.
|
|
||||||
#[error("task {task_id} timed out after {seconds}s")]
|
|
||||||
Timeout { task_id: i64, seconds: u64 },
|
|
||||||
|
|
||||||
/// The agent has been rate-limited; retry after the indicated delay.
|
|
||||||
#[error("rate limited, retry after {retry_after_secs}s")]
|
|
||||||
RateLimited { retry_after_secs: u64 },
|
|
||||||
|
|
||||||
/// A transient error that can be retried.
|
|
||||||
#[error("retryable error (attempt {attempt}): {message}")]
|
|
||||||
Retryable { attempt: u32, message: String },
|
|
||||||
|
|
||||||
/// The requested tool is not registered in the tool registry.
|
|
||||||
#[error("tool not found: {tool}")]
|
|
||||||
ToolNotFound { tool: String },
|
|
||||||
|
|
||||||
/// A tool execution failed.
|
|
||||||
#[error("tool '{tool}' execution failed: {cause}")]
|
|
||||||
ToolExecutionFailed { tool: String, cause: String },
|
|
||||||
|
|
||||||
/// The request contains invalid input.
|
|
||||||
#[error("invalid input in '{field}': {reason}")]
|
|
||||||
InvalidInput { field: String, reason: String },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AgentError>;
|
pub type Result<T> = std::result::Result<T, AgentError>;
|
||||||
|
|||||||
@ -5,222 +5,23 @@
|
|||||||
//! to the caller — this service only manages task lifecycle and state.
|
//! to the caller — this service only manages task lifecycle and state.
|
||||||
|
|
||||||
use db::database::AppDatabase;
|
use db::database::AppDatabase;
|
||||||
use models::agent_task::{ActiveModel, AgentType, Column as C, Entity, Model, TaskStatus};
|
use models::agent_task::{
|
||||||
use models::IssueId;
|
ActiveModel, AgentType, Column as C, Entity, Model, TaskStatus,
|
||||||
|
};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
entity::EntityTrait, query::{QueryFilter, QueryOrder, QuerySelect}, ActiveModelTrait,
|
entity::EntityTrait, query::{QueryFilter, QueryOrder, QuerySelect}, ActiveModelTrait,
|
||||||
ColumnTrait,
|
ColumnTrait, DbErr,
|
||||||
DbErr,
|
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// Event payload published to WebSocket clients via Redis Pub/Sub.
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct TaskEvent {
|
|
||||||
pub task_id: i64,
|
|
||||||
pub project_id: uuid::Uuid,
|
|
||||||
pub parent_id: Option<i64>,
|
|
||||||
pub event: String,
|
|
||||||
pub message: Option<String>,
|
|
||||||
pub output: Option<String>,
|
|
||||||
pub error: Option<String>,
|
|
||||||
pub status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskEvent {
|
|
||||||
pub fn started(task_id: i64, project_id: uuid::Uuid, parent_id: Option<i64>) -> Self {
|
|
||||||
Self {
|
|
||||||
task_id,
|
|
||||||
project_id,
|
|
||||||
parent_id,
|
|
||||||
event: "started".to_string(),
|
|
||||||
message: None,
|
|
||||||
output: None,
|
|
||||||
error: None,
|
|
||||||
status: TaskStatus::Running.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn progress(
|
|
||||||
task_id: i64,
|
|
||||||
project_id: uuid::Uuid,
|
|
||||||
parent_id: Option<i64>,
|
|
||||||
msg: String,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
task_id,
|
|
||||||
project_id,
|
|
||||||
parent_id,
|
|
||||||
event: "progress".to_string(),
|
|
||||||
message: Some(msg),
|
|
||||||
output: None,
|
|
||||||
error: None,
|
|
||||||
status: TaskStatus::Running.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn completed(
|
|
||||||
task_id: i64,
|
|
||||||
project_id: uuid::Uuid,
|
|
||||||
parent_id: Option<i64>,
|
|
||||||
output: String,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
task_id,
|
|
||||||
project_id,
|
|
||||||
parent_id,
|
|
||||||
event: "done".to_string(),
|
|
||||||
message: None,
|
|
||||||
output: Some(output),
|
|
||||||
error: None,
|
|
||||||
status: TaskStatus::Done.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn failed(
|
|
||||||
task_id: i64,
|
|
||||||
project_id: uuid::Uuid,
|
|
||||||
parent_id: Option<i64>,
|
|
||||||
error: String,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
task_id,
|
|
||||||
project_id,
|
|
||||||
parent_id,
|
|
||||||
event: "failed".to_string(),
|
|
||||||
message: None,
|
|
||||||
output: None,
|
|
||||||
error: Some(error),
|
|
||||||
status: TaskStatus::Failed.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cancelled(task_id: i64, project_id: uuid::Uuid, parent_id: Option<i64>) -> Self {
|
|
||||||
Self {
|
|
||||||
task_id,
|
|
||||||
project_id,
|
|
||||||
parent_id,
|
|
||||||
event: "cancelled".to_string(),
|
|
||||||
message: None,
|
|
||||||
output: None,
|
|
||||||
error: None,
|
|
||||||
status: TaskStatus::Cancelled.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper trait for publishing task lifecycle events via Redis Pub/Sub.
|
|
||||||
///
|
|
||||||
/// Callers inject a suitable `publish_fn` at construction time via
|
|
||||||
/// `TaskEvents::new(...)`. If no publisher is supplied events are silently
|
|
||||||
/// dropped (graceful degradation on startup).
|
|
||||||
pub trait TaskEventPublisher: Send + Sync {
|
|
||||||
fn publish(&self, project_id: uuid::Uuid, event: TaskEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op publisher used when no Redis Pub/Sub connection is available.
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct NoOpPublisher;
|
|
||||||
|
|
||||||
impl TaskEventPublisher for NoOpPublisher {
|
|
||||||
fn publish(&self, _: uuid::Uuid, _: TaskEvent) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TaskEvents {
|
|
||||||
publisher: Arc<dyn TaskEventPublisher>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskEvents {
|
|
||||||
pub fn new(publisher: impl TaskEventPublisher + 'static) -> Self {
|
|
||||||
Self {
|
|
||||||
publisher: Arc::new(publisher),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn noop() -> Self {
|
|
||||||
Self::new(NoOpPublisher)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit(&self, task: &Model, event: TaskEvent) {
|
|
||||||
self.publisher.publish(task.project_uuid, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emit_started(&self, task: &Model) {
|
|
||||||
self.emit(
|
|
||||||
task,
|
|
||||||
TaskEvent::started(task.id, task.project_uuid, task.parent_id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emit_progress(&self, task: &Model, msg: String) {
|
|
||||||
self.emit(
|
|
||||||
task,
|
|
||||||
TaskEvent::progress(task.id, task.project_uuid, task.parent_id, msg),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emit_completed(&self, task: &Model, output: String) {
|
|
||||||
self.emit(
|
|
||||||
task,
|
|
||||||
TaskEvent::completed(task.id, task.project_uuid, task.parent_id, output),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emit_failed(&self, task: &Model, error: String) {
|
|
||||||
self.emit(
|
|
||||||
task,
|
|
||||||
TaskEvent::failed(task.id, task.project_uuid, task.parent_id, error),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emit_cancelled(&self, task: &Model) {
|
|
||||||
self.emit(
|
|
||||||
task,
|
|
||||||
TaskEvent::cancelled(task.id, task.project_uuid, task.parent_id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder for TaskService so that the events publisher can be set independently
|
|
||||||
/// of the database connection.
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct TaskServiceBuilder {
|
|
||||||
events: Option<TaskEvents>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskServiceBuilder {
|
|
||||||
pub fn with_events(mut self, events: TaskEvents) -> Self {
|
|
||||||
self.events = Some(events);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build(self, db: AppDatabase) -> TaskService {
|
|
||||||
TaskService {
|
|
||||||
db,
|
|
||||||
events: self.events.unwrap_or_else(TaskEvents::noop),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Service for managing agent tasks (root tasks and sub-tasks).
|
/// Service for managing agent tasks (root tasks and sub-tasks).
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TaskService {
|
pub struct TaskService {
|
||||||
db: AppDatabase,
|
db: AppDatabase,
|
||||||
events: TaskEvents,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaskService {
|
impl TaskService {
|
||||||
pub fn new(db: AppDatabase) -> Self {
|
pub fn new(db: AppDatabase) -> Self {
|
||||||
Self {
|
Self { db }
|
||||||
db,
|
|
||||||
events: TaskEvents::noop(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_events(db: AppDatabase, events: TaskEvents) -> Self {
|
|
||||||
Self { db, events }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new task (root or sub-task) with status = pending.
|
/// Create a new task (root or sub-task) with status = pending.
|
||||||
@ -230,20 +31,7 @@ impl TaskService {
|
|||||||
input: impl Into<String>,
|
input: impl Into<String>,
|
||||||
agent_type: AgentType,
|
agent_type: AgentType,
|
||||||
) -> Result<Model, DbErr> {
|
) -> Result<Model, DbErr> {
|
||||||
self.create_with_parent(project_uuid, None, input, agent_type, None, None)
|
self.create_with_parent(project_uuid, None, input, agent_type, None).await
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new task bound to an issue.
|
|
||||||
pub async fn create_for_issue(
|
|
||||||
&self,
|
|
||||||
project_uuid: impl Into<uuid::Uuid>,
|
|
||||||
issue_id: IssueId,
|
|
||||||
input: impl Into<String>,
|
|
||||||
agent_type: AgentType,
|
|
||||||
) -> Result<Model, DbErr> {
|
|
||||||
self.create_with_parent(project_uuid, None, input, agent_type, None, Some(issue_id))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new sub-task with a parent reference.
|
/// Create a new sub-task with a parent reference.
|
||||||
@ -255,15 +43,8 @@ impl TaskService {
|
|||||||
agent_type: AgentType,
|
agent_type: AgentType,
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
) -> Result<Model, DbErr> {
|
) -> Result<Model, DbErr> {
|
||||||
self.create_with_parent(
|
self.create_with_parent(project_uuid, Some(parent_id), input, agent_type, title)
|
||||||
project_uuid,
|
.await
|
||||||
Some(parent_id),
|
|
||||||
input,
|
|
||||||
agent_type,
|
|
||||||
title,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_with_parent(
|
async fn create_with_parent(
|
||||||
@ -273,12 +54,10 @@ impl TaskService {
|
|||||||
input: impl Into<String>,
|
input: impl Into<String>,
|
||||||
agent_type: AgentType,
|
agent_type: AgentType,
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
issue_id: Option<IssueId>,
|
|
||||||
) -> Result<Model, DbErr> {
|
) -> Result<Model, DbErr> {
|
||||||
let model = ActiveModel {
|
let model = ActiveModel {
|
||||||
project_uuid: sea_orm::Set(project_uuid.into()),
|
project_uuid: sea_orm::Set(project_uuid.into()),
|
||||||
parent_id: sea_orm::Set(parent_id),
|
parent_id: sea_orm::Set(parent_id),
|
||||||
issue_id: sea_orm::Set(issue_id),
|
|
||||||
agent_type: sea_orm::Set(agent_type),
|
agent_type: sea_orm::Set(agent_type),
|
||||||
status: sea_orm::Set(TaskStatus::Pending),
|
status: sea_orm::Set(TaskStatus::Pending),
|
||||||
title: sea_orm::Set(title),
|
title: sea_orm::Set(title),
|
||||||
@ -291,223 +70,61 @@ impl TaskService {
|
|||||||
/// Mark a task as running and record the start time.
|
/// Mark a task as running and record the start time.
|
||||||
pub async fn start(&self, task_id: i64) -> Result<Model, DbErr> {
|
pub async fn start(&self, task_id: i64) -> Result<Model, DbErr> {
|
||||||
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
||||||
let model =
|
let model = model.ok_or_else(|| {
|
||||||
model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
DbErr::RecordNotFound("agent_task not found".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut active: ActiveModel = model.into();
|
let mut active: ActiveModel = model.into();
|
||||||
active.status = sea_orm::Set(TaskStatus::Running);
|
active.status = sea_orm::Set(TaskStatus::Running);
|
||||||
active.started_at = sea_orm::Set(Some(chrono::Utc::now().into()));
|
active.started_at = sea_orm::Set(Some(chrono::Utc::now().into()));
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
||||||
let updated = active.update(&self.db).await?;
|
active.update(&self.db).await
|
||||||
self.events.emit_started(&updated);
|
|
||||||
Ok(updated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update progress text (e.g., "step 2/5: analyzing PR").
|
/// Update progress text (e.g., "step 2/5: analyzing PR").
|
||||||
pub async fn update_progress(
|
pub async fn update_progress(&self, task_id: i64, progress: impl Into<String>) -> Result<(), DbErr> {
|
||||||
&self,
|
|
||||||
task_id: i64,
|
|
||||||
progress: impl Into<String>,
|
|
||||||
) -> Result<(), DbErr> {
|
|
||||||
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
||||||
let model =
|
let model = model.ok_or_else(|| {
|
||||||
model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
DbErr::RecordNotFound("agent_task not found".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
let progress_str = progress.into();
|
|
||||||
let mut active: ActiveModel = model.into();
|
let mut active: ActiveModel = model.into();
|
||||||
active.progress = sea_orm::Set(Some(progress_str.clone()));
|
active.progress = sea_orm::Set(Some(progress.into()));
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
||||||
let updated = active.update(&self.db).await?;
|
active.update(&self.db).await?;
|
||||||
self.events.emit_progress(&updated, progress_str);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark a task as completed with the output text.
|
/// Mark a task as completed with the output text.
|
||||||
pub async fn complete(&self, task_id: i64, output: impl Into<String>) -> Result<Model, DbErr> {
|
pub async fn complete(&self, task_id: i64, output: impl Into<String>) -> Result<Model, DbErr> {
|
||||||
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
||||||
let model =
|
let model = model.ok_or_else(|| {
|
||||||
model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
DbErr::RecordNotFound("agent_task not found".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut active: ActiveModel = model.into();
|
let mut active: ActiveModel = model.into();
|
||||||
active.status = sea_orm::Set(TaskStatus::Done);
|
active.status = sea_orm::Set(TaskStatus::Done);
|
||||||
let out = output.into();
|
active.output = sea_orm::Set(Some(output.into()));
|
||||||
active.output = sea_orm::Set(Some(out.clone()));
|
|
||||||
active.done_at = sea_orm::Set(Some(chrono::Utc::now().into()));
|
active.done_at = sea_orm::Set(Some(chrono::Utc::now().into()));
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
||||||
let updated = active.update(&self.db).await?;
|
active.update(&self.db).await
|
||||||
self.events.emit_completed(&updated, out);
|
|
||||||
Ok(updated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark a task as failed with an error message.
|
/// Mark a task as failed with an error message.
|
||||||
pub async fn fail(&self, task_id: i64, error: impl Into<String>) -> Result<Model, DbErr> {
|
pub async fn fail(&self, task_id: i64, error: impl Into<String>) -> Result<Model, DbErr> {
|
||||||
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
||||||
let model =
|
let model = model.ok_or_else(|| {
|
||||||
model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
DbErr::RecordNotFound("agent_task not found".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut active: ActiveModel = model.into();
|
let mut active: ActiveModel = model.into();
|
||||||
active.status = sea_orm::Set(TaskStatus::Failed);
|
active.status = sea_orm::Set(TaskStatus::Failed);
|
||||||
let err = error.into();
|
active.error = sea_orm::Set(Some(error.into()));
|
||||||
active.error = sea_orm::Set(Some(err.clone()));
|
|
||||||
active.done_at = sea_orm::Set(Some(chrono::Utc::now().into()));
|
active.done_at = sea_orm::Set(Some(chrono::Utc::now().into()));
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
||||||
let updated = active.update(&self.db).await?;
|
|
||||||
self.events.emit_failed(&updated, err);
|
|
||||||
Ok(updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Propagate child task status up the tree.
|
|
||||||
///
|
|
||||||
/// Only allows cancelling tasks that are not yet in a terminal state
|
|
||||||
/// (Pending / Running / Paused).
|
|
||||||
///
|
|
||||||
/// Cancelled children are marked done so that `are_children_done()` returns
|
|
||||||
/// true for the parent after cancellation.
|
|
||||||
pub async fn cancel(&self, task_id: i64) -> Result<Model, DbErr> {
|
|
||||||
// Collect all task IDs (parent + descendants) using an explicit stack.
|
|
||||||
let mut stack = vec![task_id];
|
|
||||||
let mut idx = 0;
|
|
||||||
while idx < stack.len() {
|
|
||||||
let current = stack[idx];
|
|
||||||
let children = Entity::find()
|
|
||||||
.filter(C::ParentId.eq(current))
|
|
||||||
.all(&self.db)
|
|
||||||
.await?;
|
|
||||||
for child in children {
|
|
||||||
stack.push(child.id);
|
|
||||||
}
|
|
||||||
idx += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark every collected task as cancelled (terminal state).
|
|
||||||
for id in &stack {
|
|
||||||
let model = Entity::find_by_id(*id).one(&self.db).await?;
|
|
||||||
if let Some(m) = model {
|
|
||||||
if !m.is_done() {
|
|
||||||
let mut active: ActiveModel = m.into();
|
|
||||||
active.status = sea_orm::Set(TaskStatus::Cancelled);
|
|
||||||
active.done_at = sea_orm::Set(Some(chrono::Utc::now().into()));
|
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
|
||||||
active.update(&self.db).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let final_model = Entity::find_by_id(task_id)
|
|
||||||
.one(&self.db)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
|
||||||
self.events.emit_cancelled(&final_model);
|
|
||||||
Ok(final_model)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pause a running or pending task.
|
|
||||||
///
|
|
||||||
/// Pausing a task that is not Pending/Running is a no-op that returns
|
|
||||||
/// the current model (same behaviour as `start` on an already-running task).
|
|
||||||
pub async fn pause(&self, task_id: i64) -> Result<Model, DbErr> {
|
|
||||||
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
|
||||||
let model =
|
|
||||||
model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
|
||||||
|
|
||||||
if !model.is_running() {
|
|
||||||
// Already in a terminal or paused state — return unchanged.
|
|
||||||
return Ok(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut active: ActiveModel = model.into();
|
|
||||||
active.status = sea_orm::Set(TaskStatus::Paused);
|
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
|
||||||
active.update(&self.db).await
|
active.update(&self.db).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resume a paused task back to Running.
|
|
||||||
///
|
|
||||||
/// Returns an error if the task is not currently Paused.
|
|
||||||
pub async fn resume(&self, task_id: i64) -> Result<Model, DbErr> {
|
|
||||||
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
|
||||||
let model =
|
|
||||||
model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
|
||||||
|
|
||||||
if model.status != TaskStatus::Paused {
|
|
||||||
return Err(DbErr::Custom(format!(
|
|
||||||
"cannot resume task {}: expected status Paused, got {}",
|
|
||||||
task_id, model.status
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut active: ActiveModel = model.into();
|
|
||||||
active.status = sea_orm::Set(TaskStatus::Running);
|
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
|
||||||
active.update(&self.db).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retry a failed or cancelled task by resetting it to Pending.
|
|
||||||
///
|
|
||||||
/// Clears `output`, `error`, and `done_at`; increments `retry_count`.
|
|
||||||
/// Only tasks in Failed or Cancelled state can be retried.
|
|
||||||
pub async fn retry(&self, task_id: i64) -> Result<Model, DbErr> {
|
|
||||||
let model = Entity::find_by_id(task_id).one(&self.db).await?;
|
|
||||||
let model =
|
|
||||||
model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
|
||||||
|
|
||||||
match model.status {
|
|
||||||
TaskStatus::Failed | TaskStatus::Cancelled | TaskStatus::Done => {}
|
|
||||||
_ => {
|
|
||||||
return Err(DbErr::Custom(format!(
|
|
||||||
"cannot retry task {}: only Failed/Cancelled/Done tasks can be retried (got {})",
|
|
||||||
task_id, model.status
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let retry_count = model.retry_count.map(|c| c + 1).unwrap_or(1);
|
|
||||||
|
|
||||||
let mut active: ActiveModel = model.into();
|
|
||||||
active.status = sea_orm::Set(TaskStatus::Pending);
|
|
||||||
active.output = sea_orm::Set(None);
|
|
||||||
active.error = sea_orm::Set(None);
|
|
||||||
active.done_at = sea_orm::Set(None);
|
|
||||||
active.started_at = sea_orm::Set(None);
|
|
||||||
active.retry_count = sea_orm::Set(Some(retry_count));
|
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
|
||||||
active.update(&self.db).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Propagate child task status up the tree.
|
|
||||||
///
|
|
||||||
/// When a child task reaches a terminal state, checks whether all its
|
|
||||||
/// siblings are also terminal. If so, marks the parent as failed so that
|
|
||||||
/// a stuck parent is never left in the `Running` state.
|
|
||||||
pub async fn propagate_to_parent(&self, task_id: i64) -> Result<Option<Model>, DbErr> {
|
|
||||||
let model = self
|
|
||||||
.get(task_id)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?;
|
|
||||||
|
|
||||||
let Some(parent_id) = model.parent_id else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let siblings = self.children(parent_id).await?;
|
|
||||||
if siblings.iter().all(|s| s.is_done()) {
|
|
||||||
let parent = self.get(parent_id).await?.ok_or_else(|| {
|
|
||||||
DbErr::RecordNotFound(format!("parent task {} not found", parent_id))
|
|
||||||
})?;
|
|
||||||
if parent.is_running() {
|
|
||||||
let mut active: ActiveModel = parent.into();
|
|
||||||
active.status = sea_orm::Set(TaskStatus::Failed);
|
|
||||||
active.error =
|
|
||||||
sea_orm::Set(Some("All sub-tasks failed or were cancelled".to_string()));
|
|
||||||
active.done_at = sea_orm::Set(Some(chrono::Utc::now().into()));
|
|
||||||
active.updated_at = sea_orm::Set(chrono::Utc::now().into());
|
|
||||||
let updated = active.update(&self.db).await?;
|
|
||||||
return Ok(Some(updated));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a task by ID.
|
/// Get a task by ID.
|
||||||
pub async fn get(&self, task_id: i64) -> Result<Option<Model>, DbErr> {
|
pub async fn get(&self, task_id: i64) -> Result<Option<Model>, DbErr> {
|
||||||
Entity::find_by_id(task_id).one(&self.db).await
|
Entity::find_by_id(task_id).one(&self.db).await
|
||||||
@ -523,14 +140,11 @@ impl TaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// List all active (non-terminal) tasks for a project.
|
/// List all active (non-terminal) tasks for a project.
|
||||||
pub async fn active_tasks(
|
pub async fn active_tasks(&self, project_uuid: impl Into<uuid::Uuid>) -> Result<Vec<Model>, DbErr> {
|
||||||
&self,
|
|
||||||
project_uuid: impl Into<uuid::Uuid>,
|
|
||||||
) -> Result<Vec<Model>, DbErr> {
|
|
||||||
let uuid: uuid::Uuid = project_uuid.into();
|
let uuid: uuid::Uuid = project_uuid.into();
|
||||||
Entity::find()
|
Entity::find()
|
||||||
.filter(C::ProjectUuid.eq(uuid))
|
.filter(C::ProjectUuid.eq(uuid))
|
||||||
.filter(C::Status.is_in([TaskStatus::Pending, TaskStatus::Running, TaskStatus::Paused]))
|
.filter(C::Status.is_in([TaskStatus::Pending, TaskStatus::Running]))
|
||||||
.order_by_desc(C::CreatedAt)
|
.order_by_desc(C::CreatedAt)
|
||||||
.all(&self.db)
|
.all(&self.db)
|
||||||
.await
|
.await
|
||||||
@ -584,10 +198,12 @@ impl TaskService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if all sub-tasks of a given parent are in a terminal state.
|
/// Check if all sub-tasks of a given parent are done.
|
||||||
/// Returns true if there are no children (empty tree counts as done).
|
|
||||||
pub async fn are_children_done(&self, parent_id: i64) -> Result<bool, DbErr> {
|
pub async fn are_children_done(&self, parent_id: i64) -> Result<bool, DbErr> {
|
||||||
let children = self.children(parent_id).await?;
|
let children = self.children(parent_id).await?;
|
||||||
Ok(children.is_empty() || children.iter().all(|c| c.is_done()))
|
if children.is_empty() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Ok(children.iter().all(|c| c.is_done()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
//! Executes tool calls and converts results to OpenAI `tool` messages.
|
//! Executes tool calls and converts results to OpenAI `tool` messages.
|
||||||
|
|
||||||
use futures::stream;
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use futures::stream;
|
||||||
|
|
||||||
use async_openai::types::chat::{
|
use async_openai::types::chat::{
|
||||||
ChatCompletionRequestMessage, ChatCompletionRequestToolMessage,
|
ChatCompletionRequestMessage, ChatCompletionRequestToolMessage,
|
||||||
@ -70,9 +70,8 @@ impl ToolExecutor {
|
|||||||
ctx.increment_tool_calls();
|
ctx.increment_tool_calls();
|
||||||
|
|
||||||
let concurrency = self.max_concurrency;
|
let concurrency = self.max_concurrency;
|
||||||
use tokio::sync::Mutex as AsyncMutex;
|
use std::sync::Mutex;
|
||||||
let results: AsyncMutex<Vec<ToolCallResult>> =
|
let results: Mutex<Vec<ToolCallResult>> = Mutex::new(Vec::with_capacity(calls.len()));
|
||||||
AsyncMutex::new(Vec::with_capacity(calls.len()));
|
|
||||||
|
|
||||||
stream::iter(calls.into_iter().map(|call| {
|
stream::iter(calls.into_iter().map(|call| {
|
||||||
let child_ctx = ctx.child_context();
|
let child_ctx = ctx.child_context();
|
||||||
@ -92,12 +91,12 @@ impl ToolExecutor {
|
|||||||
e.to_string(),
|
e.to_string(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
results.lock().await.push(r);
|
results.lock().unwrap().push(r);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(results.into_inner())
|
Ok(results.into_inner().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_one(
|
async fn execute_one(
|
||||||
|
|||||||
@ -79,8 +79,6 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260412_000003_create_project_skill::Migration),
|
Box::new(m20260412_000003_create_project_skill::Migration),
|
||||||
Box::new(m20260413_000001_add_skill_commit_blob::Migration),
|
Box::new(m20260413_000001_add_skill_commit_blob::Migration),
|
||||||
Box::new(m20260414_000001_create_agent_task::Migration),
|
Box::new(m20260414_000001_create_agent_task::Migration),
|
||||||
Box::new(m20260415_000001_add_issue_id_to_agent_task::Migration),
|
|
||||||
Box::new(m20260416_000001_add_retry_count_to_agent_task::Migration),
|
|
||||||
// Repo tables
|
// Repo tables
|
||||||
Box::new(m20250628_000028_create_repo::Migration),
|
Box::new(m20250628_000028_create_repo::Migration),
|
||||||
Box::new(m20250628_000029_create_repo_branch::Migration),
|
Box::new(m20250628_000029_create_repo_branch::Migration),
|
||||||
@ -250,5 +248,3 @@ pub mod m20260412_000002_create_workspace_billing_history;
|
|||||||
pub mod m20260412_000003_create_project_skill;
|
pub mod m20260412_000003_create_project_skill;
|
||||||
pub mod m20260413_000001_add_skill_commit_blob;
|
pub mod m20260413_000001_add_skill_commit_blob;
|
||||||
pub mod m20260414_000001_create_agent_task;
|
pub mod m20260414_000001_create_agent_task;
|
||||||
pub mod m20260415_000001_add_issue_id_to_agent_task;
|
|
||||||
pub mod m20260416_000001_add_retry_count_to_agent_task;
|
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
//! SeaORM migration: add issue_id to agent_task
|
|
||||||
|
|
||||||
use sea_orm_migration::prelude::*;
|
|
||||||
|
|
||||||
pub struct Migration;
|
|
||||||
|
|
||||||
impl MigrationName for Migration {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"m20260415_000001_add_issue_id_to_agent_task"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MigrationTrait for Migration {
|
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
let sql = include_str!("sql/m20260415_000001_add_issue_id_to_agent_task.sql");
|
|
||||||
super::execute_sql(manager, sql).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
super::execute_sql(manager, "ALTER TABLE agent_task DROP COLUMN IF EXISTS issue_id;").await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
//! SeaORM migration: add retry_count to agent_task
|
|
||||||
|
|
||||||
use sea_orm_migration::prelude::*;
|
|
||||||
|
|
||||||
pub struct Migration;
|
|
||||||
|
|
||||||
impl MigrationName for Migration {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"m20260416_000001_add_retry_count_to_agent_task"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MigrationTrait for Migration {
|
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
let sql = include_str!("sql/m20260416_000001_add_retry_count_to_agent_task.sql");
|
|
||||||
super::execute_sql(manager, sql).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
super::execute_sql(
|
|
||||||
manager,
|
|
||||||
"ALTER TABLE agent_task DROP COLUMN IF EXISTS retry_count;",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
-- Add issue_id column to agent_task for issue-task binding
|
|
||||||
ALTER TABLE agent_task ADD COLUMN IF NOT EXISTS issue_id UUID;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_agent_task_issue ON agent_task (issue_id);
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
-- Add retry_count column to agent_task for retry tracking
|
|
||||||
ALTER TABLE agent_task ADD COLUMN IF NOT EXISTS retry_count INTEGER DEFAULT 0;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_agent_task_retry_count ON agent_task (retry_count);
|
|
||||||
@ -10,7 +10,7 @@
|
|||||||
//! Sub-agents are represented as `agent_task` records with a parent reference,
|
//! Sub-agents are represented as `agent_task` records with a parent reference,
|
||||||
//! allowing hierarchical task trees and result aggregation.
|
//! allowing hierarchical task trees and result aggregation.
|
||||||
|
|
||||||
use crate::{DateTimeUtc, IssueId, ProjectId, UserId};
|
use crate::{DateTimeUtc, ProjectId, UserId};
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -51,14 +51,10 @@ pub enum TaskStatus {
|
|||||||
Pending,
|
Pending,
|
||||||
#[sea_orm(string_value = "Running")]
|
#[sea_orm(string_value = "Running")]
|
||||||
Running,
|
Running,
|
||||||
#[sea_orm(string_value = "Paused")]
|
|
||||||
Paused,
|
|
||||||
#[sea_orm(string_value = "Done")]
|
#[sea_orm(string_value = "Done")]
|
||||||
Done,
|
Done,
|
||||||
#[sea_orm(string_value = "Failed")]
|
#[sea_orm(string_value = "Failed")]
|
||||||
Failed,
|
Failed,
|
||||||
#[sea_orm(string_value = "Cancelled")]
|
|
||||||
Cancelled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TaskStatus {
|
impl Default for TaskStatus {
|
||||||
@ -72,10 +68,8 @@ impl std::fmt::Display for TaskStatus {
|
|||||||
match self {
|
match self {
|
||||||
TaskStatus::Pending => write!(f, "Pending"),
|
TaskStatus::Pending => write!(f, "Pending"),
|
||||||
TaskStatus::Running => write!(f, "Running"),
|
TaskStatus::Running => write!(f, "Running"),
|
||||||
TaskStatus::Paused => write!(f, "Paused"),
|
|
||||||
TaskStatus::Done => write!(f, "Done"),
|
TaskStatus::Done => write!(f, "Done"),
|
||||||
TaskStatus::Failed => write!(f, "Failed"),
|
TaskStatus::Failed => write!(f, "Failed"),
|
||||||
TaskStatus::Cancelled => write!(f, "Cancelled"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,10 +88,6 @@ pub struct Model {
|
|||||||
#[sea_orm(nullable)]
|
#[sea_orm(nullable)]
|
||||||
pub parent_id: Option<i64>,
|
pub parent_id: Option<i64>,
|
||||||
|
|
||||||
/// Issue this task is bound to (optional).
|
|
||||||
#[sea_orm(nullable)]
|
|
||||||
pub issue_id: Option<IssueId>,
|
|
||||||
|
|
||||||
/// Agent type that executes this task.
|
/// Agent type that executes this task.
|
||||||
#[sea_orm(column_type = "String(StringLen::None)", default = "React")]
|
#[sea_orm(column_type = "String(StringLen::None)", default = "React")]
|
||||||
pub agent_type: AgentType,
|
pub agent_type: AgentType,
|
||||||
@ -139,15 +129,15 @@ pub struct Model {
|
|||||||
/// Current progress description (e.g., "step 2/5: analyzing code").
|
/// Current progress description (e.g., "step 2/5: analyzing code").
|
||||||
#[sea_orm(nullable)]
|
#[sea_orm(nullable)]
|
||||||
pub progress: Option<String>,
|
pub progress: Option<String>,
|
||||||
|
|
||||||
/// Number of times this task has been retried.
|
|
||||||
#[sea_orm(nullable)]
|
|
||||||
pub retry_count: Option<i32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(belongs_to = "Entity", from = "Column::ParentId", to = "Column::Id")]
|
#[sea_orm(
|
||||||
|
belongs_to = "Entity",
|
||||||
|
from = "Column::ParentId",
|
||||||
|
to = "Column::Id"
|
||||||
|
)]
|
||||||
ParentTask,
|
ParentTask,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,16 +149,6 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_done(&self) -> bool {
|
pub fn is_done(&self) -> bool {
|
||||||
matches!(
|
matches!(self.status, TaskStatus::Done | TaskStatus::Failed)
|
||||||
self.status,
|
|
||||||
TaskStatus::Done | TaskStatus::Failed | TaskStatus::Cancelled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_running(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.status,
|
|
||||||
TaskStatus::Running | TaskStatus::Pending | TaskStatus::Paused
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
//! Room message queue: Redis Streams + Redis Pub/Sub.
|
//! Room message queue: Redis Streams + NATS.
|
||||||
|
|
||||||
pub mod producer;
|
pub mod producer;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
@ -10,6 +10,6 @@ pub use types::{
|
|||||||
RoomMessageEvent, RoomMessageStreamChunkEvent,
|
RoomMessageEvent, RoomMessageStreamChunkEvent,
|
||||||
};
|
};
|
||||||
pub use worker::{
|
pub use worker::{
|
||||||
room_worker_task, start as start_worker, start_email_worker, EmailSendFn, EmailSendFut, GetRedis,
|
EmailSendFn, EmailSendFut, GetRedis, PersistFn, RedisFuture, room_worker_task,
|
||||||
PersistFn, RedisFuture,
|
start as start_worker, start_email_worker,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,8 +2,8 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use metrics::{
|
use metrics::{
|
||||||
describe_counter, describe_gauge, describe_histogram, register_counter, register_gauge, register_histogram, Counter,
|
Counter, Gauge, Histogram, Unit, describe_counter, describe_gauge, describe_histogram,
|
||||||
Gauge, Histogram, Unit,
|
register_counter, register_gauge, register_histogram,
|
||||||
};
|
};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -20,7 +20,7 @@ pub struct RoomMetrics {
|
|||||||
pub broadcasts_sent: Counter,
|
pub broadcasts_sent: Counter,
|
||||||
pub broadcasts_dropped: Counter,
|
pub broadcasts_dropped: Counter,
|
||||||
pub duplicates_skipped: Counter,
|
pub duplicates_skipped: Counter,
|
||||||
pub redis_publish_failed: Counter,
|
pub nats_publish_failed: Counter,
|
||||||
pub message_latency_ms: Histogram,
|
pub message_latency_ms: Histogram,
|
||||||
pub ws_rate_limit_hits: Counter,
|
pub ws_rate_limit_hits: Counter,
|
||||||
pub ws_auth_failures: Counter,
|
pub ws_auth_failures: Counter,
|
||||||
@ -78,9 +78,9 @@ impl Default for RoomMetrics {
|
|||||||
"Total duplicate messages skipped (idempotency)"
|
"Total duplicate messages skipped (idempotency)"
|
||||||
);
|
);
|
||||||
describe_counter!(
|
describe_counter!(
|
||||||
"room_redis_publish_failed_total",
|
"room_nats_publish_failed_total",
|
||||||
Unit::Count,
|
Unit::Count,
|
||||||
"Total Redis publish failures"
|
"Total NATS publish failures"
|
||||||
);
|
);
|
||||||
describe_histogram!(
|
describe_histogram!(
|
||||||
"room_message_latency_ms",
|
"room_message_latency_ms",
|
||||||
@ -130,7 +130,7 @@ impl Default for RoomMetrics {
|
|||||||
broadcasts_sent: register_counter!("room_broadcasts_sent_total"),
|
broadcasts_sent: register_counter!("room_broadcasts_sent_total"),
|
||||||
broadcasts_dropped: register_counter!("room_broadcasts_dropped_total"),
|
broadcasts_dropped: register_counter!("room_broadcasts_dropped_total"),
|
||||||
duplicates_skipped: register_counter!("room_duplicates_skipped_total"),
|
duplicates_skipped: register_counter!("room_duplicates_skipped_total"),
|
||||||
redis_publish_failed: register_counter!("room_redis_publish_failed_total"),
|
nats_publish_failed: register_counter!("room_nats_publish_failed_total"),
|
||||||
message_latency_ms: register_histogram!("room_message_latency_ms"),
|
message_latency_ms: register_histogram!("room_message_latency_ms"),
|
||||||
ws_rate_limit_hits: register_counter!("room_ws_rate_limit_hits_total"),
|
ws_rate_limit_hits: register_counter!("room_ws_rate_limit_hits_total"),
|
||||||
ws_auth_failures: register_counter!("room_ws_auth_failures_total"),
|
ws_auth_failures: register_counter!("room_ws_auth_failures_total"),
|
||||||
|
|||||||
@ -242,7 +242,7 @@ impl RoomService {
|
|||||||
project_id: Uuid,
|
project_id: Uuid,
|
||||||
agent_type: AgentType,
|
agent_type: AgentType,
|
||||||
input: String,
|
input: String,
|
||||||
_title: Option<String>,
|
title: Option<String>,
|
||||||
execute: F,
|
execute: F,
|
||||||
) -> anyhow::Result<i64>
|
) -> anyhow::Result<i64>
|
||||||
where
|
where
|
||||||
|
|||||||
@ -35,7 +35,6 @@ struct OpenRouterResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[allow(dead_code)]
|
|
||||||
struct OpenRouterModel {
|
struct OpenRouterModel {
|
||||||
id: String,
|
id: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
@ -51,7 +50,6 @@ struct OpenRouterModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[allow(dead_code)]
|
|
||||||
struct OpenRouterPricing {
|
struct OpenRouterPricing {
|
||||||
prompt: String,
|
prompt: String,
|
||||||
completion: String,
|
completion: String,
|
||||||
@ -70,7 +68,6 @@ struct OpenRouterPricing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[allow(dead_code)]
|
|
||||||
struct OpenRouterArchitecture {
|
struct OpenRouterArchitecture {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
modality: Option<String>,
|
modality: Option<String>,
|
||||||
@ -85,7 +82,6 @@ struct OpenRouterArchitecture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[allow(dead_code)]
|
|
||||||
struct OpenRouterTopProvider {
|
struct OpenRouterTopProvider {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
context_length: Option<u64>,
|
context_length: Option<u64>,
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
*
|
*
|
||||||
* Environment:
|
* Environment:
|
||||||
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
||||||
* TAG - Image tag (default: git short SHA)
|
* TAG - Image tag (default: latest)
|
||||||
* TARGET - Rust build target (default: x86_64-unknown-linux-gnu)
|
* TARGET - Rust build target (default: x86_64-unknown-linux-gnu)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -22,8 +22,7 @@ const path = require('path');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
||||||
const GIT_SHA_SHORT = execSync('git rev-parse --short HEAD', {encoding: 'utf8'}).trim();
|
const TAG = process.env.TAG || new Date().toISOString().slice(0, 13).replace('T', '-');
|
||||||
const TAG = process.env.TAG || GIT_SHA_SHORT;
|
|
||||||
const BUILD_TARGET = process.env.TARGET || 'x86_64-unknown-linux-gnu';
|
const BUILD_TARGET = process.env.TARGET || 'x86_64-unknown-linux-gnu';
|
||||||
|
|
||||||
const RUST_SERVICES = ['app', 'gitserver', 'email-worker', 'git-hook', 'operator', 'static'];
|
const RUST_SERVICES = ['app', 'gitserver', 'email-worker', 'git-hook', 'operator', 'static'];
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
*
|
*
|
||||||
* Environment:
|
* Environment:
|
||||||
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
||||||
* TAG - Image tag (default: git short SHA)
|
* TAG - Image tag (default: latest)
|
||||||
* NAMESPACE - Kubernetes namespace (default: gitdata)
|
* NAMESPACE - Kubernetes namespace (default: gitdata)
|
||||||
* RELEASE - Helm release name (default: gitdata)
|
* RELEASE - Helm release name (default: gitdata)
|
||||||
* KUBECONFIG - Path to kubeconfig (default: ~/.kube/config)
|
* KUBECONFIG - Path to kubeconfig (default: ~/.kube/config)
|
||||||
@ -20,8 +20,7 @@ const path = require('path');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
||||||
const GIT_SHA_SHORT = execSync('git rev-parse --short HEAD', {encoding: 'utf8'}).trim();
|
const TAG = process.env.TAG || process.env.GITHUB_SHA?.substring(0, 8) || 'latest';
|
||||||
const TAG = process.env.TAG || process.env.GITHUB_SHA?.substring(0, 8) || GIT_SHA_SHORT;
|
|
||||||
const NAMESPACE = process.env.NAMESPACE || 'gitdataai';
|
const NAMESPACE = process.env.NAMESPACE || 'gitdataai';
|
||||||
const RELEASE = process.env.RELEASE || 'gitdata';
|
const RELEASE = process.env.RELEASE || 'gitdata';
|
||||||
const CHART_PATH = path.join(__dirname, '..', 'deploy');
|
const CHART_PATH = path.join(__dirname, '..', 'deploy');
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
*
|
*
|
||||||
* Environment:
|
* Environment:
|
||||||
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
* REGISTRY - Docker registry (default: harbor.gitdata.me/gta_team)
|
||||||
* TAG - Image tag (default: git short SHA)
|
* TAG - Image tag (default: latest)
|
||||||
* DOCKER_USER - Registry username
|
* DOCKER_USER - Registry username
|
||||||
* DOCKER_PASS - Registry password
|
* DOCKER_PASS - Registry password
|
||||||
*/
|
*/
|
||||||
@ -16,8 +16,7 @@
|
|||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gta_team';
|
||||||
const GIT_SHA_SHORT = execSync('git rev-parse --short HEAD', {encoding: 'utf8'}).trim();
|
const TAG = process.env.TAG || new Date().toISOString().slice(0, 13).replace('T', '-');
|
||||||
const TAG = process.env.TAG || GIT_SHA_SHORT;
|
|
||||||
const DOCKER_USER = process.env.DOCKER_USER || process.env.HARBOR_USERNAME;
|
const DOCKER_USER = process.env.DOCKER_USER || process.env.HARBOR_USERNAME;
|
||||||
const DOCKER_PASS = process.env.DOCKER_PASS || process.env.HARBOR_PASSWORD;
|
const DOCKER_PASS = process.env.DOCKER_PASS || process.env.HARBOR_PASSWORD;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {useQuery} from '@tanstack/react-query';
|
|||||||
import {CheckCircle2, Loader2, XCircle} from 'lucide-react';
|
import {CheckCircle2, Loader2, XCircle} from 'lucide-react';
|
||||||
import {toast} from 'sonner';
|
import {toast} from 'sonner';
|
||||||
import {projectCreate, projectInfo, workspaceList} from '@/client';
|
import {projectCreate, projectInfo, workspaceList} from '@/client';
|
||||||
import {isAxiosError} from 'axios';
|
|
||||||
import {Button} from '@/components/ui/button';
|
import {Button} from '@/components/ui/button';
|
||||||
import {Input} from '@/components/ui/input';
|
import {Input} from '@/components/ui/input';
|
||||||
import {Label} from '@/components/ui/label';
|
import {Label} from '@/components/ui/label';
|
||||||
@ -60,12 +59,11 @@ export function InitProject() {
|
|||||||
try {
|
try {
|
||||||
const lower = name.trim().toLowerCase();
|
const lower = name.trim().toLowerCase();
|
||||||
await projectInfo({path: {project_name: lower}});
|
await projectInfo({path: {project_name: lower}});
|
||||||
// 200 OK → 项目存在
|
|
||||||
setNameAvailable(false);
|
setNameAvailable(false);
|
||||||
setAvailabilityMessage('Project name already exists');
|
setAvailabilityMessage('Project name already exists');
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
// @hey-api/openapi-ts 默认 throwOnError=false,错误作为 AxiosError 返回
|
const status = (err as any)?.response?.status;
|
||||||
if (isAxiosError(err) && err.response?.status === 404) {
|
if (status === 404) {
|
||||||
setNameAvailable(true);
|
setNameAvailable(true);
|
||||||
setAvailabilityMessage('Project name is available');
|
setAvailabilityMessage('Project name is available');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user