This commit is contained in:
Twilight 2024-07-24 16:13:16 +08:00
parent ce8aff3626
commit 97753a264a
24 changed files with 533 additions and 165 deletions

View File

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssistantRequest;
use App\Models\Assistant;
use Illuminate\Http\Request;
class AssistantController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
return $this->success(
Assistant::whereUserId($request->user('api')->id)->get()
);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'name' => 'string|required',
'description' => 'string|required',
'prompt' => 'string|required|max:2000',
]);
$assistantModel = new Assistant();
$assistant = $assistantModel->create([
'name' => $request->input('name'),
'description' => $request->input('description'),
'prompt' => $request->input('prompt'),
'user_id' => $request->user('api')->id,
]);
// $tools = $request->input('tools');
// 检测这些工具是否存在且是否属于用户
// $tools = $assistantModel->whereIn('id', $tools)->whereUserId($request->user('api')->id)->get();
//
// $assistant->tools()->sync($tools->pluck('id')->toArray());
//
//
// $assistant->tools()->sync($request->input('tools'));
return $this->created($assistant);
}
/**
* Display the specified resource.
*/
public function show(AssistantRequest $request, Assistant $assistant)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(AssistantRequest $request, Assistant $assistant)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(AssistantRequest $request, Assistant $assistant)
{
//
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssistantRequest;
use App\Models\Assistant;
use App\Models\Tool;
use Illuminate\Http\Request;
class AssistantToolController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request, Assistant $assistant)
{
return $this->success(
$assistant->tools()->get()
);
}
/**
* Store a newly created resource in storage.
*/
public function store(AssistantRequest $request, Assistant $assistant)
{
$request->validate([
'tool_id' => 'required|exists:tools,id',
]);
$tool = Tool::find($request->input('tool_id'));
// 检测 tool是否属于user
if ($tool->user_id !== $request->user('api')->id) {
return $this->forbidden();
}
// 检测是否存在
if ($assistant->tools()->where('tool_id', $tool->id)->exists()) {
return $this->conflict('The tool already exists');
}
$assistant->tools()->attach($tool->id);
return $this->created($assistant->tools);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(AssistantRequest $request, Assistant $assistant, Tool $tool)
{
// 从 assistant_tools 表中删除
$assistant->tools()->detach($tool->id);
return $this->deleted();
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Web; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View File

@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\ToolRequest;
use App\Models\Tool;
use App\Repositories\Tool\Tool as ToolRepo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class ToolController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
return $this->success(
Tool::whereUserId($request->user('api')->id)->get()
);
}
/**
* Store a newly created resource in storage.
*
* @throws \Exception
*/
public function store(Request $request)
{
$request->validate([
'name' => 'string|required',
'description' => 'string|nullable',
'url' => 'string|required',
'api_key' => 'string|nullable',
]);
$url = $request->input('url');
// 检测是否存在
if (Tool::whereDiscoveryUrl($url)->exists()) {
return $this->conflict('The tool already exists');
}
$json = Http::get($url);
$toolRepo = new ToolRepo($json->json());
$tool = new Tool();
$tool->create([
'name' => $toolRepo->name,
'description' => $toolRepo->description,
'discovery_url' => $url,
'api_key' => $request->input('api_key'),
'data' => $toolRepo,
'user_id' => $request->user('api')->id,
]);
return $this->created($tool);
}
/**
* Display the specified resource.
*/
public function show(ToolRequest $request, Tool $tool)
{
return $this->success($tool);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(ToolRequest $request, Tool $tool)
{
$tool->delete();
return $this->deleted();
}
}

View File

@ -2,7 +2,123 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
abstract class Controller abstract class Controller
{ {
// public function success($data = [], $status = 200): JsonResponse
{
return $this->apiResponse($data, $status);
}
public function apiResponse($data = [], $status = 200): JsonResponse
{
// if data is string, then it is error message
if (is_string($data)) {
$data = [
'message' => $data,
];
}
return response()->json($data, $status)->setEncodingOptions(JSON_UNESCAPED_UNICODE);
}
public function created($message = 'Created'): JsonResponse
{
return $this->success($message, 201);
}
public function noContent($message = 'No content'): JsonResponse
{
return $this->success($message, 204);
}
public function badRequest($message = 'Bad request'): JsonResponse
{
return $this->error($message);
}
public function error($message = '', $code = 400): JsonResponse
{
return $this->apiResponse($message, $code);
}
public function serviceUnavailable($message = 'Service unavailable'): JsonResponse
{
return $this->error($message, 503);
}
public function forbidden($message = 'Forbidden'): JsonResponse
{
return $this->error($message, 403);
}
public function notFound($message = 'Not found'): JsonResponse
{
return $this->error($message, 404);
}
public function methodNotAllowed($message = 'Method not allowed'): JsonResponse
{
return $this->error($message, 405);
}
public function tooManyRequests($message = 'Too many requests'): JsonResponse
{
return $this->error($message, 429);
}
public function serverError($message = 'Server error'): JsonResponse
{
return $this->error($message, 500);
}
public function failed($message = 'Failed', $code = 400): JsonResponse
{
return $this->apiResponse($message, $code);
}
public function unauthorized($message = 'Unauthorized'): JsonResponse
{
return $this->error($message, 401);
}
public function accepted($message = 'Accepted'): JsonResponse
{
return $this->success($message, 202);
}
public function updated(mixed $message = 'Updated'): JsonResponse
{
if ($message instanceof Model) {
$message = $message->getChanges();
}
return $this->success($message);
}
public function deleted($message = 'Deleted'): JsonResponse
{
return $this->success($message, 204);
}
public function notAllowed($message = 'Not allowed'): JsonResponse
{
return $this->error($message, 405);
}
public function conflict($message = 'Conflict'): JsonResponse
{
return $this->error($message, 409);
}
public function notAcceptable($message = 'Not acceptable'): JsonResponse
{
return $this->error($message, 406);
}
public function preconditionFailed($message = 'Precondition failed'): JsonResponse
{
return $this->error($message, 412);
}
} }

View File

@ -1,46 +0,0 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class AssistantController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request) {}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@ -30,6 +30,7 @@ class AuthController extends Controller
*/ */
public function __construct() public function __construct()
{ {
$this->callback_url = route('oauth.callback');
$this->openIDLogic = app(OpenIDLogic::class); $this->openIDLogic = app(OpenIDLogic::class);
} }

