ChenYu 7 місяців тому
батько
коміт
3a6b45f618

+ 58 - 0
doc/AI_ORDER_V2_API.md

@@ -307,3 +307,61 @@ GET /v2/ai/order/v2/queryResult?taskId=2b80cfe9-eb90-44c2-9201-55ecff33a85d
   "timestamp": 1773033600000
 }
 ```
+
+---
+
+## 5. 查询任务执行日志
+
+*   **URL**: `/v2/ai/order/v2/taskLog`
+*   **Method**: `GET`
+*   **描述**: 根据 taskId 查询 `ai-flow-py` 容器中的执行日志。等价于在宿主机执行 `docker logs ai-flow-py 2>&1 | grep "{taskId}"`。通过 Docker Engine API(unix socket)直接访问同宿主机上的容器日志。
+
+### 前置条件
+
+部署时需要将 Docker Socket 挂载到容器内:
+```bash
+-v /var/run/docker.sock:/var/run/docker.sock
+```
+
+### 请求参数
+
+| 字段名 | 类型 | 必填 | 说明 |
+| :--- | :--- | :--- | :--- |
+| `taskId` | string | 是 | 任务ID,例如 `b07cbbc8-1f8f-4060-98f8-051b66ccf1ab` |
+
+### 响应参数(`data` 字段)
+
+返回匹配的日志行列表(`List<String>`),每行包含时间戳和日志内容。
+
+### 示例请求
+
+```http
+GET /v2/ai/order/v2/taskLog?taskId=b07cbbc8-1f8f-4060-98f8-051b66ccf1ab
+```
+
+### 示例响应
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "success": true,
+  "data": [
+    "2026-03-18T10:05:00.123456789Z INFO [task] Processing task b07cbbc8-1f8f-4060-98f8-051b66ccf1ab",
+    "2026-03-18T10:05:01.234567890Z INFO [task] Task b07cbbc8-1f8f-4060-98f8-051b66ccf1ab completed"
+  ],
+  "timestamp": 1773033600000
+}
+```
+
+### 示例响应(Docker Socket未挂载)
+
+```json
+{
+  "code": 500,
+  "message": "Docker socket 不存在: /var/run/docker.sock,请确认已挂载 -v /var/run/docker.sock:/var/run/docker.sock",
+  "success": false,
+  "data": null,
+  "timestamp": 1773033600000
+}
+```


+ 1 - 0
src/api/style.ts

@@ -5,6 +5,7 @@ export interface StyleItem {
   name?: string;
   describe?: string;
   author?: string;
+  mediaType?: string;
   coverImg?: string;
   detailImg?: string;
   originalStandardPlanPrice?: number;

+ 7 - 0
src/i18n/locales/en.json

@@ -146,6 +146,7 @@
       "standardPic": "Number of images generated by the standard plan",
       "apiType": "AI platform, currently only General is available",
       "styleType": "Trial: all users; VIP: VIP users only",
+      "mediaType": "Generated media type: image, video, or mixed image and video",
       "aiType": "Filter ability identifier",
       "aiContent": "Default params for AI API calls (JSON format)",
       "showType": "Frontend display type",
@@ -214,6 +215,7 @@
       "apiType": "AI Platform",
       "aiContent": "AI Default Params",
       "aiType": "Filter Ability",
+      "mediaType": "Media Type",
       "requirements": "Requirements",
       "styleType": "Style Type",
       "previewImg": "Video Preview"
@@ -222,6 +224,11 @@
       "trial": "Trial",
       "vip": "VIP"
     },
+    "mediaTypeOptions": {
+      "image": "Image",
+      "video": "Video",
+      "mixed": "Mixed"
+    },
     "apiTypeOptions": {
       "general": "General"
     },

+ 7 - 0
src/i18n/locales/zh.json

@@ -146,6 +146,7 @@
       "standardPic": "标准套餐生成的图片数量",
       "apiType": "AI 平台,当前仅支持通用平台",
       "styleType": "可试用:所有用户可用;VIP:仅 VIP 用户",
