main: 扩展 Agent Run 调度与队列功能
- 增加 Agent Run MVP-0,包括 RunDispatcher 和 AgentRunJob - 优化队列配置,支持 Redis 队列驱动,添加 Horizon 容器 - 更新 Docker 配置,细化角色分工,新增 Horizon 配置 - 增加测试任务 `TestJob`,扩展队列使用示例 - 更新 OpenAPI 规范,添加 Agent Run 相关接口及示例 - 编写文档,详细描述 Agent Run 流程与 MVP-0 功能 - 优化相关服务与文档,支持队列与异步运行
This commit is contained in:
@@ -37,9 +37,9 @@ SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
QUEUE_CONNECTION=redis
|
||||
|
||||
CACHE_STORE=database
|
||||
CACHE_STORE=redis
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,3 +26,5 @@ Thumbs.db
|
||||
**/caddy
|
||||
frankenphp
|
||||
frankenphp-worker.php
|
||||
rr
|
||||
.rr.yaml
|
||||
|
||||
228
.junie/guidelines.md
Normal file
228
.junie/guidelines.md
Normal file
@@ -0,0 +1,228 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.3.28
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/horizon (HORIZON) - v5
|
||||
- laravel/octane (OCTANE) - v2
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/telescope (TELESCOPE) - v5
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- phpunit/phpunit (PHPUNIT) - v11
|
||||
|
||||
## Conventions
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
- Stick to existing directory structure - don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail npm run build`, `vendor/bin/sail npm run dev`, or `vendor/bin/sail composer run dev`. Ask them.
|
||||
|
||||
## Replies
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
## Documentation Files
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
## Laravel Boost
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
|
||||
|
||||
## URLs
|
||||
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
|
||||
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
|
||||
- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
- You can and should pass multiple queries at once. The most relevant results will be returned first.
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
|
||||
|
||||
|
||||
=== php rules ===
|
||||
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even if it has one line.
|
||||
|
||||
### Constructors
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters.
|
||||
|
||||
### Type Declarations
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Comments
|
||||
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
|
||||
|
||||
## PHPDoc Blocks
|
||||
- Add useful array shape type definitions for arrays when appropriate.
|
||||
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
|
||||
=== sail rules ===
|
||||
|
||||
## Laravel Sail
|
||||
|
||||
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
|
||||
- Open the application in the browser by running `vendor/bin/sail open`.
|
||||
- Always prefix PHP, Artisan, Composer, and Node commands** with `vendor/bin/sail`. Examples:
|
||||
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
|
||||
- Install Composer packages: `vendor/bin/sail composer install`
|
||||
- Execute node commands: `vendor/bin/sail npm run dev`
|
||||
- Execute PHP scripts: `vendor/bin/sail php [script]`
|
||||
- View all available Sail commands by running `vendor/bin/sail` without arguments.
|
||||
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
## Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `vendor/bin/sail artisan test` with a specific filename or filter.
|
||||
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
## Do Things the Laravel Way
|
||||
|
||||
- Use `vendor/bin/sail artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- If you're creating a generic PHP class, use `vendor/bin/sail artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
### Database
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `vendor/bin/sail artisan make:model`.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
### Controllers & Validation
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
### Queues
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
### Authentication & Authorization
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
### URL Generation
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
### Configuration
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
### Testing
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `vendor/bin/sail artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
### Vite Error
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail npm run build` or ask the user to run `vendor/bin/sail npm run dev` or `vendor/bin/sail composer run dev`.
|
||||
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
## Laravel 12
|
||||
|
||||
- Use the `search-docs` tool to get version specific documentation.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
### Laravel 12 Structure
|
||||
- No middleware files in `app/Http/Middleware/`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
### Database
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
## Laravel Pint Code Formatter
|
||||
|
||||
- You must run `vendor/bin/sail bin pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/sail bin pint --test`, simply run `vendor/bin/sail bin pint` to fix any formatting issues.
|
||||
|
||||
|
||||
=== phpunit/core rules ===
|
||||
|
||||
## PHPUnit Core
|
||||
|
||||
- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `vendor/bin/sail artisan make:test --phpunit {name}` to create a new test.
|
||||
- If you see a test using "Pest", convert it to PHPUnit.
|
||||
- Every time a test has been updated, run that singular test.
|
||||
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files, these are core to the application.
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests, using an appropriate filter, before finalizing.
|
||||
- To run all tests: `vendor/bin/sail artisan test`.
|
||||
- To run all tests in a file: `vendor/bin/sail artisan test tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `vendor/bin/sail artisan test --filter=testName` (recommended after making a change to a related file).
|
||||
</laravel-boost-guidelines>
|
||||
11
.junie/mcp/mcp.json
Normal file
11
.junie/mcp/mcp.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "vendor/bin/sail",
|
||||
"args": [
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
12
.mcp.json
Normal file
12
.mcp.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "docker compose run app",
|
||||
"args": [
|
||||
"vendor/bin/sail",
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
225
AGENTS.md
Normal file
225
AGENTS.md
Normal file
@@ -0,0 +1,225 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.3.28
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/horizon (HORIZON) - v5
|
||||
- laravel/octane (OCTANE) - v2
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/telescope (TELESCOPE) - v5
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- phpunit/phpunit (PHPUNIT) - v11
|
||||
|
||||
## Conventions
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
- Stick to existing directory structure - don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `docker compose run --rm app npm run build`, `docker compose run --rm app npm run dev`, or `docker compose run --rm app composer run dev`. Ask them.
|
||||
|
||||
## Replies
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
- Use Chinese for all natural language communication.
|
||||
|
||||
## Documentation Files
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
## Laravel Boost
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
|
||||
|
||||
## URLs
|
||||
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
|
||||
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
|
||||
- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
- You can and should pass multiple queries at once. The most relevant results will be returned first.
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
|
||||
|
||||
|
||||
=== php rules ===
|
||||
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even if it has one line.
|
||||
|
||||
### Constructors
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters.
|
||||
|
||||
### Type Declarations
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Comments
|
||||
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
|
||||
|
||||
## PHPDoc Blocks
|
||||
- Add useful array shape type definitions for arrays when appropriate.
|
||||
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
|
||||
=== docker-compose rules ===
|
||||
|
||||
## Docker Compose
|
||||
|
||||
- This project runs with Docker Compose (Sail is not used). Always run commands through Docker Compose.
|
||||
- Start services using `docker compose up -d` and stop them with `docker compose down`.
|
||||
- Run one-off commands with `docker compose run --rm app ...` to ensure dependencies are available.
|
||||
- Common commands: `docker compose run --rm app php artisan migrate`, `docker compose run --rm app composer install`, `docker compose run --rm app npm run dev`, `docker compose run --rm app php [script]`.
|
||||
- The application is available at `http://localhost:8000` when the stack is running.
|
||||
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
## Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `docker compose run --rm app php artisan test` with a specific filename or filter.
|
||||
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
## Do Things the Laravel Way
|
||||
|
||||
- Use `docker compose run --rm app php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- If you're creating a generic PHP class, use `docker compose run --rm app php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
### Database
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `vendor/bin/sail artisan make:model`.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
### Controllers & Validation
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
### Queues
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
### Authentication & Authorization
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
### URL Generation
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
### Configuration
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
### Testing
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `vendor/bin/sail artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
### Vite Error
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail npm run build` or ask the user to run `vendor/bin/sail npm run dev` or `vendor/bin/sail composer run dev`.
|
||||
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
## Laravel 12
|
||||
|
||||
- Use the `search-docs` tool to get version specific documentation.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
### Laravel 12 Structure
|
||||
- No middleware files in `app/Http/Middleware/`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
### Database
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
## Laravel Pint Code Formatter
|
||||
|
||||
- You must run `docker compose run --rm app ./vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `docker compose run --rm app ./vendor/bin/pint --test`; just run Pint to fix any formatting issues.
|
||||
|
||||
|
||||
=== phpunit/core rules ===
|
||||
|
||||
## PHPUnit Core
|
||||
|
||||
- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `vendor/bin/sail artisan make:test --phpunit {name}` to create a new test.
|
||||
- If you see a test using "Pest", convert it to PHPUnit.
|
||||
- Every time a test has been updated, run that singular test.
|
||||
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files, these are core to the application.
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests, using an appropriate filter, before finalizing.
|
||||
- To run all tests: `docker compose run --rm app php artisan test`.
|
||||
- To run all tests in a file: `docker compose run --rm app php artisan test tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `docker compose run --rm app php artisan test --filter=testName` (recommended after making a change to a related file).
|
||||
</laravel-boost-guidelines>
|
||||
228
CLAUDE.md
Normal file
228
CLAUDE.md
Normal file
@@ -0,0 +1,228 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.3.28
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/horizon (HORIZON) - v5
|
||||
- laravel/octane (OCTANE) - v2
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/telescope (TELESCOPE) - v5
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- phpunit/phpunit (PHPUNIT) - v11
|
||||
|
||||
## Conventions
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
- Stick to existing directory structure - don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail npm run build`, `vendor/bin/sail npm run dev`, or `vendor/bin/sail composer run dev`. Ask them.
|
||||
|
||||
## Replies
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
## Documentation Files
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
## Laravel Boost
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
|
||||
|
||||
## URLs
|
||||
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
|
||||
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
|
||||
- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
- You can and should pass multiple queries at once. The most relevant results will be returned first.
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
|
||||
|
||||
|
||||
=== php rules ===
|
||||
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even if it has one line.
|
||||
|
||||
### Constructors
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters.
|
||||
|
||||
### Type Declarations
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Comments
|
||||
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
|
||||
|
||||
## PHPDoc Blocks
|
||||
- Add useful array shape type definitions for arrays when appropriate.
|
||||
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
|
||||
=== sail rules ===
|
||||
|
||||
## Laravel Sail
|
||||
|
||||
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
|
||||
- Open the application in the browser by running `vendor/bin/sail open`.
|
||||
- Always prefix PHP, Artisan, Composer, and Node commands** with `vendor/bin/sail`. Examples:
|
||||
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
|
||||
- Install Composer packages: `vendor/bin/sail composer install`
|
||||
- Execute node commands: `vendor/bin/sail npm run dev`
|
||||
- Execute PHP scripts: `vendor/bin/sail php [script]`
|
||||
- View all available Sail commands by running `vendor/bin/sail` without arguments.
|
||||
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
## Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `vendor/bin/sail artisan test` with a specific filename or filter.
|
||||
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
## Do Things the Laravel Way
|
||||
|
||||
- Use `vendor/bin/sail artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- If you're creating a generic PHP class, use `vendor/bin/sail artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
### Database
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `vendor/bin/sail artisan make:model`.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
### Controllers & Validation
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
### Queues
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
### Authentication & Authorization
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
### URL Generation
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
### Configuration
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
### Testing
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `vendor/bin/sail artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
### Vite Error
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail npm run build` or ask the user to run `vendor/bin/sail npm run dev` or `vendor/bin/sail composer run dev`.
|
||||
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
## Laravel 12
|
||||
|
||||
- Use the `search-docs` tool to get version specific documentation.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
### Laravel 12 Structure
|
||||
- No middleware files in `app/Http/Middleware/`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
### Database
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
## Laravel Pint Code Formatter
|
||||
|
||||
- You must run `vendor/bin/sail bin pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/sail bin pint --test`, simply run `vendor/bin/sail bin pint` to fix any formatting issues.
|
||||
|
||||
|
||||
=== phpunit/core rules ===
|
||||
|
||||
## PHPUnit Core
|
||||
|
||||
- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `vendor/bin/sail artisan make:test --phpunit {name}` to create a new test.
|
||||
- If you see a test using "Pest", convert it to PHPUnit.
|
||||
- Every time a test has been updated, run that singular test.
|
||||
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files, these are core to the application.
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests, using an appropriate filter, before finalizing.
|
||||
- To run all tests: `vendor/bin/sail artisan test`.
|
||||
- To run all tests in a file: `vendor/bin/sail artisan test tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `vendor/bin/sail artisan test --filter=testName` (recommended after making a change to a related file).
|
||||
</laravel-boost-guidelines>
|
||||
@@ -18,24 +18,31 @@
|
||||
- `messages(message_id UUID, session_id, role USER|AGENT|TOOL|SYSTEM, type, content, payload jsonb, seq, reply_to, dedupe_key)`
|
||||
- 约束:`unique(session_id, seq)`、`unique(session_id, dedupe_key)`;append 行锁 + 事务,seq 单调递增。
|
||||
- **实时**:SSE `/api/sessions/{id}/sse`,backlog 先补历史(按 seq),再监听 Redis `session:{id}:messages` 渠道。
|
||||
- **Agent Run 编排(MVP-0)**:user.prompt 后自动触发 RunDispatcher → `run.status=RUNNING` → AgentRunJob(DummyProvider 返回一次性回复)→ `agent.message` + `run.status=DONE/FAILED/CANCELED` 落库;同 trigger_message 幂等、同会话只允许一个 RUNNING。
|
||||
- **队列监控**:Horizon(本地默认开放 `/horizon`,非 local 环境默认拒绝访问)。
|
||||
|
||||
## 🚀 快速启动
|
||||
```bash
|
||||
# 构建并启动
|
||||
docker compose build
|
||||
docker compose up -d app pgsql redis
|
||||
docker compose up -d app horizon pgsql redis
|
||||
|
||||
# 首次迁移(仅需一次)
|
||||
docker compose exec app php artisan migrate
|
||||
|
||||
# 运行 Feature 测试
|
||||
docker compose exec app php artisan test --testsuite=Feature
|
||||
|
||||
# 队列(AgentRunJob):开发可用同步队列,或用 Horizon
|
||||
# 同步:.env / phpunit.xml 中 QUEUE_CONNECTION=sync
|
||||
# Horizon:docker compose up -d horizon(需 composer install 安装 laravel/horizon,QUEUE_CONNECTION=redis)
|
||||
```
|
||||
|
||||
## 🔑 API 能力一览(MVP-1.1 + Archive/GetMessage/SSE)
|
||||
- 会话:`POST /api/sessions`,`GET /api/sessions`(分页/状态/关键词),`GET /api/sessions/{id}`,`PATCH /api/sessions/{id}`(重命名/状态,CLOSED 不可重开),`POST /api/sessions/{id}/archive`(幂等归档→CLOSED)。
|
||||
- 消息:`POST /api/sessions/{id}/messages`(幂等 dedupe_key,CLOSED/LOCKED 门禁),`GET /api/sessions/{id}/messages`(after_seq 增量),`GET /api/sessions/{id}/messages/{message_id}`(校验 session_id)。
|
||||
- 实时:`GET /api/sessions/{id}/sse?after_seq=123`,SSE 事件 id 为 seq;`Last-Event-ID` 优先于 query。
|
||||
- Agent Run:user.prompt 自动触发;或 `POST /api/sessions/{id}/runs {trigger_message_id}` 手动触发,写入 `run.status/agent.message`。
|
||||
|
||||
详细字段/示例:`docs/ChatSession/chat-session-api.md`,OpenAPI:`docs/ChatSession/chat-session-openapi.yaml`。用户管理/鉴权文档:`docs/User/user-api.md`。
|
||||
|
||||
|
||||
@@ -9,12 +9,16 @@ use App\Http\Requests\UpdateSessionRequest;
|
||||
use App\Http\Resources\ChatSessionResource;
|
||||
use App\Http\Resources\MessageResource;
|
||||
use App\Services\ChatService;
|
||||
use App\Services\RunDispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ChatSessionController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ChatService $service)
|
||||
public function __construct(
|
||||
private readonly ChatService $service,
|
||||
private readonly RunDispatcher $runDispatcher,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -45,6 +49,10 @@ class ChatSessionController extends Controller
|
||||
'session_id' => $sessionId,
|
||||
...$request->validated(),
|
||||
]);
|
||||
|
||||
if ($message->role === 'USER' && $message->type === 'user.prompt') {
|
||||
$this->runDispatcher->dispatchForPrompt($sessionId, $message->message_id);
|
||||
}
|
||||
} catch (ChatSessionStatusException $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 403);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,12 @@ class ChatSessionSseController extends Controller
|
||||
$limit = (int) $request->query('limit', 200);
|
||||
$limit = $limit > 0 && $limit <= 500 ? $limit : 200;
|
||||
|
||||
if (app()->runningUnitTests() || app()->environment('testing') || ! class_exists(\Redis::class)) {
|
||||
$useBacklogOnly = app()->runningUnitTests()
|
||||
|| app()->environment('testing')
|
||||
|| defined('PHPUNIT_COMPOSER_INSTALL')
|
||||
|| ! class_exists(\Redis::class);
|
||||
|
||||
if ($useBacklogOnly) {
|
||||
$lastSentSeq = $afterSeq;
|
||||
ob_start();
|
||||
$this->sendBacklog($sessionId, $lastSentSeq, $limit, false);
|
||||
@@ -42,41 +47,75 @@ class ChatSessionSseController extends Controller
|
||||
|
||||
$this->sendBacklog($sessionId, $lastSentSeq, $limit);
|
||||
|
||||
$redis = Redis::connection()->client();
|
||||
if (method_exists($redis, 'setOption')) {
|
||||
$redis->setOption(\Redis::OPT_READ_TIMEOUT, 5);
|
||||
}
|
||||
|
||||
$channel = "session:{$sessionId}:messages";
|
||||
$pubSub = $redis->pubSubLoop();
|
||||
$pubSub->subscribe($channel);
|
||||
$lastPing = time();
|
||||
|
||||
foreach ($pubSub as $message) {
|
||||
if ($message->kind === 'subscribe') {
|
||||
continue;
|
||||
try {
|
||||
$redis = Redis::connection()->client();
|
||||
if (method_exists($redis, 'setOption')) {
|
||||
$redis->setOption(\Redis::OPT_READ_TIMEOUT, 5);
|
||||
}
|
||||
|
||||
if (connection_aborted()) {
|
||||
$pubSub->unsubscribe();
|
||||
break;
|
||||
}
|
||||
$channel = "session:{$sessionId}:messages";
|
||||
$lastPing = time();
|
||||
logger()->info('sse open');
|
||||
if (method_exists($redis, 'pubSubLoop')) {
|
||||
$pubSub = $redis->pubSubLoop();
|
||||
$pubSub->subscribe($channel);
|
||||
|
||||
$payloadId = $message->payload ?? null;
|
||||
if ($payloadId) {
|
||||
$msg = $this->service->getMessage($sessionId, $payloadId);
|
||||
if ($msg && $msg->seq > $lastSentSeq) {
|
||||
$this->emitMessage($msg);
|
||||
$lastSentSeq = $msg->seq;
|
||||
foreach ($pubSub as $message) {
|
||||
if ($message->kind === 'subscribe') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (connection_aborted()) {
|
||||
$pubSub->unsubscribe();
|
||||
break;
|
||||
}
|
||||
|
||||
$payloadId = $message->payload ?? null;
|
||||
if ($payloadId) {
|
||||
$msg = $this->service->getMessage($sessionId, $payloadId);
|
||||
if ($msg && $msg->seq > $lastSentSeq) {
|
||||
$this->emitMessage($msg);
|
||||
$lastSentSeq = $msg->seq;
|
||||
}
|
||||
}
|
||||
|
||||
if (time() - $lastPing >= 180) {
|
||||
logger()->info('ping: sent'.$sessionId);
|
||||
echo ": ping\n\n";
|
||||
@ob_flush();
|
||||
@flush();
|
||||
$lastPing = time();
|
||||
}
|
||||
}
|
||||
}
|
||||
logger()->info('close: sent'.$sessionId);
|
||||
unset($pubSub);
|
||||
} else {
|
||||
// Fallback for Redis drivers without pubSubLoop (older phpredis)
|
||||
$redis->subscribe([$channel], function ($redisInstance, $chan, $payload) use (&$lastSentSeq, $sessionId) {
|
||||
if (connection_aborted()) {
|
||||
$redisInstance->unsubscribe([$chan]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (time() - $lastPing >= 20) {
|
||||
echo ": ping\n\n";
|
||||
@ob_flush();
|
||||
@flush();
|
||||
$lastPing = time();
|
||||
if (! $payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
$msg = $this->service->getMessage($sessionId, $payload);
|
||||
if ($msg && $msg->seq > $lastSentSeq) {
|
||||
$this->emitMessage($msg);
|
||||
$lastSentSeq = $msg->seq;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (\RedisException $exception) {
|
||||
logger()->warning('SSE redis subscription failed', [
|
||||
'session_id' => $sessionId,
|
||||
'message' => $exception->getMessage(),
|
||||
]);
|
||||
echo ": redis-error\n\n";
|
||||
@ob_flush();
|
||||
@flush();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
28
app/Http/Controllers/RunController.php
Normal file
28
app/Http/Controllers/RunController.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\DispatchRunRequest;
|
||||
use App\Jobs\TestJob;
|
||||
use App\Services\RunDispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class RunController extends Controller
|
||||
{
|
||||
public function __construct(private readonly RunDispatcher $dispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
public function store(string $sessionId, DispatchRunRequest $request): JsonResponse
|
||||
{
|
||||
$runId = $this->dispatcher->dispatchForPrompt($sessionId, $request->validated()['trigger_message_id']);
|
||||
|
||||
return response()->json(['run_id' => $runId], 201);
|
||||
}
|
||||
|
||||
public function test()
|
||||
{
|
||||
$job = TestJob::dispatch();
|
||||
unset($job);
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/DispatchRunRequest.php
Normal file
23
app/Http/Requests/DispatchRunRequest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DispatchRunRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'trigger_message_id' => ['required', 'uuid'],
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Jobs/AgentRunJob.php
Normal file
35
app/Jobs/AgentRunJob.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Services\OutputSink;
|
||||
use App\Services\RunLoop;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class AgentRunJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public string $sessionId, public string $runId)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(RunLoop $loop, OutputSink $sink): void
|
||||
{
|
||||
try {
|
||||
$loop->run($this->sessionId, $this->runId);
|
||||
} catch (\Throwable $e) {
|
||||
$sink->appendError($this->sessionId, $this->runId, 'run.failed', $e->getMessage());
|
||||
$sink->appendRunStatus($this->sessionId, $this->runId, 'FAILED', [
|
||||
'error' => $e->getMessage(),
|
||||
'dedupe_key' => "run:{$this->runId}:status:FAILED",
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
app/Jobs/TestJob.php
Normal file
23
app/Jobs/TestJob.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class TestJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
logger('TestJob');
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->app->bind(\App\Services\Agent\AgentProviderInterface::class, function () {
|
||||
return new \App\Services\Agent\DummyAgentProvider();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
31
app/Providers/HorizonServiceProvider.php
Normal file
31
app/Providers/HorizonServiceProvider.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Horizon\Horizon;
|
||||
|
||||
class HorizonServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if (! class_exists(Horizon::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Horizon::auth(function ($request) {
|
||||
return app()->environment('local');
|
||||
});
|
||||
}
|
||||
}
|
||||
55
app/Providers/TelescopeServiceProvider.php
Normal file
55
app/Providers/TelescopeServiceProvider.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Telescope\IncomingEntry;
|
||||
use Laravel\Telescope\Telescope;
|
||||
use Laravel\Telescope\TelescopeApplicationServiceProvider;
|
||||
|
||||
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
Telescope::night();
|
||||
|
||||
$this->hideSensitiveRequestDetails();
|
||||
|
||||
$isLocal = $this->app->environment('local');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent sensitive request details from being logged by Telescope.
|
||||
*/
|
||||
protected function hideSensitiveRequestDetails(): void
|
||||
{
|
||||
if ($this->app->environment('local')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Telescope::hideRequestParameters(['_token']);
|
||||
|
||||
Telescope::hideRequestHeaders([
|
||||
'cookie',
|
||||
'x-csrf-token',
|
||||
'x-xsrf-token',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Telescope gate.
|
||||
*
|
||||
* This gate determines who can access Telescope in non-local environments.
|
||||
*/
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewTelescope', function ($user) {
|
||||
return in_array($user->email, [
|
||||
//
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
12
app/Services/Agent/AgentProviderInterface.php
Normal file
12
app/Services/Agent/AgentProviderInterface.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent;
|
||||
|
||||
interface AgentProviderInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function generate(array $context, array $options = []): string;
|
||||
}
|
||||
26
app/Services/Agent/DummyAgentProvider.php
Normal file
26
app/Services/Agent/DummyAgentProvider.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent;
|
||||
|
||||
class DummyAgentProvider implements AgentProviderInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function generate(array $context, array $options = []): string
|
||||
{
|
||||
$messages = $context['messages'] ?? [];
|
||||
$lastUser = null;
|
||||
foreach (array_reverse($messages) as $msg) {
|
||||
if (($msg['role'] ?? '') === 'USER' && ($msg['type'] ?? '') === 'user.prompt') {
|
||||
$lastUser = $msg['content'] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$summary = $lastUser ? mb_substr($lastUser, 0, 80) : 'no user prompt';
|
||||
|
||||
return sprintf('MVP reply: based on last user input -> %s', $summary);
|
||||
}
|
||||
}
|
||||
42
app/Services/Agent/HttpAgentProvider.php
Normal file
42
app/Services/Agent/HttpAgentProvider.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class HttpAgentProvider implements AgentProviderInterface
|
||||
{
|
||||
protected string $endpoint;
|
||||
|
||||
public function __construct(?string $endpoint = null)
|
||||
{
|
||||
$this->endpoint = $endpoint ?? config('services.agent_provider.endpoint', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function generate(array $context, array $options = []): string
|
||||
{
|
||||
if (empty($this->endpoint)) {
|
||||
// placeholder to avoid accidental outbound calls when未配置
|
||||
return (new DummyAgentProvider())->generate($context, $options);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'context' => $context,
|
||||
'options' => $options,
|
||||
];
|
||||
|
||||
$response = Http::post($this->endpoint, $payload);
|
||||
|
||||
if (! $response->successful()) {
|
||||
throw new \RuntimeException('Agent provider failed: '.$response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
return is_string($data) ? $data : ($data['content'] ?? '');
|
||||
}
|
||||
}
|
||||
18
app/Services/CancelChecker.php
Normal file
18
app/Services/CancelChecker.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Message;
|
||||
|
||||
class CancelChecker
|
||||
{
|
||||
public function isCanceled(string $sessionId, string $runId): bool
|
||||
{
|
||||
return Message::query()
|
||||
->where('session_id', $sessionId)
|
||||
->where('type', 'run.cancel.request')
|
||||
->whereIn('role', [Message::ROLE_USER, Message::ROLE_SYSTEM])
|
||||
->where('payload->run_id', $runId)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
49
app/Services/ContextBuilder.php
Normal file
49
app/Services/ContextBuilder.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Message;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ContextBuilder
|
||||
{
|
||||
public function __construct(private readonly int $limit = 20)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function build(string $sessionId, string $runId): array
|
||||
{
|
||||
$messages = $this->loadRecentMessages($sessionId);
|
||||
|
||||
return [
|
||||
'run_id' => $runId,
|
||||
'session_id' => $sessionId,
|
||||
'system_prompt' => 'You are an agent inside ARS. Respond concisely in plain text.',
|
||||
'messages' => $messages->map(function (Message $message) {
|
||||
return [
|
||||
'message_id' => $message->message_id,
|
||||
'role' => $message->role,
|
||||
'type' => $message->type,
|
||||
'content' => $message->content,
|
||||
'seq' => $message->seq,
|
||||
];
|
||||
})->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function loadRecentMessages(string $sessionId): Collection
|
||||
{
|
||||
return Message::query()
|
||||
->where('session_id', $sessionId)
|
||||
->whereIn('role', [Message::ROLE_USER, Message::ROLE_AGENT])
|
||||
->whereIn('type', ['user.prompt', 'agent.message'])
|
||||
->orderByDesc('seq')
|
||||
->limit($this->limit)
|
||||
->get()
|
||||
->sortBy('seq')
|
||||
->values();
|
||||
}
|
||||
}
|
||||
63
app/Services/OutputSink.php
Normal file
63
app/Services/OutputSink.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Message;
|
||||
|
||||
class OutputSink
|
||||
{
|
||||
public function __construct(private readonly ChatService $chatService)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $meta
|
||||
*/
|
||||
public function appendAgentMessage(string $sessionId, string $runId, string $content, array $meta = []): Message
|
||||
{
|
||||
return $this->chatService->appendMessage([
|
||||
'session_id' => $sessionId,
|
||||
'role' => Message::ROLE_AGENT,
|
||||
'type' => 'agent.message',
|
||||
'content' => $content,
|
||||
'payload' => array_merge($meta, ['run_id' => $runId]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $meta
|
||||
*/
|
||||
public function appendRunStatus(string $sessionId, string $runId, string $status, array $meta = []): Message
|
||||
{
|
||||
$dedupeKey = $meta['dedupe_key'] ?? null;
|
||||
unset($meta['dedupe_key']);
|
||||
|
||||
return $this->chatService->appendMessage([
|
||||
'session_id' => $sessionId,
|
||||
'role' => Message::ROLE_SYSTEM,
|
||||
'type' => 'run.status',
|
||||
'payload' => array_merge($meta, [
|
||||
'run_id' => $runId,
|
||||
'status' => $status,
|
||||
]),
|
||||
'dedupe_key' => $dedupeKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $meta
|
||||
*/
|
||||
public function appendError(string $sessionId, string $runId, string $code, string $message, array $meta = []): Message
|
||||
{
|
||||
return $this->chatService->appendMessage([
|
||||
'session_id' => $sessionId,
|
||||
'role' => Message::ROLE_SYSTEM,
|
||||
'type' => 'error',
|
||||
'content' => $code,
|
||||
'payload' => array_merge($meta, [
|
||||
'run_id' => $runId,
|
||||
'message' => $message,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
60
app/Services/RunDispatcher.php
Normal file
60
app/Services/RunDispatcher.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\AgentRunJob;
|
||||
use App\Models\Message;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RunDispatcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ChatService $chatService,
|
||||
private readonly OutputSink $outputSink,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ModelNotFoundException
|
||||
*/
|
||||
public function dispatchForPrompt(string $sessionId, string $triggerMessageId): string
|
||||
{
|
||||
$triggerMessage = $this->chatService->getMessage($sessionId, $triggerMessageId);
|
||||
if (! $triggerMessage) {
|
||||
throw (new ModelNotFoundException())->setModel(Message::class, [$triggerMessageId]);
|
||||
}
|
||||
|
||||
$existingForTrigger = Message::query()
|
||||
->where('session_id', $sessionId)
|
||||
->where('type', 'run.status')
|
||||
->where('payload->trigger_message_id', $triggerMessageId)
|
||||
->orderByDesc('seq')
|
||||
->first();
|
||||
|
||||
if ($existingForTrigger && ($existingForTrigger->payload['run_id'] ?? null)) {
|
||||
return $existingForTrigger->payload['run_id'];
|
||||
}
|
||||
|
||||
$latestStatus = Message::query()
|
||||
->where('session_id', $sessionId)
|
||||
->where('type', 'run.status')
|
||||
->orderByDesc('seq')
|
||||
->first();
|
||||
|
||||
if ($latestStatus && ($latestStatus->payload['status'] ?? null) === 'RUNNING' && ($latestStatus->payload['run_id'] ?? null)) {
|
||||
return $latestStatus->payload['run_id'];
|
||||
}
|
||||
|
||||
$runId = (string) Str::uuid();
|
||||
|
||||
$this->outputSink->appendRunStatus($sessionId, $runId, 'RUNNING', [
|
||||
'trigger_message_id' => $triggerMessageId,
|
||||
'dedupe_key' => 'run:trigger:'.$triggerMessageId,
|
||||
]);
|
||||
|
||||
dispatch(new AgentRunJob($sessionId, $runId));
|
||||
|
||||
return $runId;
|
||||
}
|
||||
}
|
||||
52
app/Services/RunLoop.php
Normal file
52
app/Services/RunLoop.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Services\Agent\AgentProviderInterface;
|
||||
use App\Services\Agent\DummyAgentProvider;
|
||||
|
||||
class RunLoop
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContextBuilder $contextBuilder,
|
||||
private readonly AgentProviderInterface $provider,
|
||||
private readonly OutputSink $outputSink,
|
||||
private readonly CancelChecker $cancelChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
public function run(string $sessionId, string $runId): void
|
||||
{
|
||||
if ($this->cancelChecker->isCanceled($sessionId, $runId)) {
|
||||
$this->outputSink->appendRunStatus($sessionId, $runId, 'CANCELED', [
|
||||
'dedupe_key' => "run:{$runId}:status:CANCELED",
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$context = $this->contextBuilder->build($sessionId, $runId);
|
||||
|
||||
if ($this->cancelChecker->isCanceled($sessionId, $runId)) {
|
||||
$this->outputSink->appendRunStatus($sessionId, $runId, 'CANCELED', [
|
||||
'dedupe_key' => "run:{$runId}:status:CANCELED",
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$reply = $this->provider->generate($context);
|
||||
|
||||
if ($this->cancelChecker->isCanceled($sessionId, $runId)) {
|
||||
$this->outputSink->appendRunStatus($sessionId, $runId, 'CANCELED', [
|
||||
'dedupe_key' => "run:{$runId}:status:CANCELED",
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->outputSink->appendAgentMessage($sessionId, $runId, $reply, [
|
||||
'provider' => $this->provider instanceof DummyAgentProvider ? 'dummy' : get_class($this->provider),
|
||||
]);
|
||||
$this->outputSink->appendRunStatus($sessionId, $runId, 'DONE', [
|
||||
'dedupe_key' => "run:{$runId}:status:DONE",
|
||||
]);
|
||||
}
|
||||
}
|
||||
14
boost.json
Normal file
14
boost.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"agents": [
|
||||
"claude_code",
|
||||
"codex",
|
||||
"phpstorm"
|
||||
],
|
||||
"editors": [
|
||||
"claude_code",
|
||||
"codex",
|
||||
"phpstorm"
|
||||
],
|
||||
"guidelines": [],
|
||||
"sail": true
|
||||
}
|
||||
@@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withProviders([
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
])
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->append(HandleCors::class);
|
||||
$middleware->alias([
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\TelescopeServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -8,12 +8,16 @@
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/horizon": "^5.40",
|
||||
"laravel/octane": "^2.13",
|
||||
"laravel/telescope": "^5.16",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"php-open-source-saver/jwt-auth": "^2.8"
|
||||
"php-open-source-saver/jwt-auth": "^2.8",
|
||||
"ext-redis": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/boost": "^1.8",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
|
||||
402
composer.lock
generated
402
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "79f1e234537460fac440cd9aa68d3e6b",
|
||||
"content-hash": "effce82b9c1c86b0542543b2f1034edc",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -1142,16 +1142,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v12.42.0",
|
||||
"version": "v12.43.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "509b33095564c5165366d81bbaa0afaac28abe75"
|
||||
"reference": "9f875fad08f5d409b4c33293eca34f7af36e8ecf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/509b33095564c5165366d81bbaa0afaac28abe75",
|
||||
"reference": "509b33095564c5165366d81bbaa0afaac28abe75",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/9f875fad08f5d409b4c33293eca34f7af36e8ecf",
|
||||
"reference": "9f875fad08f5d409b4c33293eca34f7af36e8ecf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1360,20 +1360,99 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-12-09T15:51:23+00:00"
|
||||
"time": "2025-12-16T15:27:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/octane",
|
||||
"version": "v2.13.2",
|
||||
"name": "laravel/horizon",
|
||||
"version": "v5.41.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/octane.git",
|
||||
"reference": "5b963d2da879f2cad3a84f22bafd3d8be7170988"
|
||||
"url": "https://github.com/laravel/horizon.git",
|
||||
"reference": "eb6738246ab9d3450b705126b9794dfb0ea371b3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/octane/zipball/5b963d2da879f2cad3a84f22bafd3d8be7170988",
|
||||
"reference": "5b963d2da879f2cad3a84f22bafd3d8be7170988",
|
||||
"url": "https://api.github.com/repos/laravel/horizon/zipball/eb6738246ab9d3450b705126b9794dfb0ea371b3",
|
||||
"reference": "eb6738246ab9d3450b705126b9794dfb0ea371b3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-pcntl": "*",
|
||||
"ext-posix": "*",
|
||||
"illuminate/contracts": "^9.21|^10.0|^11.0|^12.0",
|
||||
"illuminate/queue": "^9.21|^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^9.21|^10.0|^11.0|^12.0",
|
||||
"nesbot/carbon": "^2.17|^3.0",
|
||||
"php": "^8.0",
|
||||
"ramsey/uuid": "^4.0",
|
||||
"symfony/console": "^6.0|^7.0",
|
||||
"symfony/error-handler": "^6.0|^7.0",
|
||||
"symfony/polyfill-php83": "^1.28",
|
||||
"symfony/process": "^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.0",
|
||||
"orchestra/testbench": "^7.55|^8.36|^9.15|^10.8",
|
||||
"phpstan/phpstan": "^1.10|^2.0",
|
||||
"predis/predis": "^1.1|^2.0|^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-redis": "Required to use the Redis PHP driver.",
|
||||
"predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0|^3.0)."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Horizon": "Laravel\\Horizon\\Horizon"
|
||||
},
|
||||
"providers": [
|
||||
"Laravel\\Horizon\\HorizonServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "6.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Horizon\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Dashboard and code-driven configuration for Laravel queues.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"queue"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/horizon/issues",
|
||||
"source": "https://github.com/laravel/horizon/tree/v5.41.0"
|
||||
},
|
||||
"time": "2025-12-14T15:55:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/octane",
|
||||
"version": "v2.13.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/octane.git",
|
||||
"reference": "aae775360fceae422651042d73137fff092ba800"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/octane/zipball/aae775360fceae422651042d73137fff092ba800",
|
||||
"reference": "aae775360fceae422651042d73137fff092ba800",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1450,7 +1529,7 @@
|
||||
"issues": "https://github.com/laravel/octane/issues",
|
||||
"source": "https://github.com/laravel/octane"
|
||||
},
|
||||
"time": "2025-11-28T20:13:00+00:00"
|
||||
"time": "2025-12-10T15:24:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
@@ -1572,6 +1651,74 @@
|
||||
},
|
||||
"time": "2025-11-21T20:52:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/telescope",
|
||||
"version": "v5.16.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/telescope.git",
|
||||
"reference": "a868e91a0912d6a44363636f7467a8578db83026"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/telescope/zipball/a868e91a0912d6a44363636f7467a8578db83026",
|
||||
"reference": "a868e91a0912d6a44363636f7467a8578db83026",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0",
|
||||
"php": "^8.0",
|
||||
"symfony/console": "^5.3|^6.0|^7.0",
|
||||
"symfony/var-dumper": "^5.0|^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-gd": "*",
|
||||
"guzzlehttp/guzzle": "^6.0|^7.0",
|
||||
"laravel/octane": "^1.4|^2.0",
|
||||
"orchestra/testbench": "^6.47.1|^7.55|^8.36|^9.15|^10.8",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Telescope\\TelescopeServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Telescope\\": "src/",
|
||||
"Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
},
|
||||
{
|
||||
"name": "Mohamed Said",
|
||||
"email": "mohamed@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "An elegant debug assistant for the Laravel framework.",
|
||||
"keywords": [
|
||||
"debugging",
|
||||
"laravel",
|
||||
"monitoring"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/telescope/issues",
|
||||
"source": "https://github.com/laravel/telescope/tree/v5.16.0"
|
||||
},
|
||||
"time": "2025-12-09T13:34:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/tinker",
|
||||
"version": "v2.10.2",
|
||||
@@ -3429,16 +3576,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psy/psysh",
|
||||
"version": "v0.12.16",
|
||||
"version": "v0.12.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bobthecow/psysh.git",
|
||||
"reference": "ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67"
|
||||
"reference": "85fbbd9f3064e157fc21fe4362b2b5c19f2ea631"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67",
|
||||
"reference": "ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67",
|
||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/85fbbd9f3064e157fc21fe4362b2b5c19f2ea631",
|
||||
"reference": "85fbbd9f3064e157fc21fe4362b2b5c19f2ea631",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3502,9 +3649,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/bobthecow/psysh/issues",
|
||||
"source": "https://github.com/bobthecow/psysh/tree/v0.12.16"
|
||||
"source": "https://github.com/bobthecow/psysh/tree/v0.12.17"
|
||||
},
|
||||
"time": "2025-12-07T03:39:01+00:00"
|
||||
"time": "2025-12-15T04:55:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ralouphie/getallheaders",
|
||||
@@ -3628,20 +3775,20 @@
|
||||
},
|
||||
{
|
||||
"name": "ramsey/uuid",
|
||||
"version": "4.9.1",
|
||||
"version": "4.9.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ramsey/uuid.git",
|
||||
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
|
||||
"reference": "8429c78ca35a09f27565311b98101e2826affde0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
|
||||
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
|
||||
"url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0",
|
||||
"reference": "8429c78ca35a09f27565311b98101e2826affde0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
|
||||
"brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
|
||||
"php": "^8.0",
|
||||
"ramsey/collection": "^1.2 || ^2.0"
|
||||
},
|
||||
@@ -3700,9 +3847,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ramsey/uuid/issues",
|
||||
"source": "https://github.com/ramsey/uuid/tree/4.9.1"
|
||||
"source": "https://github.com/ramsey/uuid/tree/4.9.2"
|
||||
},
|
||||
"time": "2025-09-04T20:59:21+00:00"
|
||||
"time": "2025-12-14T04:43:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
@@ -6761,6 +6908,145 @@
|
||||
},
|
||||
"time": "2025-04-30T06:54:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/boost",
|
||||
"version": "v1.8.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/boost.git",
|
||||
"reference": "99c15c392f3c6f049f0671dd5dc7b6e9a75cfe7e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/boost/zipball/99c15c392f3c6f049f0671dd5dc7b6e9a75cfe7e",
|
||||
"reference": "99c15c392f3c6f049f0671dd5dc7b6e9a75cfe7e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"illuminate/console": "^10.49.0|^11.45.3|^12.28.1",
|
||||
"illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1",
|
||||
"illuminate/routing": "^10.49.0|^11.45.3|^12.28.1",
|
||||
"illuminate/support": "^10.49.0|^11.45.3|^12.28.1",
|
||||
"laravel/mcp": "^0.4.1",
|
||||
"laravel/prompts": "0.1.25|^0.3.6",
|
||||
"laravel/roster": "^0.2.9",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.20.0",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"orchestra/testbench": "^8.36.0|^9.15.0|^10.6",
|
||||
"pestphp/pest": "^2.36.0|^3.8.4|^4.1.5",
|
||||
"phpstan/phpstan": "^2.1.27",
|
||||
"rector/rector": "^2.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Boost\\BoostServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Boost\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.",
|
||||
"homepage": "https://github.com/laravel/boost",
|
||||
"keywords": [
|
||||
"ai",
|
||||
"dev",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/boost/issues",
|
||||
"source": "https://github.com/laravel/boost"
|
||||
},
|
||||
"time": "2025-12-08T21:54:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/mcp",
|
||||
"version": "v0.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/mcp.git",
|
||||
"reference": "1c7878be3931a19768f791ddf141af29f43fb4ef"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/mcp/zipball/1c7878be3931a19768f791ddf141af29f43fb4ef",
|
||||
"reference": "1c7878be3931a19768f791ddf141af29f43fb4ef",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"illuminate/console": "^10.49.0|^11.45.3|^12.41.1",
|
||||
"illuminate/container": "^10.49.0|^11.45.3|^12.41.1",
|
||||
"illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1",
|
||||
"illuminate/http": "^10.49.0|^11.45.3|^12.41.1",
|
||||
"illuminate/json-schema": "^12.41.1",
|
||||
"illuminate/routing": "^10.49.0|^11.45.3|^12.41.1",
|
||||
"illuminate/support": "^10.49.0|^11.45.3|^12.41.1",
|
||||
"illuminate/validation": "^10.49.0|^11.45.3|^12.41.1",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.20",
|
||||
"orchestra/testbench": "^8.36|^9.15|^10.8",
|
||||
"pestphp/pest": "^2.36.0|^3.8.4|^4.1.0",
|
||||
"phpstan/phpstan": "^2.1.27",
|
||||
"rector/rector": "^2.2.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp"
|
||||
},
|
||||
"providers": [
|
||||
"Laravel\\Mcp\\Server\\McpServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Mcp\\": "src/",
|
||||
"Laravel\\Mcp\\Server\\": "src/Server/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Rapidly build MCP servers for your Laravel applications.",
|
||||
"homepage": "https://github.com/laravel/mcp",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"mcp"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/mcp/issues",
|
||||
"source": "https://github.com/laravel/mcp"
|
||||
},
|
||||
"time": "2025-12-07T15:49:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pail",
|
||||
"version": "v1.2.4",
|
||||
@@ -6907,6 +7193,67 @@
|
||||
},
|
||||
"time": "2025-11-25T21:15:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/roster",
|
||||
"version": "v0.2.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/roster.git",
|
||||
"reference": "82bbd0e2de614906811aebdf16b4305956816fa6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/roster/zipball/82bbd0e2de614906811aebdf16b4305956816fa6",
|
||||
"reference": "82bbd0e2de614906811aebdf16b4305956816fa6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/console": "^10.0|^11.0|^12.0",
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0",
|
||||
"illuminate/routing": "^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||
"php": "^8.1|^8.2",
|
||||
"symfony/yaml": "^6.4|^7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.14",
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench": "^8.22.0|^9.0|^10.0",
|
||||
"pestphp/pest": "^2.0|^3.0",
|
||||
"phpstan/phpstan": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Roster\\RosterServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Roster\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Detect packages & approaches in use within a Laravel project",
|
||||
"homepage": "https://github.com/laravel/roster",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/roster/issues",
|
||||
"source": "https://github.com/laravel/roster"
|
||||
},
|
||||
"time": "2025-10-20T09:56:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/sail",
|
||||
"version": "v1.51.0",
|
||||
@@ -8945,7 +9292,8 @@
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.2"
|
||||
"php": "^8.2",
|
||||
"ext-redis": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
|
||||
186
config/horizon.php
Normal file
186
config/horizon.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the subdomain where Horizon will be accessible from. If this
|
||||
| setting is null, Horizon will reside under the same domain as the
|
||||
| application. Otherwise, this value will serve as the subdomain.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('HORIZON_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the URI path where Horizon will be accessible from. Feel free
|
||||
| to change this path to anything you like. Note that the URI will not
|
||||
| affect the paths of its internal API that aren't exposed to users.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('HORIZON_PATH', 'horizon'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the name of the Redis connection where Horizon will store the
|
||||
| meta information required for it to function. It includes the list
|
||||
| of supervisors, failed and completed jobs, job metrics, and more.
|
||||
|
|
||||
*/
|
||||
|
||||
'use' => 'default',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This prefix will be used when storing all Horizon data in Redis. You
|
||||
| may modify the prefix when you are running multiple installations
|
||||
| of Horizon on the same server so that they don't have problems.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('HORIZON_PREFIX', 'horizon:{single}'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Route Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These middleware will be assigned to every Horizon route, giving you the
|
||||
| chance to add your own middleware to this list or change any of the
|
||||
| existing middleware. Or, you can simply stick with this default list.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Wait Time Thresholds
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to configure when Horizon will notify you the
|
||||
| queue is waiting too long to process jobs. Each connection / queue
|
||||
| combination may have its own, unique threshold (in seconds).
|
||||
|
|
||||
*/
|
||||
|
||||
'waits' => [
|
||||
'redis:default' => env('HORIZON_WAIT_TIME', 60),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Trimming Times
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||
| for one hour while all failed jobs are stored for an entire week.
|
||||
|
|
||||
*/
|
||||
|
||||
'trim' => [
|
||||
'recent' => 60,
|
||||
'pending' => 60,
|
||||
'completed' => 60,
|
||||
'recent_failed' => 10080,
|
||||
'failed' => 10080,
|
||||
'monitored' => 1440,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Silenced Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Silent jobs are not displayed in the Horizon UI. This allows you to
|
||||
| fully avoid showing them or incrementing the failed jobs count
|
||||
| or displaying them on the monitoring charts. They're silenced.
|
||||
|
|
||||
*/
|
||||
|
||||
'silenced' => [
|
||||
// App\Jobs\QuietJob::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fast Termination
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When this option is enabled, Horizon's "terminate" command will not
|
||||
| wait on all of the workers to terminate unless the --wait option
|
||||
| is provided. Fast termination can shorten deployment delay by
|
||||
| allowing a new instance of Horizon to start immediately.
|
||||
|
|
||||
*/
|
||||
|
||||
'fast_termination' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Memory Limit (MB)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value describes the maximum amount of memory the Horizon worker
|
||||
| may consume before it is terminated and restarted. You should set
|
||||
| this value according to the resources available to your server.
|
||||
|
|
||||
*/
|
||||
|
||||
'memory_limit' => env('HORIZON_MEMORY_LIMIT', 128),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Worker Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the queue worker settings used by your application
|
||||
| in all environments. These supervisors and settings handle all your
|
||||
| queued jobs and will be provisioned by Horizon during deployment.
|
||||
|
|
||||
*/
|
||||
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-default' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'auto',
|
||||
'maxProcesses' => 10,
|
||||
'maxTime' => 60,
|
||||
'maxJobs' => 1000,
|
||||
'memory' => 256,
|
||||
'tries' => 3,
|
||||
],
|
||||
],
|
||||
|
||||
'local' => [
|
||||
'supervisor-default' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'simple',
|
||||
'maxProcesses' => env('HORIZON_PROCESSES', 3),
|
||||
'maxTime' => 60,
|
||||
'maxJobs' => 500,
|
||||
'memory' => 256,
|
||||
'tries' => 3,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -32,7 +32,7 @@ return [
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
'driver' => 'redis',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
|
||||
212
config/telescope.php
Normal file
212
config/telescope.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Telescope\Http\Middleware\Authorize;
|
||||
use Laravel\Telescope\Watchers;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Telescope Master Switch
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option may be used to disable all Telescope watchers regardless
|
||||
| of their individual configuration, which simply provides a single
|
||||
| and convenient way to enable or disable Telescope data storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'enabled' => env('TELESCOPE_ENABLED', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Telescope Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the subdomain where Telescope will be accessible from. If the
|
||||
| setting is null, Telescope will reside under the same domain as the
|
||||
| application. Otherwise, this value will be used as the subdomain.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('TELESCOPE_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Telescope Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the URI path where Telescope will be accessible from. Feel free
|
||||
| to change this path to anything you like. Note that the URI will not
|
||||
| affect the paths of its internal API that aren't exposed to users.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('TELESCOPE_PATH', 'telescope'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Telescope Storage Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration options determines the storage driver that will
|
||||
| be used to store Telescope's data. In addition, you may set any
|
||||
| custom options as needed by the particular driver you choose.
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('TELESCOPE_DRIVER', 'database'),
|
||||
|
||||
'storage' => [
|
||||
'database' => [
|
||||
'connection' => env('DB_CONNECTION', 'mysql'),
|
||||
'chunk' => 1000,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Telescope Queue
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration options determines the queue connection and queue
|
||||
| which will be used to process ProcessPendingUpdate jobs. This can
|
||||
| be changed if you would prefer to use a non-default connection.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => [
|
||||
'connection' => env('TELESCOPE_QUEUE_CONNECTION'),
|
||||
'queue' => env('TELESCOPE_QUEUE'),
|
||||
'delay' => env('TELESCOPE_QUEUE_DELAY', 10),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Telescope Route Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These middleware will be assigned to every Telescope route, giving you
|
||||
| the chance to add your own middleware to this list or change any of
|
||||
| the existing middleware. Or, you can simply stick with this list.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'web',
|
||||
Authorize::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowed / Ignored Paths & Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following array lists the URI paths and Artisan commands that will
|
||||
| not be watched by Telescope. In addition to this list, some Laravel
|
||||
| commands, like migrations and queue commands, are always ignored.
|
||||
|
|
||||
*/
|
||||
|
||||
'only_paths' => [
|
||||
// 'api/*'
|
||||
],
|
||||
|
||||
'ignore_paths' => [
|
||||
'livewire*',
|
||||
'nova-api*',
|
||||
'pulse*',
|
||||
'_boost*',
|
||||
'telescope/*'
|
||||
],
|
||||
|
||||
'ignore_commands' => [
|
||||
//
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Telescope Watchers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following array lists the "watchers" that will be registered with
|
||||
| Telescope. The watchers gather the application's profile data when
|
||||
| a request or task is executed. Feel free to customize this list.
|
||||
|
|
||||
*/
|
||||
|
||||
'watchers' => [
|
||||
Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true),
|
||||
|
||||
Watchers\CacheWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_CACHE_WATCHER', true),
|
||||
'hidden' => [],
|
||||
'ignore' => [],
|
||||
],
|
||||
|
||||
Watchers\ClientRequestWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true),
|
||||
'ignore_hosts' => [],
|
||||
],
|
||||
|
||||
Watchers\CommandWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
|
||||
'ignore' => [],
|
||||
],
|
||||
|
||||
Watchers\DumpWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_DUMP_WATCHER', true),
|
||||
'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false),
|
||||
],
|
||||
|
||||
Watchers\EventWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_EVENT_WATCHER', true),
|
||||
'ignore' => [],
|
||||
],
|
||||
|
||||
Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
|
||||
|
||||
Watchers\GateWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_GATE_WATCHER', true),
|
||||
'ignore_abilities' => [],
|
||||
'ignore_packages' => true,
|
||||
'ignore_paths' => [],
|
||||
],
|
||||
|
||||
Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
|
||||
|
||||
Watchers\LogWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_LOG_WATCHER', true),
|
||||
'level' => 'error',
|
||||
],
|
||||
|
||||
Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
|
||||
|
||||
Watchers\ModelWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
|
||||
'events' => ['eloquent.*'],
|
||||
'hydrations' => true,
|
||||
],
|
||||
|
||||
Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
|
||||
|
||||
Watchers\QueryWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
|
||||
'ignore_packages' => true,
|
||||
'ignore_paths' => [],
|
||||
'slow' => 100,
|
||||
],
|
||||
|
||||
Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true),
|
||||
|
||||
Watchers\RequestWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
|
||||
'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
|
||||
'ignore_http_methods' => [],
|
||||
'ignore_status_codes' => [],
|
||||
],
|
||||
|
||||
Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
|
||||
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Get the migration connection name.
|
||||
*/
|
||||
public function getConnection(): ?string
|
||||
{
|
||||
return config('telescope.storage.database.connection');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$schema = Schema::connection($this->getConnection());
|
||||
|
||||
$schema->create('telescope_entries', function (Blueprint $table) {
|
||||
$table->bigIncrements('sequence');
|
||||
$table->uuid('uuid');
|
||||
$table->uuid('batch_id');
|
||||
$table->string('family_hash')->nullable();
|
||||
$table->boolean('should_display_on_index')->default(true);
|
||||
$table->string('type', 20);
|
||||
$table->longText('content');
|
||||
$table->dateTime('created_at')->nullable();
|
||||
|
||||
$table->unique('uuid');
|
||||
$table->index('batch_id');
|
||||
$table->index('family_hash');
|
||||
$table->index('created_at');
|
||||
$table->index(['type', 'should_display_on_index']);
|
||||
});
|
||||
|
||||
$schema->create('telescope_entries_tags', function (Blueprint $table) {
|
||||
$table->uuid('entry_uuid');
|
||||
$table->string('tag');
|
||||
|
||||
$table->primary(['entry_uuid', 'tag']);
|
||||
$table->index('tag');
|
||||
|
||||
$table->foreign('entry_uuid')
|
||||
->references('uuid')
|
||||
->on('telescope_entries')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
|
||||
$schema->create('telescope_monitoring', function (Blueprint $table) {
|
||||
$table->string('tag')->primary();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$schema = Schema::connection($this->getConnection());
|
||||
|
||||
$schema->dropIfExists('telescope_entries_tags');
|
||||
$schema->dropIfExists('telescope_entries');
|
||||
$schema->dropIfExists('telescope_monitoring');
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/app/Dockerfile
|
||||
image: ars_backend:latest
|
||||
entrypoint: ["/app/docker/app/entrypoint.sh"]
|
||||
volumes:
|
||||
- ./:/app
|
||||
@@ -20,6 +21,7 @@ services:
|
||||
REDIS_HOST: redis
|
||||
REDIS_PASSWORD: "null"
|
||||
REDIS_PORT: 6379
|
||||
QUEUE_CONNECTION: redis
|
||||
ports:
|
||||
- "8000:8000"
|
||||
extra_hosts:
|
||||
@@ -29,6 +31,35 @@ services:
|
||||
- redis
|
||||
tty: true
|
||||
|
||||
horizon:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/app/Dockerfile
|
||||
image: ars_backend:latest
|
||||
entrypoint: ["/app/docker/app/entrypoint.sh"]
|
||||
environment:
|
||||
CONTAINER_ROLE: horizon
|
||||
APP_ENV: local
|
||||
APP_DEBUG: "true"
|
||||
APP_URL: http://localhost:8000
|
||||
OCTANE_SERVER: frankenphp
|
||||
DB_CONNECTION: pgsql
|
||||
DB_HOST: pgsql
|
||||
DB_PORT: 5432
|
||||
DB_DATABASE: ars_backend
|
||||
DB_USERNAME: ars
|
||||
DB_PASSWORD: secret
|
||||
REDIS_HOST: redis
|
||||
REDIS_PASSWORD: "null"
|
||||
REDIS_PORT: 6379
|
||||
QUEUE_CONNECTION: redis
|
||||
volumes:
|
||||
- ./:/app
|
||||
depends_on:
|
||||
- pgsql
|
||||
- redis
|
||||
tty: true
|
||||
|
||||
pgsql:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM dunglas/frankenphp:1-php8.3
|
||||
FROM dunglas/frankenphp:1-php8.2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -15,4 +15,15 @@ if ! grep -q "^APP_KEY=" .env 2>/dev/null || grep -q "^APP_KEY=$" .env 2>/dev/nu
|
||||
php artisan key:generate --force
|
||||
fi
|
||||
|
||||
exec php artisan octane:start --server=frankenphp --host=0.0.0.0 --port="${PORT:-8000}" --watch
|
||||
ROLE="${CONTAINER_ROLE:-app}"
|
||||
|
||||
if [ "$ROLE" = "queue" ]; then
|
||||
exec php artisan queue:work --verbose --tries="${QUEUE_TRIES:-3}" --timeout="${QUEUE_TIMEOUT:-90}"
|
||||
fi
|
||||
|
||||
if [ "$ROLE" = "horizon" ]; then
|
||||
exec php artisan horizon
|
||||
fi
|
||||
|
||||
exec php artisan octane:start --server=frankenphp --host=0.0.0.0 --port="${PORT:-8000}"
|
||||
#exec php -S 0.0.0.0:8000 -t public
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ChatSession & Message API(MVP-1)
|
||||
# ChatSession & Message API(MVP-1 + Agent Run MVP-0)
|
||||
|
||||
基地址:`http://localhost:8000/api`(FrankenPHP 容器 8000 端口)
|
||||
认证方式:JWT,`Authorization: Bearer {token}`
|
||||
@@ -7,6 +7,7 @@
|
||||
## 变更记录
|
||||
- 2025-02-14:新增 ChatSession 创建、消息追加、增量查询接口;支持状态门禁与 dedupe 幂等。
|
||||
- 2025-02-14:MVP-1.1 增加会话列表、会话更新(重命名/状态变更),列表附带最后一条消息摘要。
|
||||
- 2025-02-15:Agent Run MVP-0 —— RunDispatcher + AgentRunJob + DummyProvider;自动在 user.prompt 后触发一次 Run,落地 run.status / agent.message。
|
||||
|
||||
## 领域模型
|
||||
- `ChatSession`:`session_id`(UUID)、`session_name`、`status`(`OPEN`/`LOCKED`/`CLOSED`)、`last_seq`
|
||||
@@ -19,23 +20,26 @@
|
||||
### 创建会话
|
||||
- `POST /sessions`
|
||||
- 请求体字段
|
||||
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| session_name | 否 | string(≤255) | 会话名称 |
|
||||
- 响应 201(JSON)
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| session_id | uuid | 主键 |
|
||||
| session_name | string|null | 会话名 |
|
||||
| status | enum | `OPEN|LOCKED|CLOSED` |
|
||||
| last_seq | int | 当前最大 seq |
|
||||
| last_message_id | uuid|null | 最后一条消息 |
|
||||
| created_at, updated_at | datetime | 时间戳 |
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| session_id | uuid | 主键 |
|
||||
| session_name | string|null | 会话名 |
|
||||
| status | enum | `OPEN|LOCKED|CLOSED` |
|
||||
| last_seq | int | 当前最大 seq |
|
||||
| last_message_id | uuid|null | 最后一条消息 |
|
||||
| created_at, updated_at | datetime | 时间戳 |
|
||||
- 错误:401 未授权
|
||||
|
||||
### 追加消息
|
||||
- `POST /sessions/{session_id}/messages`
|
||||
- 请求体字段
|
||||
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| role | 是 | enum | `USER|AGENT|TOOL|SYSTEM` |
|
||||
@@ -52,6 +56,7 @@
|
||||
### 按序增量查询
|
||||
- `GET /sessions/{session_id}/messages?after_seq=0&limit=50`
|
||||
- 查询参数
|
||||
|
||||
| 参数 | 默认 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| after_seq | 0 | int | 仅返回 seq 大于该值 |
|
||||
@@ -62,6 +67,7 @@
|
||||
### 会话列表
|
||||
- `GET /sessions?page=1&per_page=15&status=OPEN&q=keyword`
|
||||
- 查询参数
|
||||
|
||||
| 参数 | 默认 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| page | 1 | int | 分页页码 |
|
||||
@@ -69,6 +75,7 @@
|
||||
| status | - | enum | 过滤 `OPEN|LOCKED|CLOSED` |
|
||||
| q | - | string | ILIKE 模糊匹配 session_name |
|
||||
- 响应 200:分页结构(`data/links/meta`),`data` 每项字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| session_id | uuid | 会话主键 |
|
||||
@@ -86,7 +93,8 @@
|
||||
|
||||
### 会话更新
|
||||
- `PATCH /sessions/{session_id}`
|
||||
- 请求体(至少一项,否则 422)
|
||||
请求体(至少一项,否则 422)
|
||||
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| session_name | 否 | string 1..255 | 自动 trim |
|
||||
@@ -118,6 +126,7 @@
|
||||
- `GET /sessions/{session_id}/sse?after_seq=123`
|
||||
- 头部:`Accept: text/event-stream`,可带 `Last-Event-ID`(优先于 query)用于断线续传。
|
||||
- 查询参数
|
||||
|
||||
| 参数 | 默认 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| after_seq | 0 | int | backlog 起始 seq(若有 Last-Event-ID 则覆盖) |
|
||||
@@ -135,6 +144,38 @@
|
||||
- 心跳:周期输出 `: ping` 保活(生产环境)。
|
||||
- 错误:401 未授权;404 session 不存在。
|
||||
|
||||
## Agent Run MVP-0(RunDispatcher + AgentRunJob)
|
||||
### 流程概述
|
||||
1. 用户追加 `role=USER && type=user.prompt` 后,Controller 自动调用 `RunDispatcher->dispatchForPrompt`。
|
||||
2. 并发保护:同会话只允许一个 RUNNING;同一个 `trigger_message_id` 幂等复用已有 `run_id`。
|
||||
3. 立即写入 `run.status`(SYSTEM/run.status,payload `{run_id,status:'RUNNING',trigger_message_id}`,dedupe_key=`run:trigger:{message_id}`)。
|
||||
4. 推送 `AgentRunJob(session_id, run_id)` 到队列(测试环境 QUEUE=sync 会同步执行)。
|
||||
5. RunLoop(使用 DummyAgentProvider):
|
||||
- Cancel 检查:存在 `run.cancel.request`(payload.run_id) 则写入 `run.status=CANCELED`,不产出 agent.message。
|
||||
- ContextBuilder:提取最近 20 条 USER/AGENT 消息(type in user.prompt/agent.message),seq 升序提供给 Provider。
|
||||
- Provider 返回一次性文本回复。
|
||||
- OutputSink 依次写入:`agent.message`(payload 含 run_id, provider)、`run.status=DONE`(dedupe_key=`run:{run_id}:status:DONE`)。
|
||||
6. 异常:AgentRunJob 捕获异常后写入 `error` + `run.status=FAILED`(dedupe)。
|
||||
|
||||
### Run 相关消息类型(落库即真相源)
|
||||
| type | role | payload 关键字段 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| run.status | SYSTEM | run_id, status(RUNNING/DONE/CANCELED/FAILED), trigger_message_id?, error? | Run 生命周期事件,CLOSED 状态下允许写入 |
|
||||
| agent.message | AGENT | run_id, provider | Provider 的一次性回复 |
|
||||
| run.cancel.request | USER/SYSTEM | run_id | CancelChecker 依据该事件判断是否中止 |
|
||||
| error | SYSTEM | run_id, message | 任务异常时落库 |
|
||||
|
||||
### 触发 Run(调试入口)
|
||||
- `POST /sessions/{session_id}/runs`
|
||||
- 请求体字段
|
||||
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| trigger_message_id | 是 | uuid | 通常为 `user.prompt` 消息 ID |
|
||||
- 行为:同 `trigger_message_id` 幂等;若已有 RUNNING 则复用其 run_id。
|
||||
- 响应 201:`{ run_id }`
|
||||
- 错误:401 未授权;404 session 不存在或 trigger_message 不属于该 session。
|
||||
|
||||
## cURL 示例
|
||||
```bash
|
||||
# 创建会话
|
||||
@@ -163,4 +204,9 @@ curl -s http://localhost:8000/api/sessions/$SESSION_ID/messages/{message_id} \
|
||||
curl -N http://localhost:8000/api/sessions/$SESSION_ID/sse?after_seq=10 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: text/event-stream"
|
||||
|
||||
# 手动触发 Run(调试用,实际 user.prompt 会自动触发)
|
||||
curl -s -X POST http://localhost:8000/api/sessions/$SESSION_ID/runs \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"trigger_message_id":"'$MESSAGE_ID'"}'
|
||||
```
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: ChatSession & Message API
|
||||
version: 1.0.0
|
||||
version: 1.1.0
|
||||
description: |
|
||||
ChatSession & Message MVP-1,支持会话创建、消息追加、增量查询。自然语言:中文。
|
||||
ChatSession & Message API(含 Archive/GetMessage/SSE 与 Run 调度)。自然语言:中文。
|
||||
servers:
|
||||
- url: http://localhost:8000/api
|
||||
description: 本地开发(FrankenPHP / Docker)
|
||||
tags:
|
||||
- name: ChatSession
|
||||
description: 会话管理与消息
|
||||
description: 会话管理
|
||||
- name: Message
|
||||
description: 消息追加与查询
|
||||
- name: Run
|
||||
description: Agent Run 调度
|
||||
paths:
|
||||
/sessions:
|
||||
post:
|
||||
@@ -77,9 +81,92 @@ paths:
|
||||
$ref: '#/components/schemas/PaginationMeta'
|
||||
"401":
|
||||
description: 未授权
|
||||
/sessions/{session_id}/messages:
|
||||
/sessions/{session_id}:
|
||||
get:
|
||||
tags: [ChatSession]
|
||||
summary: 获取会话详情
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: 会话详情
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChatSession'
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: 未找到
|
||||
patch:
|
||||
tags: [ChatSession]
|
||||
summary: 更新会话(重命名/状态)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateSessionRequest'
|
||||
responses:
|
||||
"200":
|
||||
description: 更新成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChatSession'
|
||||
"403":
|
||||
description: 状态门禁禁止
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
"422":
|
||||
description: 校验失败
|
||||
"401":
|
||||
description: 未授权
|
||||
/sessions/{session_id}/archive:
|
||||
post:
|
||||
tags: [ChatSession]
|
||||
summary: 归档会话(设为 CLOSED,幂等)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: 归档成功(或已归档)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChatSession'
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: 未找到
|
||||
/sessions/{session_id}/messages:
|
||||
post:
|
||||
tags: [Message]
|
||||
summary: 追加消息(含幂等与状态门禁)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -111,126 +198,8 @@ paths:
|
||||
$ref: '#/components/schemas/Error'
|
||||
"401":
|
||||
description: 未授权
|
||||
/sessions/{session_id}/messages/{message_id}:
|
||||
get:
|
||||
tags: [ChatSession]
|
||||
summary: 获取单条消息(校验 session_id)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: path
|
||||
name: message_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: 消息详情
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MessageResource'
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: 未找到或不属于该会话
|
||||
/sessions/{session_id}/archive:
|
||||
post:
|
||||
tags: [ChatSession]
|
||||
summary: 归档会话(设为 CLOSED,幂等)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: 归档成功(或已归档)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChatSession'
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: 未找到
|
||||
/sessions/{session_id}/sse:
|
||||
get:
|
||||
tags: [ChatSession]
|
||||
summary: SSE 增量推送(backlog + Redis 实时)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: after_seq
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
description: backlog 起始 seq(若有 Last-Event-ID 以其为准)
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
default: 200
|
||||
maximum: 500
|
||||
responses:
|
||||
"200":
|
||||
description: text/event-stream SSE 流
|
||||
content:
|
||||
text/event-stream:
|
||||
schema:
|
||||
type: string
|
||||
example: |
|
||||
id: 1
|
||||
event: message
|
||||
data: {"message_id":"...","seq":1}
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: 未找到
|
||||
/sessions/{session_id}:
|
||||
get:
|
||||
tags: [ChatSession]
|
||||
summary: 获取会话详情
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: 会话详情
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChatSession'
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: 未找到
|
||||
get:
|
||||
tags: [ChatSession]
|
||||
tags: [Message]
|
||||
summary: 按 seq 增量查询消息
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -268,10 +237,80 @@ paths:
|
||||
$ref: '#/components/schemas/MessageResource'
|
||||
"401":
|
||||
description: 未授权
|
||||
/sessions/{session_id}:
|
||||
patch:
|
||||
tags: [ChatSession]
|
||||
summary: 更新会话(重命名/状态)
|
||||
/sessions/{session_id}/messages/{message_id}:
|
||||
get:
|
||||
tags: [Message]
|
||||
summary: 获取单条消息(校验 session_id)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: path
|
||||
name: message_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: 消息详情
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MessageResource'
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: 未找到或不属于该会话
|
||||
/sessions/{session_id}/sse:
|
||||
get:
|
||||
tags: [Message]
|
||||
summary: SSE 增量推送(backlog + Redis 实时)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: after_seq
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
description: backlog 起始 seq(若有 Last-Event-ID 以其为准)
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
default: 200
|
||||
maximum: 500
|
||||
responses:
|
||||
"200":
|
||||
description: text/event-stream SSE 流
|
||||
content:
|
||||
text/event-stream:
|
||||
schema:
|
||||
type: string
|
||||
example: |
|
||||
id: 1
|
||||
event: message
|
||||
data: {"message_id":"...","seq":1}
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: 未找到
|
||||
/sessions/{session_id}/runs:
|
||||
post:
|
||||
tags: [Run]
|
||||
summary: 触发一次 Agent Run(按 trigger_message_id 幂等)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
@@ -286,24 +325,22 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateSessionRequest'
|
||||
$ref: '#/components/schemas/DispatchRunRequest'
|
||||
responses:
|
||||
"200":
|
||||
description: 更新成功
|
||||
"201":
|
||||
description: 已触发(或复用进行中的 RUNNING)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChatSession'
|
||||
"403":
|
||||
description: 状态门禁禁止
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
"422":
|
||||
description: 校验失败
|
||||
type: object
|
||||
properties:
|
||||
run_id:
|
||||
type: string
|
||||
format: uuid
|
||||
"401":
|
||||
description: 未授权
|
||||
"404":
|
||||
description: session 或 trigger_message 不存在
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
@@ -363,6 +400,13 @@ components:
|
||||
status:
|
||||
type: string
|
||||
enum: [OPEN, LOCKED, CLOSED]
|
||||
DispatchRunRequest:
|
||||
type: object
|
||||
required: [trigger_message_id]
|
||||
properties:
|
||||
trigger_message_id:
|
||||
type: string
|
||||
format: uuid
|
||||
AppendMessageRequest:
|
||||
type: object
|
||||
required: [role, type]
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_DATABASE" value="testing"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\ChatSessionController;
|
||||
use App\Http\Controllers\RunController;
|
||||
use App\Http\Controllers\UserController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -10,6 +11,8 @@ Route::get('/health', fn () => ['status' => 'ok']);
|
||||
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
|
||||
Route::get('test',[RunController::class,'test']);
|
||||
|
||||
Route::middleware('auth.jwt')->group(function () {
|
||||
Route::get('/me', function (Request $request) {
|
||||
return $request->user();
|
||||
@@ -30,4 +33,5 @@ Route::middleware('auth.jwt')->group(function () {
|
||||
Route::patch('/sessions/{session_id}', [ChatSessionController::class, 'update']);
|
||||
Route::post('/sessions/{session_id}/archive', [ChatSessionController::class, 'archive']);
|
||||
Route::get('/sessions/{session_id}/sse', [\App\Http\Controllers\ChatSessionSseController::class, 'stream']);
|
||||
Route::post('/sessions/{session_id}/runs', [RunController::class, 'store']);
|
||||
});
|
||||
|
||||
126
tests/Feature/AgentRunTest.php
Normal file
126
tests/Feature/AgentRunTest.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\AgentRunJob;
|
||||
use App\Models\Message;
|
||||
use App\Services\ChatService;
|
||||
use App\Services\RunDispatcher;
|
||||
use App\Services\RunLoop;
|
||||
use App\Services\OutputSink;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AgentRunTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_dispatch_and_run_creates_agent_reply_and_statuses(): void
|
||||
{
|
||||
Queue::fake();
|
||||
$service = app(ChatService::class);
|
||||
$dispatcher = app(RunDispatcher::class);
|
||||
|
||||
$session = $service->createSession('Run Session');
|
||||
$prompt = $service->appendMessage([
|
||||
'session_id' => $session->session_id,
|
||||
'role' => Message::ROLE_USER,
|
||||
'type' => 'user.prompt',
|
||||
'content' => 'hello agent',
|
||||
]);
|
||||
|
||||
$runId = $dispatcher->dispatchForPrompt($session->session_id, $prompt->message_id);
|
||||
|
||||
Queue::assertPushed(AgentRunJob::class, function ($job) use ($session, $runId) {
|
||||
return $job->sessionId === $session->session_id && $job->runId === $runId;
|
||||
});
|
||||
|
||||
// simulate worker execution
|
||||
(new AgentRunJob($session->session_id, $runId))->handle(
|
||||
app(RunLoop::class),
|
||||
app(OutputSink::class)
|
||||
);
|
||||
|
||||
$messages = Message::query()
|
||||
->where('session_id', $session->session_id)
|
||||
->orderBy('seq')
|
||||
->get();
|
||||
|
||||
$this->assertTrue($messages->contains(fn ($m) => $m->type === 'run.status' && ($m->payload['status'] ?? null) === 'RUNNING'));
|
||||
$this->assertTrue($messages->contains(fn ($m) => $m->type === 'agent.message' && ($m->payload['run_id'] ?? null) === $runId));
|
||||
$this->assertTrue($messages->contains(fn ($m) => $m->type === 'run.status' && ($m->payload['status'] ?? null) === 'DONE'));
|
||||
}
|
||||
|
||||
public function test_second_prompt_dispatches_new_run_after_first_completes(): void
|
||||
{
|
||||
Queue::fake();
|
||||
$service = app(ChatService::class);
|
||||
$dispatcher = app(RunDispatcher::class);
|
||||
|
||||
$session = $service->createSession('Sequential Runs');
|
||||
$firstPrompt = $service->appendMessage([
|
||||
'session_id' => $session->session_id,
|
||||
'role' => Message::ROLE_USER,
|
||||
'type' => 'user.prompt',
|
||||
'content' => 'first run',
|
||||
]);
|
||||
|
||||
$firstRunId = $dispatcher->dispatchForPrompt($session->session_id, $firstPrompt->message_id);
|
||||
|
||||
(new AgentRunJob($session->session_id, $firstRunId))->handle(
|
||||
app(RunLoop::class),
|
||||
app(OutputSink::class)
|
||||
);
|
||||
|
||||
$secondPrompt = $service->appendMessage([
|
||||
'session_id' => $session->session_id,
|
||||
'role' => Message::ROLE_USER,
|
||||
'type' => 'user.prompt',
|
||||
'content' => 'second run',
|
||||
]);
|
||||
|
||||
$secondRunId = $dispatcher->dispatchForPrompt($session->session_id, $secondPrompt->message_id);
|
||||
|
||||
$this->assertNotSame($firstRunId, $secondRunId);
|
||||
|
||||
Queue::assertPushed(AgentRunJob::class, 2);
|
||||
Queue::assertPushed(AgentRunJob::class, function ($job) use ($secondRunId, $session) {
|
||||
return $job->runId === $secondRunId && $job->sessionId === $session->session_id;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_cancel_prevents_agent_reply_and_marks_canceled(): void
|
||||
{
|
||||
Queue::fake();
|
||||
$service = app(ChatService::class);
|
||||
$dispatcher = app(RunDispatcher::class);
|
||||
$loop = app(RunLoop::class);
|
||||
|
||||
$session = $service->createSession('Cancel Session');
|
||||
$prompt = $service->appendMessage([
|
||||
'session_id' => $session->session_id,
|
||||
'role' => Message::ROLE_USER,
|
||||
'type' => 'user.prompt',
|
||||
'content' => 'please cancel',
|
||||
]);
|
||||
|
||||
$runId = $dispatcher->dispatchForPrompt($session->session_id, $prompt->message_id);
|
||||
|
||||
$service->appendMessage([
|
||||
'session_id' => $session->session_id,
|
||||
'role' => Message::ROLE_USER,
|
||||
'type' => 'run.cancel.request',
|
||||
'payload' => ['run_id' => $runId],
|
||||
]);
|
||||
|
||||
$loop->run($session->session_id, $runId);
|
||||
|
||||
$messages = Message::query()
|
||||
->where('session_id', $session->session_id)
|
||||
->get();
|
||||
|
||||
$this->assertFalse($messages->contains(fn ($m) => $m->type === 'agent.message' && ($m->payload['run_id'] ?? null) === $runId));
|
||||
$this->assertTrue($messages->contains(fn ($m) => $m->type === 'run.status' && ($m->payload['status'] ?? null) === 'CANCELED'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user