View File

@ -1,75 +0,0 @@
<?php
namespace App\Http\Controllers\Web;
use Apiboard\OpenAPI\OpenAPI;
use App\Http\Controllers\Controller;
use App\Repositories\Tool\Tool;
use cebe\openapi\exceptions\IOException;
use cebe\openapi\exceptions\TypeErrorException;
use cebe\openapi\exceptions\UnresolvableReferenceException;
use cebe\openapi\json\InvalidJsonPointerSyntaxException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class ToolController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
* @throws \Exception
*/
public function store(Request $request)
{
$request->validate([
'name' => 'string|required',
'description' => 'string|nullable',
'url' => 'string|required',
'api_key' => 'string|nullable',
]);
$url = $request->input('url');
$url = "localhost:8081/fn.php";
$json = Http::get($url);
$tool = new Tool($json->json());
dd($tool);
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class JSONRequest
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AssistantRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
$assistant = $this->route('assistant');
if ($assistant && $assistant->user_id === $this->user('api')->id) {
return true;
}
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ToolRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
$tool = $this->route('tool');
if ($tool) {
return $tool->user_id === $this->user()->id;
}
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -6,7 +6,8 @@
class Qwen extends Base class Qwen extends Base
{ {
protected string $model = "qwen-max"; protected string $model = 'qwen-max';
/** /**
* Create a new class instance. * Create a new class instance.
*/ */
@ -15,30 +16,30 @@ public function __construct()
// //
} }
public function getResponse() public function getResponse(): void
{ {
$client = new Client([ $client = new Client([
'headers' => [ 'headers' => [
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'Accept' => 'text/event-stream', 'Accept' => 'text/event-stream',
'Cache-Control' => 'no-cache', 'Cache-Control' => 'no-cache',
'Authorization' => 'Bearer ' . config('services.dashscope.api_key'), 'Authorization' => 'Bearer '.config('services.dashscope.api_key'),
] ],
]); ]);
$url = config('services.dashscope.api_base') . '/compatible-mode/v1/chat/completions'; $url = config('services.dashscope.api_base').'/compatible-mode/v1/chat/completions';
$messages = [ $messages = [
[ [
'role' => 'user', 'role' => 'user',
'content' => "你好", 'content' => '你好',
], ],
]; ];
$jsonBody = json_encode([ $jsonBody = json_encode([
'model' => $this->model, 'model' => $this->model,
'messages' => $messages, 'messages' => $messages,
'stream' => true 'stream' => true,
]); ]);
$response = $client->request('POST', $url, [ $response = $client->request('POST', $url, [
@ -47,15 +48,14 @@ public function getResponse()
]); ]);
$body = $response->getBody(); $body = $response->getBody();
// while (!$body->eof()) { // while (!$body->eof()) {
// echo $body->read(1024); // echo $body->read(1024);
// } // }
while (!$body->eof()) { while (! $body->eof()) {
// $body->read(1); // $body->read(1);
echo $body->read(1); echo $body->read(1);
} }
} }
} }