+      "mediaType": "生成结果的媒体类型:图片、视频或图片视频混合",
       "aiType": "滤镜能力标识",
       "aiContent": "调用 AI API 时的默认参数(JSON 格式)",
       "showType": "前端展示类型",
@@ -214,6 +215,7 @@
       "apiType": "AI 平台",
       "aiContent": "AI 默认参数",
       "aiType": "滤镜能力",
+      "mediaType": "媒体类型",
       "requirements": "Requirements",
       "styleType": "模版类型",
       "previewImg": "视频封面预览"
@@ -222,6 +224,11 @@
       "trial": "可试用",
       "vip": "VIP"
     },
+    "mediaTypeOptions": {
+      "image": "图片",
+      "video": "视频",
+      "mixed": "图片视频混合"
+    },
     "apiTypeOptions": {
       "general": "通用平台"
     },

+ 18 - 0
src/pages/styles/StyleFormPage.tsx

@@ -69,6 +69,7 @@ export function StyleFormPage() {
           form.setFieldsValue({
             ...data,
             apiType: data.apiType ?? 2,
+            mediaType: data.mediaType ?? "0",
             showType: data.showType ?? 0,
             aiType: data.aiType || "workflow",
             stylemode: data.stylemode ?? 0,
@@ -119,6 +120,7 @@ export function StyleFormPage() {
     } else {
       form.setFieldsValue({
         apiType: 2,
+        mediaType: "0",
         showType: 0,
         styleType: 0,
         stylemode: 0,
@@ -688,6 +690,22 @@ export function StyleFormPage() {
                 />
               </Form.Item>
 
+              <Form.Item
+                label={<HintLabel label={t("style.fields.mediaType")} hint={t("style.hints.mediaType")} />}
+                name="mediaType"
+                rules={[{ required: true, message: t("style.required") }]}
+              >
+                <Radio.Group
+                  optionType="button"
+                  buttonStyle="solid"
+                  options={[
+                    { label: t("style.mediaTypeOptions.image"), value: "0" },
+                    { label: t("style.mediaTypeOptions.video"), value: "1" },
+                    { label: t("style.mediaTypeOptions.mixed"), value: "2" },
+                  ]}
+                />
+              </Form.Item>
+
               <Form.Item
                 label={<HintLabel label={t("style.fields.templateZip")} hint={t("style.hints.templateZip")} />}
                 name="templateZip"

+ 13 - 0
src/pages/styles/StylePage.tsx

@@ -109,6 +109,16 @@ export function StylePage() {
     }).join(" / ");
   }
 
+  function mediaTypeConfig(val?: string) {
+    if (val === "1") {
+      return { color: "purple" as const, label: t("style.mediaTypeOptions.video") };
+    }
+    if (val === "2") {
+      return { color: "cyan" as const, label: t("style.mediaTypeOptions.mixed") };
+    }
+    return { color: "blue" as const, label: t("style.mediaTypeOptions.image") };
+  }
+
   const columns: TableProps<StyleItem>["columns"] = [
     {
       title: "ID",
@@ -170,6 +180,9 @@ export function StylePage() {
         <div className="flex flex-col gap-1">
           {r.styleType === 0 && <Tag color="green" style={{ margin: 0, width: "fit-content" }}>{t("style.styleTypeOptions.trial")}</Tag>}
           {r.styleType === 1 && <Tag color="gold" style={{ margin: 0, width: "fit-content" }}>{t("style.styleTypeOptions.vip")}</Tag>}
+          <Tag color={mediaTypeConfig(r.mediaType).color} style={{ margin: 0, width: "fit-content" }}>
+            {mediaTypeConfig(r.mediaType).label}
+          </Tag>
           {r.platform && (
             <span className="text-xs text-gray-500">{platformLabel(r.platform)}</span>
           )}