visuddhinanda 1 rok pred
rodič
commit
0226ced4f2

+ 158 - 0
api-v8/app/Http/Controllers/AiModelController.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\StoreAiModelRequest;
+use App\Http\Requests\UpdateAiModelRequest;
+use App\Models\AiModel;
+use Illuminate\Http\Request;
+use App\Http\Api\AuthApi;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Log;
+use App\Http\Api\StudioApi;
+use App\Http\Resources\AiModelResource;
+
+
+class AiModelController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $user = AuthApi::current($request);
+        if (!$user) {
+            Log::error('notification auth failed {request}', ['request' => $request]);
+            return $this->error(__('auth.failed'), 401, 401);
+        }
+        switch ($request->get('view')) {
+            case 'all':
+                $table = AiModel::whereNotNull('owner_id');
+                break;
+            case 'studio':
+                $studioId = StudioApi::getIdByName($request->get('name'));
+
+                $table = AiModel::where('owner_id', $studioId);
+
+                break;
+        }
+        if ($request->has('keyword')) {
+            $table = $table->where('name', 'like', '%' . $request->get('keyword') . '%');
+        }
+        $count = $table->count();
+
+        $table = $table->orderBy(
+            $request->get('order', 'created_at'),
+            $request->get('dir', 'asc')
+        );
+
+        $table = $table->skip($request->get("offset", 0))
+            ->take($request->get('limit', 1000));
+
+        Log::debug('sql', ['sql' => $table->toSql()]);
+
+        $result = $table->get();
+
+        return $this->ok(
+            [
+                "rows" => AiModelResource::collection(resource: $result),
+                "count" => $count,
+            ]
+        );
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \App\Http\Requests\StoreAiModelRequest  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(StoreAiModelRequest $request)
+    {
+        //
+        $user = AuthApi::current($request);
+        if (!$user) {
+            return $this->error(__('auth.failed'), 401, 401);
+        }
+        $studioId = StudioApi::getIdByName($request->get('studio_name'));
+        Log::debug('store', ['studioId' => $studioId, 'user' => $user]);
+        if (!self::canEdit($user['user_uid'], $studioId)) {
+            return $this->error(__('auth.failed'), 403, 403);
+        }
+        $new = new AiModel();
+        $new->name = $request->get('name');
+        $new->uid = Str::uuid();
+        $new->owner_id = $studioId;
+        $new->editor_id = $user['user_uid'];
+        $new->save();
+        return $this->ok(new AiModelResource($new));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\AiModel  $aiModel
+     * @return \Illuminate\Http\Response
+     */
+    public function show(AiModel $aiModel)
+    {
+        //
+        return $this->ok(new AiModelResource($aiModel));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \App\Http\Requests\UpdateAiModelRequest  $request
+     * @param  \App\Models\AiModel  $aiModel
+     * @return \Illuminate\Http\Response
+     */
+    public function update(UpdateAiModelRequest $request, AiModel $aiModel)
+    {
+        //
+        $user = AuthApi::current($request);
+        if (!$user) {
+            return $this->error(__('auth.failed'), 401, 401);
+        }
+        if (!self::canEdit($user['user_uid'], $aiModel->owner_id)) {
+            return $this->error(__('auth.failed'), 403, 403);
+        }
+        $aiModel->name = $request->get('name');
+        $aiModel->description = $request->get('description');
+        $aiModel->url = $request->get('url');
+        $aiModel->model = $request->get('model');
+        $aiModel->key = $request->get('key');
+        $aiModel->privacy = $request->get('privacy');
+        $aiModel->editor_id = $user['user_uid'];
+        $aiModel->save();
+        return $this->ok(new AiModelResource($aiModel));
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\AiModel  $aiModel
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request, AiModel $aiModel)
+    {
+        //
+        $user = AuthApi::current($request);
+        if (!$user) {
+            return $this->error(__('auth.failed'), 401, 401);
+        }
+        if (!self::canEdit($user['user_uid'], $aiModel->owner_id)) {
+            return $this->error(__('auth.failed'), 403, 403);
+        }
+        $del = $aiModel->delete();
+        return $this->ok($del);
+    }
+
+    public static function canEdit($user_uid, $owner_uid)
+    {
+        return $user_uid === $owner_uid;
+    }
+}

+ 30 - 0
api-v8/app/Http/Requests/StoreAiModelRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class StoreAiModelRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            //
+        ];
+    }
+}