View File

@ -12,6 +12,7 @@ class Assistant extends Model
protected $fillable = [ protected $fillable = [
'name', 'name',
'description', 'description',
'prompt',
'user_id', 'user_id',
]; ];

View File

@ -5,19 +5,22 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Http;
class Tool extends Model class Tool extends Model
{ {
protected $fillable = [ protected $fillable = [
'name', 'name',
'description', 'description',
'url', 'discovery_url',
'api_key', 'api_key',
'callback_url', 'data',
'user_id', 'user_id',
]; ];
protected $casts = [
'data' => 'array',
];
public function user(): BelongsTo public function user(): BelongsTo
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
@ -28,15 +31,4 @@ public function functions(): HasMany
return $this->hasMany(ToolFunction::class); return $this->hasMany(ToolFunction::class);
} }
public function fetchFunctions(): void
{
$url = $this->url;
$json = Http::get($url);
$json = $json->json();
$this->callback_url = $json['callback_url'];
}
} }

View File

@ -40,7 +40,8 @@ private function setJWTGuard(): void
$jwt = $request->bearerToken(); $jwt = $request->bearerToken();
if (empty($jwt)) { if (empty($jwt)) {
return response()->json(['error' => 'No token provided'], 401); return null;
// return response()->json(['error' => 'No token provided'], 401);
} }
$headers = new stdClass(); $headers = new stdClass();
@ -49,12 +50,18 @@ private function setJWTGuard(): void
$decoded = JWT::decode($jwt, $keys, $headers); $decoded = JWT::decode($jwt, $keys, $headers);
// $request->attributes->add(['token_type' => $headers->typ]); // $request->attributes->add(['token_type' => $headers->typ]);
} catch (Exception $e) { } catch (Exception $e) {
return response()->json(['error' => 'Invalid token, '.$e->getMessage()], 401); // dd($e);
return null;
// return response()->json(['error' => 'Invalid token, '.$e->getMessage()], 401);
} }
// must id_token // must id_token
if ($headers->typ !== 'id_token') { if ($headers->typ !== 'id_token') {
return response()->json(['error' => 'The token not id_token'], 401); return null;
// return response()->json(['error' => 'The token not id_token'], 401);
} }
// 检查是否有 字段 // 检查是否有 字段
@ -63,17 +70,22 @@ private function setJWTGuard(): void
]; ];
foreach ($required_fields as $field) { foreach ($required_fields as $field) {
if (! isset($decoded->$field)) { if (!isset($decoded->$field)) {
return response()->json(['error' => 'The token not contain the field '.$field], 401); return null;
// return response()->json(['error' => 'The token not contain the field '.$field], 401);
} }
} }
if (config('oauth.force_aud')) { if (config('oauth.force_aud')) {
if (! in_array($decoded->aud, config('oauth.trusted_aud'))) { if (!in_array($decoded->aud, config('oauth.trusted_aud'))) {
return response()->json(['error' => 'The application rejected the token, token aud is '.$decoded->aud.', app aud is '.config('oauth.client_id')], 401); throw new Exception('The application rejected the token, token aud is ' . $decoded->aud . ', app aud is ' . config('oauth.client_id'));
// return response()->json(['error' => 'The application rejected the token, token aud is '.$decoded->aud.', app aud is '.config('oauth.client_id')], 401);
} }
return response()->json(['error' => 'The token not match the application, '.' token aud is '.$decoded->aud.', app aud is '.config('oauth.client_id')], 401); // throw
throw new Exception('The token not match the application, ' . ' token aud is ' . $decoded->aud . ', app aud is ' . config('oauth.client_id'));
// return response()->json(['error' => 'The token not match the application, '.' token aud is '.$decoded->aud.', app aud is '.config('oauth.client_id')], 401);
} }
return User::where('external_id', $decoded->sub)->firstOrCreate([ return User::where('external_id', $decoded->sub)->firstOrCreate([

View File

@ -9,6 +9,7 @@ class Tool
public string $name; public string $name;
public string $homepage_url; public string $homepage_url;
public string $callback_url; public string $callback_url;
public string $description; public string $description;
@ -25,6 +26,7 @@ public function __construct(array $data)
$this->data = $data; $this->data = $data;
$this->parse(); $this->parse();
} }
/** /**
* @throws Exception * @throws Exception
*/ */

View File

@ -7,13 +7,18 @@
class ToolFunction class ToolFunction
{ {
public string $name; public string $name;
public string $description; public string $description;
public array $parameters; public array $parameters;
protected array $data; protected array $data;
public array $required; public array $required;
/** /**
* Create a new class instance. * Create a new class instance.
*
* @throws Exception * @throws Exception
*/ */
public function __construct(array $data) public function __construct(array $data)

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Middleware\JSONRequest;
use App\Http\Middleware\JWTMiddleware; use App\Http\Middleware\JWTMiddleware;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
@ -16,6 +17,10 @@
// $middleware->alias([ // $middleware->alias([
// 'jwt' => JWTMiddleware::class // 'jwt' => JWTMiddleware::class
// ]); // ]);
// add for api
$middleware->api([
JSONRequest::class
]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {
// //

View File

@ -42,7 +42,7 @@
], ],
'api' => [ 'api' => [
'driver' => 'jwt', 'driver' => 'jwt',
// 'provider' => 'users', // 'provider' => 'users',
], ],
], ],

View File

@ -38,6 +38,6 @@
'dashscope' => [ 'dashscope' => [
'api_key' => env('DASHSCOPE_API_KEY'), 'api_key' => env('DASHSCOPE_API_KEY'),
'api_base' => env('DASHSCOPE_API_BASE', 'https://dashscope.aliyuncs.com'), 'api_base' => env('DASHSCOPE_API_BASE', 'https://dashscope.aliyuncs.com'),
] ],
]; ];