+ 30 - 0
api-v8/app/Http/Requests/UpdateAiModelRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class UpdateAiModelRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return false;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            //
+        ];
+    }
+}

+ 19 - 0
api-v8/app/Http/Resources/AiModelResource.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class AiModelResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
+     */
+    public function toArray($request)
+    {
+        return parent::toArray($request);
+    }
+}

+ 15 - 0
api-v8/app/Models/AiModel.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class AiModel extends Model
+{
+    use HasFactory;
+    protected $primaryKey = 'uid';
+    protected $casts = [
+        'uid' => 'string'
+    ];
+}

+ 40 - 0
api-v8/database/migrations/2025_01_27_152548_create_ai_models_table.php

@@ -0,0 +1,40 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateAiModelsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('ai_models', function (Blueprint $table) {
+            $table->id();
+            $table->uuid('uid')->unique();
+            $table->string('name', 64)->index();
+            $table->text('description')->nullable();
+            $table->string('url', 1024)->nullable()->index();
+            $table->string('model', 1024)->nullable()->index();
+            $table->string('key', 1024)->nullable();
+            $table->string('privacy', 32)->index()->default('private')->comment('隐私性:private|public');
+            $table->uuid('owner_id')->index()->comment('任务拥有者:用户或者team-space');
+            $table->uuid('editor_id')->index();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('ai_models');
+    }
+}

+ 69 - 0
dashboard-v4/dashboard/src/components/ai/AiModelCreate.tsx

@@ -0,0 +1,69 @@
+import { useIntl } from "react-intl";
+import {
+  ProForm,
+  ProFormInstance,
+  ProFormText,
+} from "@ant-design/pro-components";
+import { message } from "antd";
+import { post } from "../../request";
+import { useRef } from "react";
+import { IAiModelRequest, IAiModelResponse } from "../api/ai";
+
+interface IWidget {
+  studioName?: string;
+  onCreate?: Function;
+}
+const AiModelCreate = ({ studioName, onCreate }: IWidget) => {
+  const intl = useIntl();
+  const formRef = useRef<ProFormInstance>();
+
+  return (
+    <ProForm<IAiModelRequest>
+      formRef={formRef}
+      onFinish={async (values: IAiModelRequest) => {
+        if (typeof studioName === "undefined") {
+          return;
+        }
+        const url = `/v2/ai-model`;
+        console.info("api request", url, values);
+        const res = await post<IAiModelRequest, IAiModelResponse>(url, values);
+        console.info("api response", res);
+        if (res.ok) {
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+          if (typeof onCreate !== "undefined") {
+            onCreate();
+            formRef.current?.resetFields();
+          }
+        } else {
+          message.error(res.message);
+        }
+      }}
+    >
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="name"
+          required
+          label={intl.formatMessage({ id: "channel.name" })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "channel.create.message.noname",
+              }),
+            },
+          ]}
+        />
+        <ProFormText
+          width="md"
+          name="studio_name"
+          initialValue={studioName}
+          hidden
+          required
+        />
+      </ProForm.Group>
+    </ProForm>
+  );
+};
+
+export default AiModelCreate;

+ 107 - 0
dashboard-v4/dashboard/src/components/ai/AiModelEdit.tsx

@@ -0,0 +1,107 @@
+import { useIntl } from "react-intl";
+import {
+  ProForm,
+  ProFormInstance,
+  ProFormText,
+  ProFormTextArea,
+} from "@ant-design/pro-components";
+import { message } from "antd";
+import { get, put } from "../../request";
+import { useRef } from "react";
+import { IAiModelRequest, IAiModelResponse } from "../api/ai";
+import PublicitySelect from "../studio/PublicitySelect";
+
+interface IWidget {
+  studioName?: string;
+  modelId?: string;
+  onChange?: Function;
+}
+const AiModelEdit = ({ studioName, modelId, onChange }: IWidget) => {
+  const intl = useIntl();
+  const formRef = useRef<ProFormInstance>();
+
+  return (
+    <ProForm<IAiModelRequest>
+      formRef={formRef}
+      onFinish={async (values: IAiModelRequest) => {
+        if (typeof studioName === "undefined") {
+          return;
+        }
+        const url = `/v2/ai-model/${modelId}`;
+        console.info("api request", url, values);
+        const res = await put<IAiModelRequest, IAiModelResponse>(url, values);
+        console.info("api response", res);
+        if (res.ok) {
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+          onChange && onChange();
+        } else {
+          message.error(res.message);
+        }
+      }}
+      request={async () => {
+        const url = `/v2/ai-model/${modelId}`;
+        console.info("api request", url);
+        const res = await get<IAiModelResponse>(url);
+        console.info("api response", res);
+        return res.data;
+      }}
+    >
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="name"
+          required
+          label={intl.formatMessage({ id: "forms.fields.name.label" })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "channel.create.message.noname",
+              }),
+            },
+          ]}
+        />
+        <ProFormText
+          width="md"
+          name="studio_name"
+          initialValue={studioName}
+          hidden
+          required
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="url"
+          label={intl.formatMessage({ id: "forms.fields.url.label" })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="model"
+          label={intl.formatMessage({ id: "forms.fields.model.label" })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="key"
+          label={intl.formatMessage({ id: "forms.fields.key.label" })}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <PublicitySelect name="privacy" disable={["public_no_list"]} />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormTextArea
+          width="md"
+          name="description"
+          label={intl.formatMessage({ id: "forms.fields.description.label" })}
+        />
+      </ProForm.Group>
+    </ProForm>
+  );
+};
+
+export default AiModelEdit;

+ 252 - 0
dashboard-v4/dashboard/src/components/ai/AiModelList.tsx

@@ -0,0 +1,252 @@
+import { Link } from "react-router-dom";
+import { useIntl } from "react-intl";
+import { Button, Popover, Typography, Dropdown, Modal, message } from "antd";
+import { ActionType, ProList } from "@ant-design/pro-components";
+import {
+  PlusOutlined,
+  DeleteOutlined,
+  ExclamationCircleOutlined,
+} from "@ant-design/icons";
+
+import { delete_, get } from "../../request";
+
+import { RoleValueEnum } from "../../components/studio/table";
+import { IDeleteResponse } from "../../components/api/Article";
+import { useRef, useState } from "react";
+
+import { getSorterUrl } from "../../utils";
+import { IAiModel, IAiModelListResponse } from "../api/ai";
+import AiModelCreate from "./AiModelCreate";
+
+const { Text } = Typography;
+
+interface IWidget {
+  studioName?: string;
+}
+const AiModelList = ({ studioName }: IWidget) => {
+  const intl = useIntl(); //i18n
+
+  const [openCreate, setOpenCreate] = useState(false);
+
+  const showDeleteConfirm = (id: string, title: string) => {
+    Modal.confirm({
+      icon: <ExclamationCircleOutlined />,
+      title:
+        intl.formatMessage({
+          id: "message.delete.confirm",
+        }) +
+        intl.formatMessage({
+          id: "message.irrevocable",
+        }),
+
+      content: title,
+      okText: intl.formatMessage({
+        id: "buttons.delete",
+      }),
+      okType: "danger",
+      cancelText: intl.formatMessage({
+        id: "buttons.no",
+      }),
+      onOk() {
+        console.log("delete", id);
+        return delete_<IDeleteResponse>(`/v2/group/${id}`)
+          .then((json) => {
+            if (json.ok) {
+              message.success("删除成功");
+              ref.current?.reload();
+            } else {
+              message.error(json.message);
+            }
+          })
+          .catch((e) => console.log("Oops errors!", e));
+      },
+    });
+  };
+
+  const ref = useRef<ActionType>();
+
+  return (
+    <>
+      <ProList<IAiModel>
+        actionRef={ref}
+        onRow={(record) => ({
+          onClick: () => {},
+        })}
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 50,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.name.label",
+            }),
+            dataIndex: "name",
+            key: "name",
+            tooltip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              return (
+                <div key={index}>
+                  <div>
+                    <Link to={`/studio/${studioName}/group/${row.uid}/show`}>
+                      {row.name}
+                    </Link>
+                  </div>
+                  <Text type="secondary"></Text>
+                </div>
+              );
+            },
+          },
+
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.description.label",
+            }),
+            dataIndex: "description",
+            key: "description",
+            search: false,
+            tooltip: "过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.role.label",
+            }),
+            dataIndex: "role",
+            key: "role",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: RoleValueEnum(),
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created_at",
+            width: 100,
+            search: false,
+            dataIndex: "created_at",
+            valueType: "date",
+            sorter: true,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => [
+              <Dropdown.Button
+                key={index}
+                type="link"
+                menu={{
+                  items: [
+                    {
+                      key: "remove",
+                      label: intl.formatMessage({
+                        id: "buttons.delete",
+                      }),
+                      icon: <DeleteOutlined />,
+                      danger: true,
+                    },
+                  ],
+                  onClick: (e) => {
+                    switch (e.key) {
+                      case "share":
+                        break;
+                      case "remove":
+                        showDeleteConfirm(row.uid, row.name);
+                        break;
+                      default:
+                        break;
+                    }
+                  },
+                }}
+              >
+                <Link
+                  to={`/studio/${studioName}/group/${row.uid}/edit`}
+                  target="_blank"
+                >
+                  {intl.formatMessage({
+                    id: "buttons.edit",
+                  })}
+                </Link>
+              </Dropdown.Button>,
+            ],
+          },
+        ]}
+        metas={{
+          title: {
+            dataIndex: "name",
+            render(dom, entity, index, action, schema) {
+              return (
+                <Link to={`/studio/${studioName}/ai/models/${entity.uid}/edit`}>
+                  {entity.name}
+                </Link>
+              );
+            },
+          },
+        }}
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+          let url = `/v2/ai-model?view=studio&name=${studioName}`;
+          const offset = ((params.current ?? 1) - 1) * (params.pageSize ?? 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          url += params.keyword ? "&search=" + params.keyword : "";
+          url += getSorterUrl(sorter);
+
+          console.info("api request", url);
+          const res = await get<IAiModelListResponse>(url);
+          console.info("api response", res);
+          return {
+            total: res.data.total,
+            succcess: res.ok,
+            data: res.data.rows,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={() => [
+          <Popover
+            content={
+              <AiModelCreate
+                studioName={studioName}
+                onCreate={() => {
+                  setOpenCreate(false);
+                  ref.current?.reload();
+                }}
+              />
+            }
+            placement="bottomRight"
+            trigger="click"
+            open={openCreate}
+            onOpenChange={(open: boolean) => {
+              setOpenCreate(open);
+            }}
+          >
+            <Button key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({ id: "buttons.create" })}
+            </Button>
+          </Popover>,
+        ]}
+      />
+    </>
+  );
+};
+
+export default AiModelList;