View File

@ -18,7 +18,7 @@
| |
*/ */
'driver' => env('SESSION_DRIVER', 'database'), 'driver' => env('SESSION_DRIVER', 'redis'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -17,7 +17,7 @@ public function up(): void
$table->string('name'); $table->string('name');
$table->string('description')->nullable(); $table->string('description')->nullable();
$table->string('discovery_url'); $table->string('discovery_url')->index();
$table->string('api_key')->nullable(); $table->string('api_key')->nullable();

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('assistants', function (Blueprint $table) {
// prompt
$table->text('prompt')->after('description');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('assistants', function (Blueprint $table) {
$table->dropColumn('prompt');
});
}
};

View File

@ -1,8 +1,23 @@
<?php <?php
use App\Http\Controllers\Api\AssistantController;
use App\Http\Controllers\Api\AssistantToolController;
use App\Http\Controllers\Api\ChatController;
use App\Http\Controllers\Api\ToolController;
use App\Http\Middleware\JSONRequest;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) { Route::middleware(['auth:api,web'])->group(function () {
Route::get('/user', function (Request $request) {
return $request->user(); return $request->user();
})->middleware('auth:api,web'); });
Route::apiResource('tools', ToolController::class);
Route::apiResource('assistants', AssistantController::class);
Route::apiResource('chats', ChatController::class);
Route::get('assistants/{assistant}/tools', [AssistantToolController::class, 'index']);
Route::post('assistants/{assistant}/tools', [AssistantToolController::class, 'store']);
Route::delete('assistants/{assistant}/tools/{tool}', [AssistantToolController::class, 'destroy']);
});