+ 22 - 0
dashboard-v4/dashboard/src/pages/studio/ai/index.tsx

@@ -0,0 +1,22 @@
+import { Outlet } from "react-router-dom";
+import { Layout } from "antd";
+
+import LeftSider from "../../../components/studio/LeftSider";
+import { styleStudioContent } from "../style";
+
+const { Content } = Layout;
+
+const Widget = () => {
+  return (
+    <Layout>
+      <Layout>
+        <LeftSider selectedKeys={"ai"} openKeys={["ai"]} />
+        <Content style={styleStudioContent}>
+          <Outlet />
+        </Content>
+      </Layout>
+    </Layout>
+  );
+};
+
+export default Widget;

+ 22 - 0
dashboard-v4/dashboard/src/pages/studio/ai/model_edit.tsx

@@ -0,0 +1,22 @@
+import { useParams } from "react-router-dom";
+import { Card } from "antd";
+
+import GoBack from "../../../components/studio/GoBack";
+import AiModelEdit from "../../../components/ai/AiModelEdit";
+
+const Widget = () => {
+  const { studioname } = useParams();
+  const { modelId } = useParams(); //url 参数
+
+  return (
+    <Card
+      title={
+        <GoBack to={`/studio/${studioname}/ai/models/list`} title={"返回"} />
+      }
+    >
+      <AiModelEdit studioName={studioname} modelId={modelId} />
+    </Card>
+  );
+};
+
+export default Widget;

+ 8 - 0
dashboard-v4/dashboard/src/pages/studio/ai/models.tsx

@@ -0,0 +1,8 @@
+import { useParams } from "react-router-dom";
+import AiModelList from "../../../components/ai/AiModelList";
+
+const Widget = () => {
+  const { studioname } = useParams(); //url 参数
+  return <AiModelList studioName={studioname} />;
+};
+export default Widget;