diff --git a/.gitignore b/.gitignore index bbc5717e4727..59f3eb7120a7 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ data/ .gomodcache/ .gocache-temp .gopath -.test token_estimator_test.go +# Local copy of vendor API doc; not tracked (example URLs may trip GitHub push protection) +Pingxingshijie-AI接口.md +.test skills-lock.json diff --git a/common/api_type.go b/common/api_type.go index 39c1fe9a5406..3409d8f3ee7b 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -53,6 +53,12 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = constant.APITypeMokaAI case constant.ChannelTypeVolcEngine: apiType = constant.APITypeVolcEngine + case constant.ChannelTypePingXingShiJie: + // Text/embed/rerank/image sync: same HTTP paths as Volcengine Ark (/api/v3/*). Async tasks still use task/pingxingshijie. + apiType = constant.APITypeVolcEngine + case constant.ChannelTypeKieAI: + // KieAI currently exposes async Market jobs through task/kie; sync calls fall back to OpenAI-compatible handling when used. + apiType = constant.APITypeOpenAI case constant.ChannelTypeBaiduV2: apiType = constant.APITypeBaiduV2 case constant.ChannelTypeOpenRouter: diff --git a/constant/channel.go b/constant/channel.go index 48502bedc52c..d31d60aa8f1d 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -55,6 +55,8 @@ const ( ChannelTypeSora = 55 ChannelTypeReplicate = 56 ChannelTypeCodex = 57 + ChannelTypePingXingShiJie = 58 + ChannelTypeKieAI = 59 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -118,6 +120,8 @@ var ChannelBaseURLs = []string{ "https://api.openai.com", //55 "https://api.replicate.com", //56 "https://chatgpt.com", //57 + "https://api.pingxingshijie.cn", //58 PingXingShiJie + "https://api.kie.ai", //59 KieAI } var ChannelTypeNames = map[int]string{ @@ -175,6 +179,8 @@ var ChannelTypeNames = map[int]string{ ChannelTypeSora: "Sora", ChannelTypeReplicate: "Replicate", ChannelTypeCodex: "Codex", + ChannelTypePingXingShiJie: "PingXingShiJie", + ChannelTypeKieAI: "KieAI", } func GetChannelTypeName(channelType int) string { diff --git a/constant/task.go b/constant/task.go index ecccf4dfe119..d700c81e2716 100644 --- a/constant/task.go +++ b/constant/task.go @@ -4,7 +4,7 @@ type TaskPlatform string const ( TaskPlatformSuno TaskPlatform = "suno" - TaskPlatformMidjourney = "mj" + TaskPlatformMidjourney TaskPlatform = "mj" ) const ( @@ -12,6 +12,7 @@ const ( SunoActionLyrics = "LYRICS" TaskActionGenerate = "generate" + TaskActionAssetUpload = "assetUpload" TaskActionTextGenerate = "textGenerate" TaskActionFirstTailGenerate = "firstTailGenerate" TaskActionReferenceGenerate = "referenceGenerate" diff --git a/controller/channel-test.go b/controller/channel-test.go index b225585ed7a3..4cd98b2a0afa 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -122,6 +122,11 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, requestPath = "/v1/images/generations" } + // PingXingShiJie: sync image models use OpenAI image relay; text uses chat (Volc-style upstream via adaptor mapping). + if channel.Type == constant.ChannelTypePingXingShiJie && strings.Contains(strings.ToLower(testModel), "seedream") { + requestPath = "/v1/images/generations" + } + // responses-only models if strings.Contains(strings.ToLower(testModel), "codex") { requestPath = "/v1/responses" diff --git a/controller/model.go b/controller/model.go index 4dbd45838dd8..aa13f283a4a8 100644 --- a/controller/model.go +++ b/controller/model.go @@ -14,6 +14,8 @@ import ( "github.com/QuantumNous/new-api/relay/channel/lingyiwanwu" "github.com/QuantumNous/new-api/relay/channel/minimax" "github.com/QuantumNous/new-api/relay/channel/moonshot" + taskkie "github.com/QuantumNous/new-api/relay/channel/task/kie" + taskpxsj "github.com/QuantumNous/new-api/relay/channel/task/pingxingshijie" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" "github.com/QuantumNous/new-api/service" @@ -79,6 +81,14 @@ func init() { OwnedBy: minimax.ChannelName, }) } + for _, modelName := range taskkie.ModelList { + openAIModels = append(openAIModels, dto.OpenAIModels{ + Id: modelName, + Object: "model", + Created: 1626777600, + OwnedBy: taskkie.ChannelName, + }) + } for modelName, _ := range constant.MidjourneyModel2Action { openAIModels = append(openAIModels, dto.OpenAIModels{ Id: modelName, @@ -93,6 +103,14 @@ func init() { } channelId2Models = make(map[int][]string) for i := 1; i <= constant.ChannelTypeDummy; i++ { + if i == constant.ChannelTypePingXingShiJie { + channelId2Models[i] = append([]string(nil), taskpxsj.ModelList...) + continue + } + if i == constant.ChannelTypeKieAI { + channelId2Models[i] = append([]string(nil), taskkie.ModelList...) + continue + } apiType, success := common.ChannelType2APIType(i) if !success || apiType == constant.APITypeAIProxyLibrary { continue diff --git a/controller/model_list_test.go b/controller/model_list_test.go index 97d27cae5c6c..7fd81d914fd1 100644 --- a/controller/model_list_test.go +++ b/controller/model_list_test.go @@ -12,6 +12,7 @@ import ( "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" + taskkie "github.com/QuantumNous/new-api/relay/channel/task/kie" "github.com/QuantumNous/new-api/setting/config" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/gin-gonic/gin" @@ -210,6 +211,14 @@ func TestListModelsIncludesTieredBillingModel(t *testing.T) { require.Empty(t, missingExprPricing.BillingExpr) } +func TestKieModelsAppearInGlobalModelRegistry(t *testing.T) { + for _, modelName := range taskkie.ModelList { + aiModel, ok := openAIModelsMap[modelName] + require.True(t, ok, "missing Kie model %s", modelName) + require.Equal(t, taskkie.ChannelName, aiModel.OwnedBy) + } +} + func TestListModelsTokenLimitIncludesTieredBillingModel(t *testing.T) { withSelfUseModeDisabled(t) withTieredBillingConfig(t, map[string]string{ diff --git a/controller/relay.go b/controller/relay.go index c97ab45b4ac4..4dfc7216a21e 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -19,6 +19,7 @@ import ( relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relay/helper" + taskpxsj "github.com/QuantumNous/new-api/relay/channel/task/pingxingshijie" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" @@ -572,6 +573,7 @@ func RelayTask(c *gin.Context) { task := model.InitTask(result.Platform, relayInfo) task.PrivateData.UpstreamTaskID = result.UpstreamTaskID + task.PrivateData.UpstreamKind = taskpxsj.UpstreamKindFromPath(c.Request.URL.Path) task.PrivateData.BillingSource = relayInfo.BillingSource task.PrivateData.SubscriptionId = relayInfo.SubscriptionId task.PrivateData.TokenId = relayInfo.TokenId diff --git a/docs/pingxingshijie-api-reference.md b/docs/pingxingshijie-api-reference.md new file mode 100644 index 000000000000..82d3a2513cf0 --- /dev/null +++ b/docs/pingxingshijie-api-reference.md @@ -0,0 +1,409 @@ +# PingXingShiJie(渠道 58)下游 API 参考 + +本文档描述 **经本网关(new-api)** 访问平行视界能力时的 HTTP 接口:请求参数、默认值、取值范围说明、成功/失败响应与字段含义。 +上游原始规范见厂商文档(本地可参考 `Pingxingshijie-AI接口.md`,默认不纳入版本库以免示例链接触发密钥扫描)。网关将请求转发至渠道配置的 Base URL(默认 `https://api.pingxingshijie.cn`),并对异步任务做统一落库与轮询。 + +**相关文档** + +- [OpenAI 兼容性说明](./pingxingshijie-openai-compatibility.md) + +--- + +## 1. 通用约定 + +### 1.1 认证与请求头 + +| 参数名 | 位置 | 必填 | 值范围 / 格式 | 默认值 | 含义 | +|--------|------|------|----------------|--------|------| +| `Authorization` | Header | 是 | `Bearer ` | — | 与本系统其它 API 一致的访问令牌 | +| `Content-Type` | Header | POST 时建议固定 | `application/json` | — | JSON 请求体 | +| `Accept` | Header | 否 | `application/json` | — | 响应一般为 JSON | + +**说明**:下列路由挂在 `router/video-router.go` 的 `/v1` 分组上,使用 **Token 认证** + **Distribute(按模型选渠道)**。调用方必须在渠道中配置 **渠道类型 58(PingXingShiJie)**,且令牌对该渠道上的 **model** 有权限。 + +### 1.2 模型与选路(Distributor) + +| 参数名 | 位置 | 必填 | 含义 | +|--------|------|------|------| +| `model` | Body(JSON) | 对 **POST 提交类**接口为 **是**(分发器强校验) | 与后台渠道/模型配置一致的模型 ID;用于选择渠道与映射上游模型名 | + +**说明**:`GET` 查询任务状态类接口 **不需要** 在 Body 中带 `model`(分发器对对应路径 `shouldSelectChannel = false`)。 + +### 1.3 常见 HTTP 状态码(任务类接口) + +| HTTP | 含义(摘要) | +|------|----------------| +| 200 | 成功(GET/POST 业务成功) | +| 400 | 请求体非法、缺少 `model`/`prompt`、任务不存在等 | +| 401 | 未授权或 Token 无效 | +| 402 / 403 / 429 | 额度、权限、限流等(与本系统全局策略一致) | +| 5xx | 网关或上游异常 | + +### 1.4 统一错误响应 Body(任务类失败时) + +业务错误通过 `dto.TaskError` 返回(常见字段如下)。 + +| 字段名 | 类型 | 含义 | +|--------|------|------| +| `code` | string | 机器可读错误码,如 `invalid_request`、`get_channel_failed`、`task_not_exist` | +| `message` | string | 人类可读说明 | +| `data` | any | 附加数据,可为 `null` | + +**说明**:部分场景下上游返回 HTTP 200 但 JSON `code != 0`,网关会转换为任务错误,不视为成功。 + +### 1.5 成功提交时的附加响应头 + +| 头名称 | 含义 | +|--------|------| +| `X-New-Api-Other-Ratios` | JSON 字符串:计费相关的其它倍率(如视频含视频输入时的 `video_input` 等),用于客户端或调试观测 | + +### 1.6 文本对话(Chat Completions) + +| 项目 | 说明 | +|------|------| +| 客户端路由 | 与其它渠道一致,使用全局 **`POST /v1/chat/completions`**(见 `router/relay-router.go`),认证与分组规则不变。 | +| 网关 → 上游 | 渠道类型 **58** 复用 **Volcengine** adaptor 做请求/鉴权/部分响应处理;文本上游路径为 **`{Base URL}/v2/chat/completions`**(与视频/图等 **`/v2/*`** 同一 API 族)。**不要**使用 `{Base URL}/v1/chat/completions` 或 `/api/v3/chat/completions`:在该域名下会返回 HTTP 400「接口不存在」。Base URL 默认 `https://api.pingxingshijie.cn`。 | +| `model` | Body 必填;值为控制台/方舟侧 **endpoint 模型 ID**(与后台模型映射一致)。 | +| 响应包装 | 若上游 JSON **根级含 `code`**(含 **`/v2/chat/completions`** 业务错误如 **`{"code":401,"message":"..."}`**),网关在 **非流式** 下会:对 **`code != 0`** 直接返回对应 HTTP 状态与文案;对 **`code == 0`** 且含 **`data`** 时解包 **`data`** 后再按 OpenAI Chat Completion 解析。无根级 **`code`** 的纯 OpenAI 形体会原样解析。流式(SSE)仍透传上游字节流(若上游对流也包装,需客户端或后续版本单独适配)。 | +| 后台「测试渠道」 | 已支持 58:模型名 **含 `seedream`**(不区分大小写)时走 **`/v1/images/generations`** 测同步图;否则默认走 **`/v1/chat/completions`** 测文本。 | + +--- + +## 2. 视频生成 + +### 2.1 提交任务 + +**`POST /v1/video/generations`** + +网关将 Body 解析为统一任务结构 `TaskSubmitReq`,再转换为上游 `/v2/video/generations` 所需的 Ark 形请求(`content` 数组 + 顶层参数)。**未显式设置 `generate_audio` 时,网关会默认置为 `true`** 再转发上游。 + +#### 2.1.1 Body 顶层字段(网关 `TaskSubmitReq`) + +| 参数名 | 类型 | 必填 | 默认值 | 值范围 / 说明 | +|--------|------|------|--------|----------------| +| `model` | string | **是** | — | 须在渠道可用模型列表中;示例见 `Pingxingshijie-AI接口.md` §视频生成(如 `doubao-seedance-2-0-fast-260128` 等) | +| `prompt` | string | **是**(非空) | — | 文生视频主提示词;会与 `metadata` 中的多模态 `content` 合并(见下) | +| `metadata` | object | 否 | — | 与上游视频请求对齐的扩展字段;见 **2.1.2** | +| `images` | string[] | 否 | — | 便捷图生通道:每个元素映射为一条 `type: image_url` 的 content 项(URL / Base64 / `asset://...` 等规则见上游文档) | +| `image` | string | 否 | — | 单图;会并入 `images` | +| `seconds` | string | 否 | — | 可解析为整数的秒数字符串,映射上游 `duration`(秒) | +| `duration` | number | 否 | — | 与 `seconds` 二选一语义,整数秒 | +| `size` | string | 否 | — | 其它任务类型复用字段;本链路主要尺寸信息见 `metadata` / 上游 | +| `mode` | string | 否 | — | 预留 | +| `input_reference` | string | 否 | — | 与其它任务类型对齐的参考图字段 | + +**`prompt` 与 `metadata.content` 的合并规则(实现摘要)** + +- 若 `metadata.content` 中已含 **`draft_task`**,则不再自动追加文本项。 +- 否则:先合并 `metadata` 解析出的 `content`/`resolution`/`ratio` 等,再追加一条 `type: text`、`text: prompt` 的条目(并过滤掉仅用于占位的旧 text 项)。 + +**Seedance 1.5 Pro:草稿任务 ID 放大(仅 `draft_task`,不需要 `metadata.draft`)** + +「草稿 id 放大」只需在 **`metadata.content`** 里提供 **`type: "draft_task"`** 与 **`draft_task.id`**(指向上游已返回的 **draft 预览**任务 id,如 `cgt-...`)。**不需要**单独设置 **`metadata.draft`**;该布尔字段与「引用草稿任务 id 做放大」不是同一语义。网关仍会校验顶层 **`prompt` 非空**(可与业务无关的占位文案),但不会把 `prompt` 当作 `content` 文本发给上游。 + +`resolution` 在 Seedance 1.5 Pro 放大场景下应为 **`720p`** 或 **`1080p`**(若误传 `480p` 等,网关会对该模型归一为合法放大档位)。其它 `metadata` 字段(如 **`watermark`**、**`return_last_frame`**)按上游支持情况原样合并。 + +示例(下游 OpenAI 风格任务体): + +```json +{ + "model": "doubao-seedance-1-5-pro-251215", + "prompt": "Generate from draft", + "metadata": { + "content": [ + { + "type": "draft_task", + "draft_task": { + "id": "cgt-20260416103233-xccct" + } + } + ], + "watermark": false, + "resolution": "720p", + "return_last_frame": true + } +} +``` + +#### 2.1.2 `metadata` 内常用字段(与上游 `requestPayload` 对齐) + +下列字段由网关合并进上游 JSON(与 `Pingxingshijie-AI接口.md` 中 POST body 一致;具体枚举与约束以该文档为准)。 + +| 参数名 | 类型 | 必填 | 默认值 | 含义 / 值范围(摘要) | +|--------|------|------|--------|------------------------| +| `content` | array | 条件必填 | — | 多模态条目:`type` 为 `text` \| `image_url` \| `video_url` \| `audio_url` \| `draft_task` 等;结构见上游文档 | +| `resolution` | string | 视模型而定 | 上游默认如 `720p` | 如 `480p`、`720p` | +| `ratio` | string | 视模型而定 | — | 如 `16:9`、`9:16`、`adaptive` 等 | +| `duration` | number | 视模型而定 | — | 整数秒;Seedance 2.0 等范围见上游文档(如 `[4,15]` 或 `-1`) | +| `generate_audio` | boolean | 否 | **网关默认 `true`** | 是否生成与画面对齐的声音;若客户端在 `metadata` 中显式传入,则以客户端为准 | +| `watermark` | boolean | 否 | — | 是否水印 | +| `draft` | boolean | 否 | — | 草稿任务相关 | +| `return_last_frame` | boolean | 否 | — | 是否返回尾帧等(BoolValue) | +| `service_tier` | string | 否 | — | 服务层级 | +| `execution_expires_after` | number | 否 | — | 执行过期(IntValue) | +| `frames` / `seed` | number | 否 | — | 帧数、随机种子等 | +| `camera_fixed` | boolean | 否 | — | 相机固定 | +| `tools` | array | 否 | — | 工具声明 | +| `callback_url` | string | 否 | — | 回调 URL | + +**计费侧**:若 `metadata.content` 中存在 **视频输入**(`video_url` 等),网关可能对模型应用 **`video_input` 倍率**(见渠道侧配置与 `pingxingshijie` 常量中的映射)。 + +#### 2.1.3 成功响应 HTTP 200 — Body(`dto.OpenAIVideo`) + +提交成功后返回 **网关公开任务 ID**(**不**直接暴露上游 `cgt-...` 在对外 id 字段中;上游 ID 存于任务私有数据用于轮询)。 + +| 字段名 | 类型 | 含义 | +|--------|------|------| +| `id` | string | 公开任务 ID(与 `task_id` 相同语义) | +| `task_id` | string | 与 `id` 一致,兼容字段 | +| `object` | string | 固定为 `video` | +| `model` | string | 请求中的业务模型名(映射前/后以网关实现为准,一般为客户端传入的模型别名) | +| `status` | string | 初始多为 `queued`(`dto.VideoStatusQueued`) | +| `progress` | number | 初始多为 `0` | +| `created_at` | number | Unix 时间戳(秒) | + +**说明**:客户端应使用返回的 **`id` 或 `task_id`** 作为后续 **GET 查询** 的路径参数。 + +--- + +### 2.2 查询视频任务 + +**`GET /v1/video/generations/:task_id`** + +| 参数名 | 位置 | 必填 | 含义 | +|--------|------|------|------| +| `task_id` | Path | 是 | 提交接口返回的 **公开** `task_id` | + +**无 Body**。 + +#### 成功响应 HTTP 200 — Body(`dto.OpenAIVideo`) + +| 字段名 | 类型 | 含义 | +|--------|------|------| +| `id` | string | 公开任务 ID | +| `task_id` | string | 同 `id` | +| `object` | string | `video` | +| `model` | string | 任务属性中的原始模型名 | +| `status` | string | `queued` \| `in_progress` \| `completed` \| `failed` \| `unknown`(由内部状态映射) | +| `progress` | number | 0–100,来自内部进度字符串 | +| `created_at` | number | 创建时间戳(秒) | +| `completed_at` | number | 更新时间戳(秒),有则返回 | +| `metadata` | object | 可选;其中常见 **`url`** 为视频可播放地址(来自上游 `content.video_url`) | +| `error` | object | 失败时存在:`message`、`code` | + +--- + +### 2.3 OpenAI 风格视频路由(可选) + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/v1/videos` | POST | 提交;Body 仍通过任务通道解析(multipart 等以网关实现为准) | +| `/v1/videos/:task_id` | GET | 查询;响应形状与 **2.2** 相同(`OpenAIVideo`) | + +模型、Body 字段与 **2.1** 同源逻辑,仅路径不同。 + +--- + +### 2.4 视频 Remix(可选) + +**`POST /v1/videos/:video_id/remix`** + +| 参数名 | 位置 | 必填 | 含义 | +|--------|------|------|------| +| `video_id` | Path | 是 | **本系统内**已有任务的公开 ID(用于锁定渠道与继承计费上下文) | +| Body | JSON | 是 | 与其它任务提交类似的 `TaskSubmitReq`(`model`、`prompt`、`metadata` 等) | + +成功/失败响应与任务提交通道一致;详细约束见 `relay/relay_task.go` 中 `ResolveOriginTask`。 + +--- + +## 3. 图片生成(异步) + +### 3.1 提交任务 + +**`POST /v1/images/generations/async`** + +Body **原样转发**为上游 `POST /v2/image/generations` 的 JSON,仅将 **`model`** 替换为映射后的上游模型名。校验走 `ValidateBasicTaskRequest`:**`prompt` 必填非空**,且 **`model` 必填**(用于分发)。 + +#### 3.1.1 Body 字段(与上游 Seedream 对齐 — 摘要) + +完整参数、枚举与约束以 **`Pingxingshijie-AI接口.md` §提交图片生成任务** 为准。常见字段如下: + +| 参数名 | 类型 | 必填 | 默认值 | 含义 / 范围(摘要) | +|--------|------|------|--------|---------------------| +| `model` | string | **是** | — | 如 `doubao-seedream-5-0-260128`、`doubao-seedream-4-0-250828` 等 | +| `prompt` | string | **是** | — | 文生/图生提示词 | +| `image` | string 或 string[] | 视模式 | — | 参考图 URL 列表等(见上游) | +| `sequential_image_generation` | string | 视模型 | — | 如 `auto` | +| `sequential_image_generation_options` | object | 视模型 | — | 如 `max_images` | +| `size` | string | 视模型 | — | 如 `2K` | +| `output_format` | string | 视模型 | — | 如 `png` | +| `watermark` | boolean | 视模型 | — | 是否水印 | + +#### 3.1.2 成功响应 HTTP 200 + +为兼容现有任务通道,提交成功时 Body 仍使用 **`dto.OpenAIVideo`** 外壳(`object: video`),便于客户端统一处理异步任务: + +| 字段名 | 类型 | 含义 | +|--------|------|------| +| `id` | string | **公开任务 ID**(用于 GET 查询,**不是**上游 `I20...` 任务号) | +| `task_id` | string | 同 `id` | +| `object` | string | `video`(历史兼容) | +| `model` | string | 请求模型名 | +| `status` | string | 初始多为 `queued` | +| `progress` | number | 初始多为 `0` | +| `created_at` | number | Unix 秒 | + +**说明**:上游返回的图片任务 ID 保存在服务端任务记录中,用于轮询上游 `GET /v2/image/generations/tasks/{id}`,**客户端只需保存公开 `task_id`**。 + +--- + +### 3.2 查询图片任务 + +**`GET /v1/images/generations/:task_id`** + +| 参数名 | 位置 | 必填 | 含义 | +|--------|------|------|------| +| `task_id` | Path | 是 | 提交接口返回的 **公开** 任务 ID | + +#### 成功响应 HTTP 200 — Body(扩展 JSON) + +| 字段名 | 类型 | 含义 | +|--------|------|------| +| `object` | string | 固定 `pingxingshijie.image.generation.task` | +| `id` | string | 公开任务 ID | +| `task_id` | string | 同 `id` | +| `status` | string | `queued` \| `in_progress` \| `completed` \| `failed` \| `unknown` | +| `progress` | string | 内部进度,如 `50%` | +| `model` | string | 原始模型名 | +| `created_at` | number | 秒级时间戳 | +| `updated_at` | number | 秒级时间戳 | +| `url` | string | 成功时可能存在:结果图地址(来自上游 `content.image_url`) | +| `error` | object | 失败时可能存在:`message`、`code` | + +--- + +## 4. 素材(Asset)异步 + +### 4.1 提交上传任务 + +**`POST /v1/assets/upload`** + +Body 转发至上游 `POST /v2/asset/upload`。分发器要求 JSON 中必须显式传入 **`model`**,且素材上传只允许专用模型名 **`pingxingshijie-asset`**,用于网关选路与计费;转发上游时会移除该网关专用字段。 + +#### 4.1.1 Body 字段(与上游一致 — 摘要) + +详见 **`Pingxingshijie-AI接口.md` §素材上传**。 + +| 参数名 | 类型 | 必填 | 默认值 | 含义 / 范围(摘要) | +|--------|------|------|--------|---------------------| +| `model` | string | 是 | 无 | 必须为 `pingxingshijie-asset`;其它模型会被拒绝,避免素材上传错误使用生图/视频模型计费或归类 | +| `image_url` | string | **是**(上游) | — | 素材来源 URL(或其它上游允许的形式) | +| `asset_type` | string | **是**(上游) | — | `Image` \| `Video` \| `Audio` | + +**网关行为摘要** + +- 转发给上游的 JSON 会保留 `image_url`、`asset_type` 等素材字段,但会移除网关专用的 `model` 字段。 +- 分发器需要 `model` 选渠道和计费;素材上传必须显式使用 **`pingxingshijie-asset`**。 +- 适配器在校验阶段可能为内部 `TaskSubmitReq` 填入占位 `prompt`(如 `asset-upload`),**仅用于本网关任务校验/计费上下文**,**不要求**客户端在 Body 中携带 `prompt`。 + +#### 4.1.2 成功响应 HTTP 200 + +| 字段名 | 类型 | 含义 | +|--------|------|------| +| `id` | string | 公开任务 ID | +| `task_id` | string | 同 `id` | +| `asset_id` | string | 上游素材 ID(用于后续 `asset://...` 引用) | +| `object` | string | `pingxingshijie.asset.upload` | + +--- + +### 4.2 查询素材任务 + +**`GET /v1/assets/:asset_id`** + +| 参数名 | 位置 | 必填 | 含义 | +|--------|------|------|------| +| `asset_id` | Path | 是 | 上传响应返回的 **上游素材 ID**(`asset_id`) | + +服务端按该 `asset_id` 找到本地任务记录,并周期性以 `POST /v2/asset/status`(body: `{"asset_id":"<上游ID>"}`)轮询上游。为兼容旧客户端,若未找到 `asset_id`,网关仍会回退尝试公开 `task_id`。 + +#### 成功响应 HTTP 200 — Body + +| 字段名 | 类型 | 含义 | +|--------|------|------| +| `object` | string | `pingxingshijie.asset.task` | +| `id` | string | 公开任务 ID | +| `task_id` | string | 同 `id` | +| `status` | string | 内部任务状态枚举字符串,如 `QUEUED`、`IN_PROGRESS`、`SUCCESS`、`FAILURE` 等 | +| `progress` | string | 如 `50%`、`100%` | +| `created_at` | number | 秒 | +| `updated_at` | number | 秒 | +| `data` | object | 最近一次上游 **envelope 内层 data** 快照(含 `Result.Status`、`Result.URL` 等,字段名与上游一致) | +| `fail_reason` | string | 失败原因(若有) | + +**上游 `Result.Status` 语义(轮询侧)**:`Processing` → 进行中;`Active` → 成功可用;`Failed` → 失败(详见上游文档)。 + +--- + +## 5. 内部任务状态与对外 `status` 映射(摘要) + +| 内部 `TaskStatus` | 视频/图片 GET 中 `status`(`ToVideoStatus`) | +|-------------------|-----------------------------------------------| +| `QUEUED` / `SUBMITTED` | `queued` | +| `IN_PROGRESS` | `in_progress` | +| `SUCCESS` | `completed` | +| `FAILURE` | `failed` | +| 其它 | `unknown` | + +素材 GET 的 `status` 字段为 **内部枚举字符串**,与上表不完全相同,请以 **4.2** 为准。 + +--- + +## 6. curl 示例(最佳实践) + +```bash +export GATEWAY='https://your-new-api.example.com' +export TOKEN='your-token' + +# 视频提交 +curl -sS -X POST "${GATEWAY}/v1/video/generations" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"model":"doubao-seedance-2-0-fast-260128","prompt":"Hello","metadata":{"resolution":"720p","ratio":"16:9"}}' + +# 视频查询(TASK_ID 为返回的 id/task_id) +curl -sS "${GATEWAY}/v1/video/generations/${TASK_ID}" -H "Authorization: Bearer ${TOKEN}" + +# 图片异步提交 +curl -sS -X POST "${GATEWAY}/v1/images/generations/async" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"model":"doubao-seedream-4-0-250828","prompt":"A red mug","size":"2K","output_format":"png","watermark":false}' + +# 素材上传 +curl -sS -X POST "${GATEWAY}/v1/assets/upload" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ +-d '{"model":"pingxingshijie-asset","image_url":"https://example.com/a.jpg","asset_type":"Image"}' +``` + +--- + +## 7. 路径一览 + +| 能力 | 方法 | 路径 | +|------|------|------| +| 视频提交 | POST | `/v1/video/generations` | +| 视频查询 | GET | `/v1/video/generations/:task_id` | +| 视频(OpenAI 形) | POST/GET | `/v1/videos`、`/v1/videos/:task_id` | +| 视频 Remix | POST | `/v1/videos/:video_id/remix` | +| 图片异步提交 | POST | `/v1/images/generations/async` | +| 图片异步查询 | GET | `/v1/images/generations/:task_id` | +| 素材上传 | POST | `/v1/assets/upload` | +| 素材查询 | GET | `/v1/assets/:asset_id` | +| 视频内容代理 | GET | `/v1/videos/:task_id/content`(`TokenOrUserAuth`,见 `video-router`) | + +--- + +**文档版本**:与网关实现 `relay/channel/task/pingxingshijie`、`router/video-router.go` 同步;若行为变更,请以代码为准并更新本文档。 diff --git a/docs/pingxingshijie-downstream-curl.md b/docs/pingxingshijie-downstream-curl.md new file mode 100644 index 000000000000..016dda8ce66e --- /dev/null +++ b/docs/pingxingshijie-downstream-curl.md @@ -0,0 +1,7 @@ +# PingXingShiJie — Downstream curl (deprecated as standalone spec) + +Detailed parameter tables, response field meanings, and curl examples are maintained in: + +**[pingxingshijie-api-reference.md](./pingxingshijie-api-reference.md)** + +OpenAI compatibility notes remain in [pingxingshijie-openai-compatibility.md](./pingxingshijie-openai-compatibility.md). diff --git a/docs/pingxingshijie-openai-compatibility.md b/docs/pingxingshijie-openai-compatibility.md new file mode 100644 index 000000000000..e0b9091474dc --- /dev/null +++ b/docs/pingxingshijie-openai-compatibility.md @@ -0,0 +1,30 @@ +# PingXingShiJie (channel 58) — OpenAI compatibility notes + +This document describes how the gateway exposes PingXingShiJie async APIs next to OpenAI-style routes, and where behavior differs from official OpenAI APIs. + +**Downstream API reference (parameters, responses, curl):** [pingxingshijie-api-reference.md](./pingxingshijie-api-reference.md). + +## Shared conventions + +- **Authentication**: Same as other channels — `Authorization: Bearer ` on gateway routes that use token auth. +- **Base URL (upstream)**: Default `https://api.pingxingshijie.cn` (overridable per channel). Upstream responses use a unified envelope: `{"code":0,"msg":"...","data":{...}}` with HTTP 200; business errors use non-zero `code`. + +## Routes aligned with existing gateway / OpenAI-style usage + +| Capability | Gateway routes | Upstream (PingXingShiJie) | +|------------|----------------|---------------------------| +| Text chat | `POST /v1/chat/completions` (same global relay router as other OpenAI-style channels) | `POST /v2/chat/completions` on the channel **Base URL** (same `/v2/*` family as video/image; `/v1/chat/completions` and `/api/v3/chat/completions` return HTTP 400 “接口不存在” on the default host) | +| Video async | `POST /v1/video/generations`, `POST /v1/videos`, `GET /v1/video/generations/:task_id`, `GET /v1/videos/:task_id` | `POST /v2/video/generations`, `GET /v2/video/generations/tasks/{id}` | +| Image async | `POST /v1/images/generations/async`, `GET /v1/images/generations/:task_id` | `POST /v2/image/generations`, `GET /v2/image/generations/tasks/{id}` | +| Asset async | `POST /v1/assets/upload`, `GET /v1/assets/:asset_id` | `POST /v2/asset/upload`, `POST /v2/asset/status` (polled server-side) | + +## Differences from OpenAI + +- **Chat**: The gateway still exposes **`/v1/chat/completions`** to clients. For channel 58 the relay reuses the **Volcengine** adaptor, but the **upstream URL** is **`{Base URL}/v2/chat/completions`**. The default host does not serve working **`/v1/chat/completions`** or **`/api/v3/chat/completions`** for this product (they return HTTP 400 “接口不存在”). When the upstream returns a JSON envelope with a top-level **`code`** (including **`message`** instead of **`msg`** for errors), **non-streaming** responses are normalized: **`code != 0`** is surfaced as an HTTP error; **`code == 0`** with **`data`** unwraps **`data`** before OpenAI-shaped parsing. +- **Official `POST /v1/images/generations` (OpenAI)**: Synchronous image URL in the response body. **This gateway’s** `POST /v1/images/generations/async` is **async**: it returns a public `task_id` and requires **`GET /v1/images/generations/:task_id`** (or the unified task APIs) to poll until completion. Clients must not assume OpenAI’s synchronous semantics on the async route. +- **Video**: `generate_audio` is sent upstream; when omitted in the mapped request, it defaults to **true** (per provider contract). +- **Assets**: There is **no** OpenAI-standard equivalent. `POST /v1/assets/upload` / `GET /v1/assets/:asset_id` are **gateway extensions** for PingXingShiJie asset upload and status surfaced as a single task record. Clients should pass the `asset_id` returned by upload; the gateway keeps a fallback for legacy public `task_id` lookups. + +## Task storage + +- Tasks store `private_data.upstream_kind` as `video` | `image` | `asset` so polling hits the correct upstream endpoint (including **POST** `/v2/asset/status` for assets). diff --git a/docs/token-management-api-reference.md b/docs/token-management-api-reference.md new file mode 100644 index 000000000000..361225f5effe --- /dev/null +++ b/docs/token-management-api-reference.md @@ -0,0 +1,777 @@ +# Token Management API Reference + +> 认证方式:Session Cookie 或 Bearer Token(用户登录后的会话凭证,非 API Key)。 + +--- + +## Overview + +Token 管理接口位于 `/api/token/` 路径下,采用 **用户级认证** (`middleware.UserAuth()`),即用户只能管理自己的 Token,管理员没有独立的管理其他用户 Token 的端点。 + +### 通用响应格式 + +所有接口统一返回 HTTP 200,通过 `success` 字段区分成功/失败: + +```json +// 成功(含 data) +{ + "success": true, + "message": "", + "data": { ... } +} + +// 成功(无 data) +{ + "success": true, + "message": "" +} + +// 失败 +{ + "success": false, + "message": "错误描述文本" +} +``` + +### Token 状态常量 + +| 值 | 常量 | 含义 | +|----|------|------| +| 1 | `TokenStatusEnabled` | 启用 | +| 2 | `TokenStatusDisabled` | 手动禁用 | +| 3 | `TokenStatusExpired` | 已过期 | +| 4 | `TokenStatusExhausted` | 额度耗尽 | + +### Token 数据模型 + +| 字段 | 类型 | JSON key | 说明 | +|------|------|----------|------| +| Id | int | `id` | 主键 | +| UserId | int | `user_id` | 所属用户 ID | +| Key | string | `key` | 48 字符 API 密钥(自动生成,列表/详情中脱敏显示) | +| Status | int | `status` | 状态:1=启用, 2=禁用, 3=过期, 4=耗尽 | +| Name | string | `name` | 显示名称,最长 50 字符 | +| CreatedTime | int64 | `created_time` | 创建时间(Unix 秒) | +| AccessedTime | int64 | `accessed_time` | 最后访问时间(Unix 秒) | +| ExpiredTime | int64 | `expired_time` | 过期时间(Unix 秒),-1 表示永不过期 | +| RemainQuota | int | `remain_quota` | 剩余额度 | +| UnlimitedQuota | bool | `unlimited_quota` | 是否无限额度 | +| ModelLimitsEnabled | bool | `model_limits_enabled` | 是否启用模型限制 | +| ModelLimits | string | `model_limits` | 允许的模型列表(逗号分隔) | +| AllowIps | *string | `allow_ips` | IP 白名单(换行分隔),`null` 或空字符串表示不限 | +| UsedQuota | int | `used_quota` | 已用额度 | +| Group | string | `group` | 分组名称 | +| CrossGroupRetry | bool | `cross_group_retry` | 跨分组重试(仅 `auto` 组有效) | + +### Key 脱敏规则 + +- 长度 ≤ 4:全部替换为 `*` +- 长度 ≤ 8:前2 + `****` + 后2 +- 长度 > 8:前4 + `**********` + 后4 + +--- + +## 1. 获取所有令牌 + +``` +GET /api/token/ +``` + +**认证**:UserAuth + +### 查询参数 + +| 参数 | 位置 | 必选 | 默认值 | 说明 | +|------|------|------|--------|------| +| `p` / `page` | query | 否 | 1 | 页码,最小 1 | +| `page_size` / `ps` / `size` | query | 否 | 10 (`ItemsPerPage`) | 每页条数,最大 100 | + +### 成功响应 + +```json +{ + "success": true, + "message": "", + "data": { + "page": 1, + "page_size": 10, + "total": 42, + "items": [ + { + "id": 1, + "user_id": 1, + "key": "sk-a**********xyz1", + "status": 1, + "name": "我的令牌", + "created_time": 1700000000, + "accessed_time": 1700100000, + "expired_time": -1, + "remain_quota": 500000, + "unlimited_quota": false, + "model_limits_enabled": false, + "model_limits": "", + "allow_ips": "", + "used_quota": 100000, + "group": "default", + "cross_group_retry": false + } + ] + } +} +``` + +### 错误响应 + +```json +{ + "success": false, + "message": "数据库错误描述" +} +``` + +--- + +## 2. 搜索令牌 + +``` +GET /api/token/search +``` + +**认证**:UserAuth + SearchRateLimit(每用户 10 次/分钟) + +### 查询参数 + +| 参数 | 位置 | 必选 | 默认值 | 说明 | +|------|------|------|--------|------| +| `keyword` | query | 否 | `""` | 按名称搜索,支持 `%` 通配符(最多 2 个),不含 `%` 时精确匹配 | +| `token` | query | 否 | `""` | 按 Key 搜索(可带/不带 `sk-` 前缀),同样支持 `%` 通配符 | +| `p` / `page` | query | 否 | 1 | 页码 | +| `page_size` / `ps` / `size` | query | 否 | 10 | 每页条数,最大 100 | + +### 搜索规则 + +- `keyword` 和 `token` 均为空时,等同于获取全部令牌 +- 使用 `%` 通配符(模糊搜索)时,去掉 `%` 后的关键词长度必须 ≥ 2 +- 不允许连续 `%%` +- 超量用户(令牌数超过系统上限)禁止模糊搜索,仅允许精确匹配 +- 硬上限:最多返回 100 条结果 + +### 成功响应 + +格式同 [获取所有令牌](#1-获取所有令牌),分页结构一致。 + +### 错误响应 + +```json +{ + "success": false, + "message": "搜索模式中不允许包含连续的 % 通配符" +} +``` + +```json +{ + "success": false, + "message": "使用模糊搜索时,关键词长度至少为 2 个字符" +} +``` + +```json +{ + "success": false, + "message": "令牌数量超过上限,仅允许精确搜索,请勿使用 % 通配符" +} +``` + +```json +{ + "success": false, + "message": "搜索令牌失败" +} +``` + +--- + +## 3. 获取单个令牌 + +``` +GET /api/token/:id +``` + +**认证**:UserAuth + +### 路径参数 + +| 参数 | 位置 | 必选 | 说明 | +|------|------|------|------| +| `id` | path | 是 | 令牌 ID | + +### 成功响应 + +```json +{ + "success": true, + "message": "", + "data": { + "id": 1, + "user_id": 1, + "key": "sk-a**********xyz1", + "status": 1, + "name": "我的令牌", + "created_time": 1700000000, + "accessed_time": 1700100000, + "expired_time": -1, + "remain_quota": 500000, + "unlimited_quota": false, + "model_limits_enabled": false, + "model_limits": "", + "allow_ips": "", + "used_quota": 100000, + "group": "default", + "cross_group_retry": false + } +} +``` + +### 错误响应 + +```json +{ + "success": false, + "message": "id 或 userId 为空!" +} +``` + +```json +{ + "success": false, + "message": "record not found" +} +``` + +--- + +## 4. 获取令牌完整 Key + +``` +POST /api/token/:id/key +``` + +**认证**:UserAuth + CriticalRateLimit + DisableCache + +**频率限制**:20 次 / 20 分钟 + +### 路径参数 + +| 参数 | 位置 | 必选 | 说明 | +|------|------|------|------| +| `id` | path | 是 | 令牌 ID | + +### 请求体 + +无 + +### 成功响应 + +```json +{ + "success": true, + "message": "", + "data": { + "key": "sk-abcdef12345678901234567890123456789012345678" + } +} +``` + +### 错误响应 + +同 [获取单个令牌](#3-获取单个令牌) 的错误格式。 + +--- + +## 5. 创建令牌 + +``` +POST /api/token/ +``` + +**认证**:UserAuth + +### 请求体 + +Content-Type: `application/json` + +| 字段 | 类型 | 必选 | 默认值 | 说明 | +|------|------|------|--------|------| +| `name` | string | 否 | `""` | 令牌名称,最长 50 字符 | +| `expired_time` | int64 | 否 | -1 | 过期时间(Unix 秒),-1 表示永不过期 | +| `remain_quota` | int | 否 | 0 | 初始剩余额度 | +| `unlimited_quota` | bool | 否 | false | 是否无限额度 | +| `model_limits_enabled` | bool | 否 | false | 是否启用模型限制 | +| `model_limits` | string | 否 | `""` | 允许的模型列表(逗号分隔,如 `gpt-4,claude-3`) | +| `allow_ips` | string/null | 否 | null | IP 白名单(换行分隔),null 或空字符串表示不限 | +| `group` | string | 否 | `""` | 分组名称 | +| `cross_group_retry` | bool | 否 | false | 跨分组重试(仅 `auto` 组有效) | + +### 验证规则 + +1. `name` 长度不得超过 50 字符 +2. 非无限额度时,`remain_quota` 必须 ≥ 0 且 ≤ `1000000000 * QuotaPerUnit` +3. 用户令牌数量不能超过系统上限(`operation_setting.GetMaxUserTokens()`) + +### 请求示例 + +```json +{ + "name": "我的令牌", + "expired_time": 1735689600, + "remain_quota": 500000, + "unlimited_quota": false, + "model_limits_enabled": true, + "model_limits": "gpt-4,claude-3-opus", + "allow_ips": "192.168.1.0/24\n10.0.0.1", + "group": "default", + "cross_group_retry": false +} +``` + +### 成功响应 + +```json +{ + "success": true, + "message": "" +} +``` + +> 创建成功后响应体中不包含令牌 Key。如需获取 Key,需调用 [获取令牌完整 Key](#4-获取令牌完整-key)。 + +### 错误响应 + +```json +{ + "success": false, + "message": "令牌名称过长" +} +``` + +```json +{ + "success": false, + "message": "额度不能为负数" +} +``` + +```json +{ + "success": false, + "message": "额度超出最大值: " +} +``` + +```json +{ + "success": false, + "message": "已达到最大令牌数量限制 (100)" +} +``` + +```json +{ + "success": false, + "message": "令牌生成失败" +} +``` + +--- + +## 6. 更新令牌 + +``` +PUT /api/token/ +``` + +**认证**:UserAuth + +### 查询参数 + +| 参数 | 位置 | 必选 | 默认值 | 说明 | +|------|------|------|--------|------| +| `status_only` | query | 否 | `""`(空) | 非空时仅更新 `status` 字段,忽略其他字段 | + +### 请求体 + +Content-Type: `application/json` + +| 字段 | 类型 | 必选(status_only 为空时) | 默认值 | 说明 | +|------|------|---------------------------|--------|------| +| `id` | int | **是** | — | 要更新的令牌 ID | +| `status` | int | `status_only` 模式下必选 | — | 新状态值 | +| `name` | string | 否 | — | 令牌名称,最长 50 字符 | +| `expired_time` | int64 | 否 | — | 过期时间 | +| `remain_quota` | int | 否 | — | 剩余额度 | +| `unlimited_quota` | bool | 否 | — | 是否无限额度 | +| `model_limits_enabled` | bool | 否 | — | 是否启用模型限制 | +| `model_limits` | string | 否 | — | 允许的模型列表 | +| `allow_ips` | string/null | 否 | — | IP 白名单 | +| `group` | string | 否 | — | 分组名称 | +| `cross_group_retry` | bool | 否 | — | 跨分组重试 | + +### 状态更新限制 + +当尝试将 `status` 设为 1(启用)时,系统会检查原始令牌状态: +- 如果令牌已过期(状态 3)且过期时间已到,**拒绝启用** +- 如果令牌已耗尽(状态 4)且无剩余额度且非无限额度,**拒绝启用** + +### 请求示例 + +```json +// 完整更新 +{ + "id": 1, + "name": "更新后的名称", + "expired_time": -1, + "remain_quota": 1000000, + "unlimited_quota": false, + "model_limits_enabled": false, + "model_limits": "", + "allow_ips": null, + "group": "vip", + "cross_group_retry": true +} +``` + +```json +// 仅更新状态 +// PUT /api/token/?status_only=true +{ + "id": 1, + "status": 2 +} +``` + +### 成功响应 + +```json +{ + "success": true, + "message": "", + "data": { + "id": 1, + "key": "sk-a**********xyz1", + "status": 2, + "name": "更新后的名称", + "created_time": 1700000000, + "accessed_time": 1700100000, + "expired_time": -1, + "remain_quota": 1000000, + "unlimited_quota": false, + "model_limits_enabled": false, + "model_limits": "", + "allow_ips": null, + "used_quota": 100000, + "group": "vip", + "cross_group_retry": true + } +} +``` + +### 错误响应 + +```json +{ + "success": false, + "message": "id 或 userId 为空!" +} +``` + +```json +{ + "success": false, + "message": "record not found" +} +``` + +```json +{ + "success": false, + "message": "令牌已过期,无法启用" +} +``` + +```json +{ + "success": false, + "message": "令牌额度已耗尽,无法启用" +} +``` + +```json +{ + "success": false, + "message": "令牌名称过长" +} +``` + +--- + +## 7. 删除令牌 + +``` +DELETE /api/token/:id +``` + +**认证**:UserAuth + +### 路径参数 + +| 参数 | 位置 | 必选 | 说明 | +|------|------|------|------| +| `id` | path | 是 | 令牌 ID | + +### 请求体 + +无 + +### 成功响应 + +```json +{ + "success": true, + "message": "" +} +``` + +> 删除为软删除(GORM `DeletedAt`),数据保留在数据库中。 + +### 错误响应 + +```json +{ + "success": false, + "message": "record not found" +} +``` + +--- + +## 8. 批量删除令牌 + +``` +POST /api/token/batch +``` + +**认证**:UserAuth + +### 请求体 + +Content-Type: `application/json` + +| 字段 | 类型 | 必选 | 说明 | +|------|------|------|------| +| `ids` | int[] | 是 | 要删除的令牌 ID 数组 | + +### 请求示例 + +```json +{ + "ids": [1, 2, 3, 5] +} +``` + +### 成功响应 + +```json +{ + "success": true, + "message": "", + "data": 4 +} +``` + +> `data` 为实际成功删除的数量(仅统计属于当前用户的令牌)。 + +### 错误响应 + +```json +{ + "success": false, + "message": "参数无效" +} +``` + +> 当 `ids` 为空或 JSON 解析失败时返回。 + +--- + +## 9. 批量获取令牌 Key + +``` +POST /api/token/batch/keys +``` + +**认证**:UserAuth + CriticalRateLimit + DisableCache + +**频率限制**:20 次 / 20 分钟 + +### 请求体 + +Content-Type: `application/json` + +| 字段 | 类型 | 必选 | 说明 | +|------|------|------|------| +| `ids` | int[] | 是 | 令牌 ID 数组,最多 100 个 | + +### 请求示例 + +```json +{ + "ids": [1, 2, 3] +} +``` + +### 成功响应 + +```json +{ + "success": true, + "message": "", + "data": { + "keys": { + "1": "sk-abcdef12345678901234567890123456789012345678", + "2": "sk-ghijklmnopqrstuvwxzy1234567890abcdefghijklmnop", + "3": "sk-qrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz" + } + } +} +``` + +> `keys` 的 key 为令牌 ID(字符串),value 为完整 API Key。 + +### 错误响应 + +```json +{ + "success": false, + "message": "参数无效" +} +``` + +```json +{ + "success": false, + "message": "批量操作数量过多,最大允许: 100" +} +``` + +--- + +## 10. 查询令牌用量 + +``` +GET /api/usage/token/ +``` + +**认证**:TokenAuthReadOnly(通过 Bearer Token 认证,只读模式,不检查额度/过期) + +### 请求头 + +| 头 | 必选 | 说明 | +|----|------|------| +| `Authorization` | 是 | `Bearer sk-` 格式 | + +### 查询参数 + +无 + +### 成功响应 + +```json +{ + "code": true, + "message": "ok", + "data": { + "object": "token_usage", + "name": "我的令牌", + "total_granted": 600000, + "total_used": 100000, + "total_available": 500000, + "unlimited_quota": false, + "model_limits": { + "gpt-4": true, + "claude-3": true + }, + "model_limits_enabled": true, + "expires_at": 1735689600 + } +} +``` + +> 注意:此接口响应格式与标准格式不同,使用 `code` 而非 `success`。 +> `expires_at` 为 0 表示永不过期(原始值 -1 转换为 0)。 + +### 错误响应 + +```json +// HTTP 401 +{ + "success": false, + "message": "No Authorization header" +} +``` + +```json +// HTTP 401 +{ + "success": false, + "message": "Invalid Bearer token" +} +``` + +```json +// HTTP 200 +{ + "success": false, + "message": "获取令牌信息失败" +} +``` + +--- + +## 11. 查询令牌调用日志 + +``` +GET /api/log/token +``` + +**认证**:TokenAuthReadOnly + CORS + CriticalRateLimit + +### 查询参数 + +具体参数取决于 `controller.GetLogByKey` 实现,通常支持分页和时间范围过滤。 + +### 成功响应 + +返回该 Token 的调用日志列表(具体格式取决于日志控制器实现)。 + +--- + +## 附:未注册的端点 + +以下 Controller 函数已定义但 **未在任何 Router 中注册**,属于死代码: + +- `GetTokenStatus` — 返回令牌额度摘要(OpenAI 兼容格式 `credit_summary`),未使用 + +--- + +## 源码参考 + +| 文件 | 说明 | +|------|------| +| `controller/token.go` | 所有 Token 控制器函数 | +| `model/token.go` | Token 数据模型、数据库操作、搜索/验证逻辑 | +| `model/token_cache.go` | Token Redis 缓存层 | +| `router/api-router.go:249-261` | Token 路由注册 | +| `common/gin.go` | `ApiSuccess` / `ApiError` / `ApiErrorI18n` 响应函数 | +| `common/page_info.go` | `PageInfo` 分页结构与 `GetPageQuery` 参数解析 | +| `common/constants.go` | Token 状态常量、频率限制配置 | diff --git a/dto/openai_request.go b/dto/openai_request.go index 8c104ddd242d..9912679a9372 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -3,6 +3,8 @@ package dto import ( "encoding/json" "fmt" + "mime" + "path/filepath" "strings" "github.com/QuantumNous/new-api/common" @@ -386,7 +388,14 @@ func (m *MediaContent) ToFileSource() types.FileSource { if file == nil || file.FileData == "" { return nil } - return types.NewFileSourceFromData(file.FileData, "") + mimeType := "" + if ext := filepath.Ext(file.FileName); ext != "" { + mimeType = mime.TypeByExtension(ext) + } + if mimeType == "" && file.FileName != "" { + mimeType = "application/octet-stream" + } + return types.NewFileSourceFromData(file.FileData, mimeType) case ContentTypeVideoUrl: video := m.GetVideoUrl() if video == nil || video.Url == "" { diff --git a/dto/task.go b/dto/task.go index 4a9a8e2e6d18..6bb94f2d7bf8 100644 --- a/dto/task.go +++ b/dto/task.go @@ -30,26 +30,28 @@ func (t *TaskResponse[T]) IsSuccess() bool { } type TaskDto struct { - ID int64 `json:"id"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - TaskID string `json:"task_id"` - Platform string `json:"platform"` - UserId int `json:"user_id"` - Group string `json:"group"` - ChannelId int `json:"channel_id"` - Quota int `json:"quota"` - Action string `json:"action"` - Status string `json:"status"` - FailReason string `json:"fail_reason"` - ResultURL string `json:"result_url,omitempty"` // 任务结果 URL(视频地址等) - SubmitTime int64 `json:"submit_time"` - StartTime int64 `json:"start_time"` - FinishTime int64 `json:"finish_time"` - Progress string `json:"progress"` - Properties any `json:"properties"` - Username string `json:"username,omitempty"` - Data json.RawMessage `json:"data"` + ID int64 `json:"id"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + TaskID string `json:"task_id"` + Platform string `json:"platform"` + UserId int `json:"user_id"` + Group string `json:"group"` + ChannelId int `json:"channel_id"` + Quota int `json:"quota"` + Action string `json:"action"` + Status string `json:"status"` + FailReason string `json:"fail_reason"` + ResultURL string `json:"result_url,omitempty"` // 任务结果 URL(视频地址等) + // UpstreamKind is set for PingXingShiJie-style async tasks: video | image | asset (routes polling / admin UI). + UpstreamKind string `json:"upstream_kind,omitempty"` + SubmitTime int64 `json:"submit_time"` + StartTime int64 `json:"start_time"` + FinishTime int64 `json:"finish_time"` + Progress string `json:"progress"` + Properties any `json:"properties"` + Username string `json:"username,omitempty"` + Data json.RawMessage `json:"data"` } type FetchReq struct { diff --git a/middleware/distributor.go b/middleware/distributor.go index 2263fae3fae5..f8b2d6558bf1 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -14,6 +14,7 @@ import ( "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/model" + taskpxsj "github.com/QuantumNous/new-api/relay/channel/task/pingxingshijie" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/ratio_setting" @@ -262,6 +263,33 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { if _, ok := c.Get("relay_mode"); !ok { c.Set("relay_mode", relayMode) } + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations/async") { + relayMode := relayconstant.RelayModeUnknown + if c.Request.Method == http.MethodPost { + req, err := getModelFromRequest(c) + if err != nil { + return nil, false, err + } + modelRequest.Model = req.Model + relayMode = relayconstant.RelayModeVideoSubmit + } + c.Set("relay_mode", relayMode) + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations/") && c.Request.Method == http.MethodGet { + c.Set("relay_mode", relayconstant.RelayModeVideoFetchByID) + shouldSelectChannel = false + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/assets/upload") && c.Request.Method == http.MethodPost { + req, err := getModelFromRequest(c) + if err != nil { + return nil, false, err + } + modelRequest.Model = req.Model + if err := taskpxsj.ValidateAssetUploadModel(modelRequest.Model); err != nil { + return nil, false, err + } + c.Set("relay_mode", relayconstant.RelayModeVideoSubmit) + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/assets/") && c.Request.Method == http.MethodGet { + c.Set("relay_mode", relayconstant.RelayModeVideoFetchByID) + shouldSelectChannel = false } else if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models/") || strings.HasPrefix(c.Request.URL.Path, "/v1/models/") { // Gemini API 路径处理: /v1beta/models/gemini-2.0-flash:generateContent relayMode := relayconstant.RelayModeGemini diff --git a/middleware/distributor_test.go b/middleware/distributor_test.go new file mode 100644 index 000000000000..030c741e550a --- /dev/null +++ b/middleware/distributor_test.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + taskpxsj "github.com/QuantumNous/new-api/relay/channel/task/pingxingshijie" + "github.com/gin-gonic/gin" +) + +func newJSONContext(method, path, body string) *gin.Context { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(method, path, strings.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + return c +} + +func TestGetModelRequestAssetUploadRejectsNonAssetModel(t *testing.T) { + c := newJSONContext(http.MethodPost, "/v1/assets/upload", `{"model":"doubao-seedream-4-5-251128"}`) + + _, _, err := getModelRequest(c) + + if err == nil { + t.Fatal("expected non-asset model to be rejected for /v1/assets/upload") + } +} + +func TestGetModelRequestAssetUploadRejectsBlankModel(t *testing.T) { + c := newJSONContext(http.MethodPost, "/v1/assets/upload", `{"image_url":"https://example.com/a.jpg","asset_type":"Image"}`) + + _, _, err := getModelRequest(c) + + if err == nil { + t.Fatalf("expected blank model to be rejected for /v1/assets/upload; required model is %q", taskpxsj.AssetPlaceholderModel) + } +} + +func TestGetModelRequestAssetUploadAllowsAssetModel(t *testing.T) { + c := newJSONContext(http.MethodPost, "/v1/assets/upload", `{"model":"pingxingshijie-asset","image_url":"https://example.com/a.jpg","asset_type":"Image"}`) + + req, shouldSelectChannel, err := getModelRequest(c) + + if err != nil { + t.Fatal(err) + } + if !shouldSelectChannel { + t.Fatal("asset upload should select a channel") + } + if req.Model != taskpxsj.AssetPlaceholderModel { + t.Fatalf("model = %q, want %q", req.Model, taskpxsj.AssetPlaceholderModel) + } +} diff --git a/model/task.go b/model/task.go index 5d00de51339f..b1c80ecaf987 100644 --- a/model/task.go +++ b/model/task.go @@ -4,6 +4,7 @@ import ( "bytes" "database/sql/driver" "encoding/json" + "strings" "time" "github.com/QuantumNous/new-api/common" @@ -99,7 +100,9 @@ func (m Properties) Value() (driver.Value, error) { type TaskPrivateData struct { Key string `json:"key,omitempty"` UpstreamTaskID string `json:"upstream_task_id,omitempty"` // 上游真实 task ID - ResultURL string `json:"result_url,omitempty"` // 任务成功后的结果 URL(视频地址等) + // UpstreamKind is used by PingXingShiJie (channel 58) to route polling: video | image | asset. + UpstreamKind string `json:"upstream_kind,omitempty"` + ResultURL string `json:"result_url,omitempty"` // 任务成功后的结果 URL(视频地址等) // 计费上下文:用于异步退款/差额结算(轮询阶段读取) BillingSource string `json:"billing_source,omitempty"` // "wallet" 或 "subscription" SubscriptionId int `json:"subscription_id,omitempty"` // 订阅 ID,用于订阅退款 @@ -128,11 +131,34 @@ func (t *Task) GetUpstreamTaskID() string { // GetResultURL 获取任务结果 URL(视频地址等) // 新数据存在 PrivateData.ResultURL 中;旧数据回退到 FailReason(历史兼容) +// Async image tasks may have been mis-stored as /v1/videos/.../content when the adaptor returned no URL; +// in that case, recover the real image URL from task.Data (PingXingShiJie / Seedream envelope). func (t *Task) GetResultURL() string { - if t.PrivateData.ResultURL != "" { - return t.PrivateData.ResultURL + u := t.PrivateData.ResultURL + if u == "" { + u = t.FailReason } - return t.FailReason + if u == "" { + return "" + } + if !isVideoProxyContentURL(u, t.TaskID) { + return u + } + extracted := extractFirstImageLikeHTTPURLFromJSON(t.Data) + if extracted == "" { + return u + } + if t.PrivateData.UpstreamKind == "image" { + return extracted + } + // Legacy rows without upstream_kind: only override when payload clearly looks like an image task. + if t.PrivateData.UpstreamKind == "" && looksLikeImageAssetURL(extracted) { + if strings.Contains(strings.ToLower(t.Properties.UpstreamModelName), "seedream") || + strings.Contains(strings.ToLower(t.Properties.OriginModelName), "seedream") { + return extracted + } + } + return u } // GenerateTaskID 生成对外暴露的 task_xxxx 格式 ID @@ -343,6 +369,27 @@ func GetByTaskId(userId int, taskId string) (*Task, bool, error) { return task, exist, err } +func GetByUpstreamTaskId(userId int, upstreamTaskId string) (*Task, bool, error) { + if upstreamTaskId == "" { + return nil, false, nil + } + var task *Task + query := DB.Where("user_id = ?", userId) + if common.UsingPostgreSQL { + query = query.Where("private_data->>'upstream_task_id' = ?", upstreamTaskId) + } else if common.UsingMySQL { + query = query.Where("JSON_UNQUOTE(JSON_EXTRACT(private_data, '$.upstream_task_id')) = ?", upstreamTaskId) + } else { + query = query.Where("json_extract(private_data, '$.upstream_task_id') = ?", upstreamTaskId) + } + err := query.First(&task).Error + exist, err := RecordExist(err) + if err != nil { + return nil, false, err + } + return task, exist, err +} + func GetByTaskIds(userId int, taskIds []any) ([]*Task, error) { if len(taskIds) == 0 { return nil, nil diff --git a/model/task_cas_test.go b/model/task_cas_test.go index ba34a73291bc..30f55c0b51ae 100644 --- a/model/task_cas_test.go +++ b/model/task_cas_test.go @@ -72,6 +72,26 @@ func insertTask(t *testing.T, task *Task) { require.NoError(t, DB.Create(task).Error) } +func TestGetByUpstreamTaskIdFindsAssetByPrivateData(t *testing.T) { + truncateTables(t) + insertTask(t, &Task{ + TaskID: "task_public_asset", + UserId: 7, + Platform: "58", + Status: TaskStatusSubmitted, + PrivateData: TaskPrivateData{ + UpstreamTaskID: "asset-20260319082447-qrrjp", + UpstreamKind: "asset", + }, + }) + + task, exist, err := GetByUpstreamTaskId(7, "asset-20260319082447-qrrjp") + require.NoError(t, err) + require.True(t, exist) + require.NotNil(t, task) + assert.Equal(t, "task_public_asset", task.TaskID) +} + // --------------------------------------------------------------------------- // Snapshot / Equal — pure logic tests (no DB) // --------------------------------------------------------------------------- diff --git a/model/task_result_url.go b/model/task_result_url.go new file mode 100644 index 000000000000..90a4d3133929 --- /dev/null +++ b/model/task_result_url.go @@ -0,0 +1,80 @@ +package model + +import ( + "encoding/json" + "strings" + + "github.com/QuantumNous/new-api/common" +) + +// isVideoProxyContentURL reports whether u is this gateway's /v1/videos/:task_id/content proxy URL. +func isVideoProxyContentURL(u, taskID string) bool { + u = strings.TrimSpace(u) + if u == "" || taskID == "" { + return false + } + return strings.Contains(u, "/v1/videos/") && strings.Contains(u, taskID) && strings.Contains(u, "/content") +} + +// looksLikeImageAssetURL is a light heuristic for HTTP URLs that point to raster images (TOS, CDN, etc.). +func looksLikeImageAssetURL(u string) bool { + lower := strings.ToLower(strings.TrimSpace(u)) + if !strings.HasPrefix(lower, "http") { + return false + } + if strings.Contains(lower, ".jpeg") || strings.Contains(lower, ".jpg") || + strings.Contains(lower, ".png") || strings.Contains(lower, ".webp") || + strings.Contains(lower, ".gif") { + return true + } + // Seedream and similar APIs may omit extension in signed URLs + if strings.Contains(lower, "seedream") || strings.Contains(lower, "image") && strings.Contains(lower, "generation") { + return true + } + return false +} + +func walkFirstImageLikeURL(v any) string { + switch x := v.(type) { + case map[string]any: + if u, ok := x["url"].(string); ok && strings.HasPrefix(u, "http") && looksLikeImageAssetURL(u) { + return u + } + for _, vv := range x { + if s := walkFirstImageLikeURL(vv); s != "" { + return s + } + } + case []any: + for _, item := range x { + if s := walkFirstImageLikeURL(item); s != "" { + return s + } + } + } + return "" +} + +// extractFirstImageLikeHTTPURLFromJSON scans nested task payload JSON for the first image result URL. +func extractFirstImageLikeHTTPURLFromJSON(raw json.RawMessage) string { + if len(raw) == 0 { + return "" + } + var root any + if err := common.Unmarshal(raw, &root); err != nil { + return "" + } + return walkFirstImageLikeURL(root) +} + +// ExtractImageURLFromJSONBytes parses arbitrary JSON bytes (e.g. upstream poll body) for an image URL. +func ExtractImageURLFromJSONBytes(raw []byte) string { + if len(raw) == 0 { + return "" + } + var root any + if err := common.Unmarshal(raw, &root); err != nil { + return "" + } + return walkFirstImageLikeURL(root) +} diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index d2f7c6bb6d5a..8cc9f36d0ca3 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -81,3 +81,13 @@ type TaskAdaptor interface { type OpenAIVideoConverter interface { ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) } + +// OpenAIAsyncImageConverter converts stored PingXingShiJie image tasks to a client-friendly JSON for GET /v1/images/generations/:task_id. +type OpenAIAsyncImageConverter interface { + ConvertToOpenAIAsyncImage(originTask *model.Task) ([]byte, error) +} + +// OpenAIAssetTaskConverter converts stored PingXingShiJie asset tasks for GET /v1/assets/:asset_id. +type OpenAIAssetTaskConverter interface { + ConvertToOpenAIAssetTask(originTask *model.Task) ([]byte, error) +} diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index e177e56dab14..b39ab451b44b 100644 --- a/relay/channel/claude/relay-claude.go +++ b/relay/channel/claude/relay-claude.go @@ -1,6 +1,7 @@ package claude import ( + "encoding/base64" "encoding/json" "fmt" "io" @@ -385,6 +386,22 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe if err != nil { return nil, fmt.Errorf("get file data failed: %s", err.Error()) } + if strings.HasPrefix(mimeType, "text/") { + decodedText, err := base64.StdEncoding.DecodeString(base64Data) + if err != nil { + return nil, fmt.Errorf("decode text file data failed: %s", err.Error()) + } + claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{ + Type: "text", + Text: common.GetPointer[string](string(decodedText)), + }) + continue + } + + if !strings.HasPrefix(mimeType, "application/pdf") && !strings.HasPrefix(mimeType, "image/") { + continue + } + claudeMediaMessage := dto.ClaudeMediaMessage{ Source: &dto.ClaudeMessageSource{ Type: "base64", diff --git a/relay/channel/task/ali/adaptor.go b/relay/channel/task/ali/adaptor.go index 5b6b01d939e6..247677a5323a 100644 --- a/relay/channel/task/ali/adaptor.go +++ b/relay/channel/task/ali/adaptor.go @@ -35,23 +35,32 @@ type AliVideoRequest struct { // AliVideoInput 视频输入参数 type AliVideoInput struct { - Prompt string `json:"prompt,omitempty"` // 文本提示词 - ImgURL string `json:"img_url,omitempty"` // 首帧图像URL或Base64(图生视频) - FirstFrameURL string `json:"first_frame_url,omitempty"` // 首帧图片URL(首尾帧生视频) - LastFrameURL string `json:"last_frame_url,omitempty"` // 尾帧图片URL(首尾帧生视频) - AudioURL string `json:"audio_url,omitempty"` // 音频URL(wan2.5支持) - NegativePrompt string `json:"negative_prompt,omitempty"` // 反向提示词 - Template string `json:"template,omitempty"` // 视频特效模板 + Prompt string `json:"prompt,omitempty"` // 文本提示词 + ImgURL string `json:"img_url,omitempty"` // 首帧图像URL或Base64(图生视频) + FirstFrameURL string `json:"first_frame_url,omitempty"` // 首帧图片URL(首尾帧生视频) + LastFrameURL string `json:"last_frame_url,omitempty"` // 尾帧图片URL(首尾帧生视频) + AudioURL string `json:"audio_url,omitempty"` // 音频URL(wan2.5支持) + NegativePrompt string `json:"negative_prompt,omitempty"` // 反向提示词 + Template string `json:"template,omitempty"` // 视频特效模板 + Media []AliMedia `json:"media,omitempty"` // PixVerse 媒体素材 +} + +// AliMedia PixVerse 媒体素材 +type AliMedia struct { + Type string `json:"type"` // image_url / first_frame / last_frame + URL string `json:"url"` + RefName string `json:"ref_name,omitempty"` // 参考生视频引用名 } // AliVideoParameters 视频参数 type AliVideoParameters struct { - Resolution string `json:"resolution,omitempty"` // 分辨率: 480P/720P/1080P(图生视频、首尾帧生视频) + Resolution string `json:"resolution,omitempty"` // 分辨率: 360P/480P/540P/720P/1080P Size string `json:"size,omitempty"` // 尺寸: 如 "832*480"(文生视频) Duration int `json:"duration,omitempty"` // 时长: 3-10秒 PromptExtend bool `json:"prompt_extend,omitempty"` // 是否开启prompt智能改写 Watermark bool `json:"watermark,omitempty"` // 是否添加水印 - Audio *bool `json:"audio,omitempty"` // 是否添加音频(wan2.5) + Audio *bool `json:"audio,omitempty"` // 是否添加音频 + ShotType string `json:"shot_type,omitempty"` // 镜头类型: single/multi(PixVerse v6) Seed int `json:"seed,omitempty"` // 随机数种子 } @@ -87,20 +96,22 @@ type AliUsage struct { type AliMetadata struct { // Input 相关 - AudioURL string `json:"audio_url,omitempty"` // 音频URL - ImgURL string `json:"img_url,omitempty"` // 图片URL(图生视频) - FirstFrameURL string `json:"first_frame_url,omitempty"` // 首帧图片URL(首尾帧生视频) - LastFrameURL string `json:"last_frame_url,omitempty"` // 尾帧图片URL(首尾帧生视频) - NegativePrompt string `json:"negative_prompt,omitempty"` // 反向提示词 - Template string `json:"template,omitempty"` // 视频特效模板 + AudioURL string `json:"audio_url,omitempty"` // 音频URL + ImgURL string `json:"img_url,omitempty"` // 图片URL(图生视频) + FirstFrameURL string `json:"first_frame_url,omitempty"` // 首帧图片URL(首尾帧生视频) + LastFrameURL string `json:"last_frame_url,omitempty"` // 尾帧图片URL(首尾帧生视频) + NegativePrompt string `json:"negative_prompt,omitempty"` // 反向提示词 + Template string `json:"template,omitempty"` // 视频特效模板 + Media []AliMedia `json:"media,omitempty"` // PixVerse 媒体素材 // Parameters 相关 - Resolution *string `json:"resolution,omitempty"` // 分辨率: 480P/720P/1080P + Resolution *string `json:"resolution,omitempty"` // 分辨率: 360P/480P/540P/720P/1080P Size *string `json:"size,omitempty"` // 尺寸: 如 "832*480" Duration *int `json:"duration,omitempty"` // 时长 PromptExtend *bool `json:"prompt_extend,omitempty"` // 是否开启prompt智能改写 Watermark *bool `json:"watermark,omitempty"` // 是否添加水印 Audio *bool `json:"audio,omitempty"` // 是否添加音频 + ShotType string `json:"shot_type,omitempty"` // 镜头类型: single/multi Seed *int `json:"seed,omitempty"` // 随机数种子 } @@ -179,9 +190,29 @@ var ( } ) +var ( + size360p = []string{ + "640*360", "360*640", "640*480", "480*640", "640*640", + "640*432", "432*640", "640*288", + } + size540p = []string{ + "1024*576", "576*1024", "1024*768", "768*1024", "1024*1024", + "1024*688", "688*1024", "1024*448", + } +) + +// isPixverseModel 判断是否为 PixVerse 系列模型 +func isPixverseModel(model string) bool { + return strings.HasPrefix(model, "pixverse/") +} + func sizeToResolution(size string) (string, error) { - if lo.Contains(size480p, size) { + if lo.Contains(size360p, size) { + return "360P", nil + } else if lo.Contains(size480p, size) { return "480P", nil + } else if lo.Contains(size540p, size) { + return "540P", nil } else if lo.Contains(size720p, size) { return "720P", nil } else if lo.Contains(size1080p, size) { @@ -249,6 +280,50 @@ func ProcessAliOtherRatios(aliReq *AliVideoRequest) (map[string]float64, error) otherRatios[fmt.Sprintf("resolution-%s", resolution)] = ratio } } + + // PixVerse 系列模型计费倍率 + if isPixverseModel(aliReq.Model) { + audioKey := "no-audio" + if aliReq.Parameters.Audio != nil && *aliReq.Parameters.Audio { + audioKey = "audio" + } + ratioKey := fmt.Sprintf("%s-%s", resolution, audioKey) + + if strings.Contains(aliReq.Model, "v6") { + // PixVerse v6 基准:360P 无声 = 1.0 (0.15元/秒) + pixverseV6Ratios := map[string]float64{ + "360P-no-audio": 1.0, + "360P-audio": 0.21 / 0.15, + "480P-no-audio": 1.0, + "540P-no-audio": 0.21 / 0.15, + "540P-audio": 0.27 / 0.15, + "720P-no-audio": 0.27 / 0.15, + "720P-audio": 0.36 / 0.15, + "1080P-no-audio": 0.53 / 0.15, + "1080P-audio": 0.68 / 0.15, + } + if ratio, ok := pixverseV6Ratios[ratioKey]; ok { + otherRatios[fmt.Sprintf("resolution-%s", resolution)] = ratio + } + } else { + // PixVerse v5.6 基准:360P/540P 无声 = 1.0 (0.21元/秒) + pixverseV56Ratios := map[string]float64{ + "360P-no-audio": 1.0, + "360P-audio": 0.47 / 0.21, + "480P-no-audio": 1.0, + "540P-no-audio": 1.0, + "540P-audio": 0.47 / 0.21, + "720P-no-audio": 0.27 / 0.21, + "720P-audio": 0.53 / 0.21, + "1080P-no-audio": 0.44 / 0.21, + "1080P-audio": 0.70 / 0.21, + } + if ratio, ok := pixverseV56Ratios[ratioKey]; ok { + otherRatios[fmt.Sprintf("resolution-%s", resolution)] = ratio + } + } + } + return otherRatios, nil } @@ -257,48 +332,85 @@ func (a *TaskAdaptor) convertToAliRequest(info *relaycommon.RelayInfo, req relay if info.IsModelMapped { upstreamModel = info.UpstreamModelName } + aliReq := &AliVideoRequest{ Model: upstreamModel, Input: AliVideoInput{ Prompt: req.Prompt, - ImgURL: req.InputReference, }, Parameters: &AliVideoParameters{ - PromptExtend: true, // 默认开启智能改写 - Watermark: false, + Watermark: false, }, } - // 处理分辨率映射 + if isPixverseModel(upstreamModel) { + if err := a.buildPixverseRequest(aliReq, req); err != nil { + return nil, err + } + } else { + // wan 系列模型:使用 img_url / first_frame_url + aliReq.Input.ImgURL = req.InputReference + aliReq.Parameters.PromptExtend = true + a.buildWanResolution(aliReq, req) + } + + // 处理时长 + if req.Duration > 0 { + aliReq.Parameters.Duration = req.Duration + } else if req.Seconds != "" { + seconds, err := strconv.Atoi(req.Seconds) + if err != nil { + return nil, errors.Wrap(err, "convert seconds to int failed") + } + aliReq.Parameters.Duration = seconds + } else { + aliReq.Parameters.Duration = 5 // 默认5秒 + } + + // 从 metadata 中提取额外参数(覆盖默认值) + if req.Metadata != nil { + if metadataBytes, err := common.Marshal(req.Metadata); err == nil { + err = common.Unmarshal(metadataBytes, aliReq) + if err != nil { + return nil, errors.Wrap(err, "unmarshal metadata failed") + } + } else { + return nil, errors.Wrap(err, "marshal metadata failed") + } + } + + if aliReq.Model != upstreamModel { + return nil, errors.New("can't change model with metadata") + } + + return aliReq, nil +} + +// buildWanResolution 处理 wan 系列模型的分辨率参数 +func (a *TaskAdaptor) buildWanResolution(aliReq *AliVideoRequest, req relaycommon.TaskSubmitReq) { if req.Size != "" { - // text to video size must be contained * + // wan t2v size must contain * if strings.Contains(req.Model, "t2v") && !strings.Contains(req.Size, "*") { - return nil, fmt.Errorf("invalid size: %s, example: %s", req.Size, "1920*1080") + return // will be caught by validation later } if strings.Contains(req.Size, "*") { aliReq.Parameters.Size = req.Size } else { resolution := strings.ToUpper(req.Size) - // 支持 480p, 720p, 1080p 或 480P, 720P, 1080P if !strings.HasSuffix(resolution, "P") { resolution = resolution + "P" } aliReq.Parameters.Resolution = resolution } } else { - // 根据模型设置默认分辨率 - if strings.Contains(req.Model, "t2v") { // image to video - if strings.HasPrefix(req.Model, "wan2.5") { - aliReq.Parameters.Size = "1920*1080" - } else if strings.HasPrefix(req.Model, "wan2.2") { + if strings.Contains(req.Model, "t2v") { + if strings.HasPrefix(req.Model, "wan2.5") || strings.HasPrefix(req.Model, "wan2.2") { aliReq.Parameters.Size = "1920*1080" } else { aliReq.Parameters.Size = "1280*720" } } else { - if strings.HasPrefix(req.Model, "wan2.6") { - aliReq.Parameters.Resolution = "1080P" - } else if strings.HasPrefix(req.Model, "wan2.5") { + if strings.HasPrefix(req.Model, "wan2.6") || strings.HasPrefix(req.Model, "wan2.5") { aliReq.Parameters.Resolution = "1080P" } else if strings.HasPrefix(req.Model, "wan2.2-i2v-flash") { aliReq.Parameters.Resolution = "720P" @@ -309,38 +421,75 @@ func (a *TaskAdaptor) convertToAliRequest(info *relaycommon.RelayInfo, req relay } } } +} - // 处理时长 - if req.Duration > 0 { - aliReq.Parameters.Duration = req.Duration - } else if req.Seconds != "" { - seconds, err := strconv.Atoi(req.Seconds) - if err != nil { - return nil, errors.Wrap(err, "convert seconds to int failed") - } else { - aliReq.Parameters.Duration = seconds +// buildPixverseRequest 构建 PixVerse 系列模型的请求 +func (a *TaskAdaptor) buildPixverseRequest(aliReq *AliVideoRequest, req relaycommon.TaskSubmitReq) error { + // 确定模型后缀类型 + modelSuffix := "" + for _, suffix := range []string{"-t2v", "-it2v", "-kf2v", "-r2v"} { + if strings.HasSuffix(aliReq.Model, suffix) { + modelSuffix = suffix + break } - } else { - aliReq.Parameters.Duration = 5 // 默认5秒 } - // 从 metadata 中提取额外参数 - if req.Metadata != nil { - if metadataBytes, err := common.Marshal(req.Metadata); err == nil { - err = common.Unmarshal(metadataBytes, aliReq) - if err != nil { - return nil, errors.Wrap(err, "unmarshal metadata failed") + switch modelSuffix { + case "-t2v": + // 文生视频:使用 size(像素值),无 media + a.setPixverseSize(aliReq, req, "1280*720") + case "-it2v": + // 图生视频:使用 resolution(档位),media[{type:"image_url", url:img}] + a.setPixverseResolution(aliReq, req, "720P") + if req.InputReference != "" { + aliReq.Input.Media = []AliMedia{{Type: "image_url", URL: req.InputReference}} + } else if len(req.Images) > 0 { + aliReq.Input.Media = []AliMedia{{Type: "image_url", URL: req.Images[0]}} + } + case "-kf2v": + // 首尾帧生视频:使用 resolution(档位),media[{type:"first_frame"}, {type:"last_frame"}] + a.setPixverseResolution(aliReq, req, "720P") + if len(req.Images) >= 2 { + aliReq.Input.Media = []AliMedia{ + {Type: "first_frame", URL: req.Images[0]}, + {Type: "last_frame", URL: req.Images[1]}, } - } else { - return nil, errors.Wrap(err, "marshal metadata failed") + } + case "-r2v": + // 参考生视频:使用 size(像素值),media[{type:"image_url", url:img, ref_name:...}] + a.setPixverseSize(aliReq, req, "1280*720") + if len(req.Images) > 0 { + media := make([]AliMedia, 0, len(req.Images)) + for _, imgURL := range req.Images { + media = append(media, AliMedia{Type: "image_url", URL: imgURL}) + } + aliReq.Input.Media = media } } - if aliReq.Model != upstreamModel { - return nil, errors.New("can't change model with metadata") + return nil +} + +// setPixverseSize 设置 PixVerse t2v/r2v 的 size 参数(像素值格式) +func (a *TaskAdaptor) setPixverseSize(aliReq *AliVideoRequest, req relaycommon.TaskSubmitReq, defaultSize string) { + if req.Size != "" && strings.Contains(req.Size, "*") { + aliReq.Parameters.Size = req.Size + } else { + aliReq.Parameters.Size = defaultSize } +} - return aliReq, nil +// setPixverseResolution 设置 PixVerse it2v/kf2v 的 resolution 参数(档位格式) +func (a *TaskAdaptor) setPixverseResolution(aliReq *AliVideoRequest, req relaycommon.TaskSubmitReq, defaultResolution string) { + if req.Size != "" && !strings.Contains(req.Size, "*") { + resolution := strings.ToUpper(req.Size) + if !strings.HasSuffix(resolution, "P") { + resolution = resolution + "P" + } + aliReq.Parameters.Resolution = resolution + } else { + aliReq.Parameters.Resolution = defaultResolution + } } // EstimateBilling 根据用户请求参数计算 OtherRatios(时长、分辨率等)。 diff --git a/relay/channel/task/ali/constants.go b/relay/channel/task/ali/constants.go index 8dc64ec597bd..18c572a47930 100644 --- a/relay/channel/task/ali/constants.go +++ b/relay/channel/task/ali/constants.go @@ -1,11 +1,20 @@ package ali var ModelList = []string{ + // 万相系列 "wan2.5-i2v-preview", // 万相2.5 preview(有声视频)推荐 "wan2.2-i2v-flash", // 万相2.2极速版(无声视频) "wan2.2-i2v-plus", // 万相2.2专业版(无声视频) "wanx2.1-i2v-plus", // 万相2.1专业版(无声视频) "wanx2.1-i2v-turbo", // 万相2.1极速版(无声视频) + // PixVerse 系列 + "pixverse/pixverse-v6-t2v", // PixVerse v6 文生视频 + "pixverse/pixverse-v6-it2v", // PixVerse v6 图生视频 + "pixverse/pixverse-v6-kf2v", // PixVerse v6 首尾帧生视频 + "pixverse/pixverse-v5.6-t2v", // PixVerse v5.6 文生视频 + "pixverse/pixverse-v5.6-it2v", // PixVerse v5.6 图生视频 + "pixverse/pixverse-v5.6-kf2v", // PixVerse v5.6 首尾帧生视频 + "pixverse/pixverse-v5.6-r2v", // PixVerse v5.6 参考生视频 } var ChannelName = "ali" diff --git a/relay/channel/task/doubao/adaptor.go b/relay/channel/task/doubao/adaptor.go index a6dabb5f1086..91cd398f92e5 100644 --- a/relay/channel/task/doubao/adaptor.go +++ b/relay/channel/task/doubao/adaptor.go @@ -27,13 +27,19 @@ import ( // Request / Response structures // ============================ +// DraftTaskRef is the nested object for content items with type "draft_task" (Seedance draft-to-video). +type DraftTaskRef struct { + ID string `json:"id"` +} + type ContentItem struct { - Type string `json:"type,omitempty"` - Text string `json:"text,omitempty"` - ImageURL *MediaURL `json:"image_url,omitempty"` - VideoURL *MediaURL `json:"video_url,omitempty"` - AudioURL *MediaURL `json:"audio_url,omitempty"` - Role string `json:"role,omitempty"` + Type string `json:"type,omitempty"` + Text string `json:"text,omitempty"` + ImageURL *MediaURL `json:"image_url,omitempty"` + VideoURL *MediaURL `json:"video_url,omitempty"` + AudioURL *MediaURL `json:"audio_url,omitempty"` + Role string `json:"role,omitempty"` + DraftTask *DraftTaskRef `json:"draft_task,omitempty"` } type MediaURL struct { @@ -294,6 +300,13 @@ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (* r.Duration = lo.ToPtr(dto.IntValue(sec)) } + // Ark API: draft_task items must not be mixed with other content types (e.g. text, image_url). + // convertToRequestPayload normally appends prompt as text; skip that when content uses draft_task. + if contentHasDraftTask(r.Content) { + r.Content = lo.Reject(r.Content, func(c ContentItem, _ int) bool { return c.Type == "text" }) + return &r, nil + } + r.Content = lo.Reject(r.Content, func(c ContentItem, _ int) bool { return c.Type == "text" }) r.Content = append(r.Content, ContentItem{ Type: "text", @@ -303,6 +316,15 @@ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (* return &r, nil } +func contentHasDraftTask(items []ContentItem) bool { + for _, c := range items { + if c.Type == "draft_task" { + return true + } + } + return false +} + func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) { resTask := responseTask{} if err := common.Unmarshal(respBody, &resTask); err != nil { diff --git a/relay/channel/task/kie/adaptor.go b/relay/channel/task/kie/adaptor.go new file mode 100644 index 000000000000..96a6f3b4bb91 --- /dev/null +++ b/relay/channel/task/kie/adaptor.go @@ -0,0 +1,458 @@ +package kie + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +type TaskAdaptor struct { + taskcommon.BaseBilling + apiKey string + baseURL string +} + +type createTaskRequest struct { + Model string `json:"model"` + CallBackURL string `json:"callBackUrl,omitempty"` + Input map[string]any `json:"input"` +} + +type createTaskResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + TaskID string `json:"taskId"` + } `json:"data"` +} + +type recordInfoResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + TaskID string `json:"taskId"` + Model string `json:"model"` + State string `json:"state"` + ResultJSON string `json:"resultJson"` + FailCode string `json:"failCode"` + FailMsg string `json:"failMsg"` + } `json:"data"` +} + +type resultJSONPayload struct { + ResultURLs []string `json:"resultUrls"` + FirstFrameURL []string `json:"firstFrameUrl"` + LastFrameURL []string `json:"lastFrameUrl"` +} + +var imageResolutionRatioWeights = map[string]map[string]float64{ + ModelNanoBanana2: { + "1K": 5, + "2K": 8, + "4K": 12, + }, + ModelGPTImage2TextToImage: { + "1K": 3, + "2K": 5, + "4K": 8, + }, + ModelGPTImage2ImageToImage: { + "1K": 3, + "2K": 5, + "4K": 8, + }, +} + +func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { + a.baseURL = strings.TrimRight(DefaultBaseURL, "/") + if info != nil && strings.TrimSpace(info.ChannelBaseUrl) != "" { + a.baseURL = strings.TrimRight(strings.TrimSpace(info.ChannelBaseUrl), "/") + } + if info != nil { + a.apiKey = info.ApiKey + } +} + +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError { + return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) +} + +func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInfo) map[string]float64 { + if info == nil { + return nil + } + weights, ok := imageResolutionRatioWeights[info.OriginModelName] + if !ok { + return nil + } + req, err := relaycommon.GetTaskRequest(c) + if err != nil { + return nil + } + resolution := normalizeImageResolution(resolveBillingResolution(req)) + if resolution == "" { + resolution = "1K" + } + weight, ok := weights[resolution] + if !ok { + return nil + } + baseWeight := weights["1K"] + if baseWeight == 0 { + return nil + } + return map[string]float64{"resolution": weight / baseWeight} +} + +func (a *TaskAdaptor) BuildRequestURL(_ *relaycommon.RelayInfo) (string, error) { + return a.baseURL + "/api/v1/jobs/createTask", nil +} + +func (a *TaskAdaptor) BuildRequestHeader(_ *gin.Context, req *http.Request, _ *relaycommon.RelayInfo) error { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+a.apiKey) + return nil +} + +func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) { + req, err := relaycommon.GetTaskRequest(c) + if err != nil { + return nil, err + } + body, err := a.convertToRequestPayload(&req, info) + if err != nil { + return nil, err + } + data, err := common.Marshal(body) + if err != nil { + return nil, err + } + return bytes.NewReader(data), nil +} + +func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { + return channel.DoTaskApiRequest(a, c, info, requestBody) +} + +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (string, []byte, *dto.TaskError) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", nil, service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + _ = resp.Body.Close() + + var taskResp createTaskResponse + if err := common.Unmarshal(responseBody, &taskResp); err != nil { + return "", nil, service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", string(responseBody)), "unmarshal_response_body_failed", http.StatusInternalServerError) + } + if taskResp.Code != http.StatusOK { + return "", nil, service.TaskErrorWrapper(fmt.Errorf("kie api error: %s", taskResp.Msg), strconv.Itoa(taskResp.Code), http.StatusBadRequest) + } + if taskResp.Data.TaskID == "" { + return "", nil, service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) + } + + ov := dto.NewOpenAIVideo() + ov.ID = info.PublicTaskID + ov.TaskID = info.PublicTaskID + ov.CreatedAt = time.Now().Unix() + ov.Model = info.OriginModelName + c.JSON(http.StatusOK, ov) + return taskResp.Data.TaskID, responseBody, nil +} + +func (a *TaskAdaptor) FetchTask(baseURL, key string, body map[string]any, proxy string) (*http.Response, error) { + taskID, ok := body["task_id"].(string) + if !ok || taskID == "" { + return nil, fmt.Errorf("invalid task_id") + } + baseURL = strings.TrimRight(baseURL, "/") + if baseURL == "" { + baseURL = DefaultBaseURL + } + uri := fmt.Sprintf("%s/api/v1/jobs/recordInfo?taskId=%s", baseURL, url.QueryEscape(taskID)) + req, err := http.NewRequest(http.MethodGet, uri, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+key) + + client, err := service.GetHttpClientWithProxy(proxy) + if err != nil { + return nil, fmt.Errorf("new proxy http client failed: %w", err) + } + if client == nil { + client = http.DefaultClient + } + return client.Do(req) +} + +func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) { + var res recordInfoResponse + if err := common.Unmarshal(respBody, &res); err != nil { + return nil, errors.Wrap(err, "unmarshal task result failed") + } + if res.Code != http.StatusOK { + return &relaycommon.TaskInfo{Code: res.Code, Status: model.TaskStatusFailure, Progress: taskcommon.ProgressComplete, Reason: res.Msg}, nil + } + + taskResult := &relaycommon.TaskInfo{Code: 0, TaskID: res.Data.TaskID} + switch strings.ToLower(strings.TrimSpace(res.Data.State)) { + case "waiting": + taskResult.Status = model.TaskStatusSubmitted + taskResult.Progress = taskcommon.ProgressSubmitted + case "queuing": + taskResult.Status = model.TaskStatusQueued + taskResult.Progress = taskcommon.ProgressQueued + case "generating": + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = taskcommon.ProgressInProgress + case "success": + taskResult.Status = model.TaskStatusSuccess + taskResult.Progress = taskcommon.ProgressComplete + taskResult.Url = firstResultURL(res.Data.ResultJSON) + case "fail": + taskResult.Status = model.TaskStatusFailure + taskResult.Progress = taskcommon.ProgressComplete + taskResult.Reason = strings.TrimSpace(res.Data.FailMsg) + if taskResult.Reason == "" { + taskResult.Reason = strings.TrimSpace(res.Data.FailCode) + } + if taskResult.Reason == "" { + taskResult.Reason = "task failed" + } + default: + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = taskcommon.ProgressInProgress + } + return taskResult, nil +} + +func (a *TaskAdaptor) GetModelList() []string { + return ModelList +} + +func (a *TaskAdaptor) GetChannelName() string { + return ChannelName +} + +func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) { + openAIVideo := originTask.ToOpenAIVideo() + if originTask.FailReason != "" && originTask.Status == model.TaskStatusFailure { + openAIVideo.Error = &dto.OpenAIVideoError{Message: originTask.FailReason} + } + return common.Marshal(openAIVideo) +} + +func (a *TaskAdaptor) ConvertToOpenAIAsyncImage(originTask *model.Task) ([]byte, error) { + out := map[string]any{ + "object": "kie.image.generation.task", + "id": originTask.TaskID, + "task_id": originTask.TaskID, + "status": originTask.Status.ToVideoStatus(), + "progress": originTask.Progress, + "model": originTask.Properties.OriginModelName, + "created_at": originTask.CreatedAt, + "updated_at": originTask.UpdatedAt, + } + if u := originTask.GetResultURL(); u != "" { + out["url"] = u + } + if originTask.FailReason != "" { + out["error"] = map[string]any{"message": originTask.FailReason} + } + return common.Marshal(out) +} + +func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq, info *relaycommon.RelayInfo) (*createTaskRequest, error) { + modelName := resolveModelName(req, info) + if modelName == "" { + return nil, fmt.Errorf("model is required") + } + + input := map[string]any{} + if req.Prompt != "" { + input["prompt"] = req.Prompt + } + if req.Size != "" { + applySize(input, req.Size) + } + if req.Resolution != "" { + input["resolution"] = req.Resolution + } + if req.Duration > 0 { + input["duration"] = float64(req.Duration) + } else if sec, _ := strconv.Atoi(req.Seconds); sec > 0 { + input["duration"] = float64(sec) + } + + if err := taskcommon.UnmarshalMetadata(req.Metadata, &input); err != nil { + return nil, err + } + delete(input, "model") + + cfg := getModelConfig(modelName) + images := requestImages(req) + if len(images) > 0 { + switch modelName { + case ModelSeedance2: + input["first_frame_url"] = images[0] + case ModelHappyHorseImageToVideo: + input[cfg.ImageKey] = images[:1] + default: + if cfg.ImageKey != "" { + input[cfg.ImageKey] = images + } + } + } + + return &createTaskRequest{Model: modelName, Input: input}, nil +} + +func resolveModelName(req *relaycommon.TaskSubmitReq, info *relaycommon.RelayInfo) string { + modelName := strings.TrimSpace(req.Model) + if info != nil && strings.TrimSpace(info.UpstreamModelName) != "" { + modelName = strings.TrimSpace(info.UpstreamModelName) + } + if shouldUseDefaultModel(modelName, info) { + path := "" + if info != nil { + path = info.RequestURLPath + } + return DefaultModelForRequest(path, req.HasImage()) + } + return modelName +} + +func shouldUseDefaultModel(modelName string, info *relaycommon.RelayInfo) bool { + modelName = strings.TrimSpace(modelName) + if modelName == "" || modelName == "dall-e" { + return true + } + if info == nil { + return false + } + action := "" + if info.TaskRelayInfo != nil { + action = info.Action + } + fallback := service.CoverTaskActionToModelName(constant.TaskPlatform(strconv.Itoa(constant.ChannelTypeKieAI)), action) + if modelName == fallback { + return true + } + return action != "" && modelName == ChannelName+"_"+strings.ToLower(action) +} + +func requestImages(req *relaycommon.TaskSubmitReq) []string { + images := append([]string(nil), req.Images...) + if req.Image != "" { + images = append([]string{req.Image}, images...) + } + return images +} + +func resolveBillingResolution(req relaycommon.TaskSubmitReq) string { + input := map[string]any{} + if req.Size != "" { + applySize(input, req.Size) + } + if req.Resolution != "" { + input["resolution"] = req.Resolution + } + if err := taskcommon.UnmarshalMetadata(req.Metadata, &input); err != nil { + return "" + } + resolution, _ := input["resolution"].(string) + return resolution +} + +func normalizeImageResolution(resolution string) string { + resolution = strings.ToUpper(strings.TrimSpace(resolution)) + switch resolution { + case "1K", "2K", "4K": + return resolution + default: + return "" + } +} + +func applySize(input map[string]any, size string) { + size = strings.TrimSpace(size) + if strings.Contains(size, "x") { + parts := strings.Split(size, "x") + if len(parts) == 2 { + w, wErr := strconv.Atoi(parts[0]) + h, hErr := strconv.Atoi(parts[1]) + if wErr == nil && hErr == nil && w > 0 && h > 0 { + input["aspect_ratio"] = simplifyRatio(w, h) + input["resolution"] = resolutionFromDimensions(w, h) + return + } + } + } + if strings.HasSuffix(strings.ToLower(size), "p") || strings.HasSuffix(strings.ToUpper(size), "K") { + input["resolution"] = size + } +} + +func simplifyRatio(w, h int) string { + g := gcd(w, h) + return fmt.Sprintf("%d:%d", w/g, h/g) +} + +func resolutionFromDimensions(w, h int) string { + shorter := min(w, h) + if shorter >= 1080 { + return "1080p" + } + if shorter >= 720 { + return "720p" + } + return "480p" +} + +func gcd(a, b int) int { + for b != 0 { + a, b = b, a%b + } + if a < 0 { + return -a + } + return a +} + +func firstResultURL(raw string) string { + if strings.TrimSpace(raw) == "" { + return "" + } + var payload resultJSONPayload + if err := common.UnmarshalJsonStr(raw, &payload); err != nil { + return "" + } + for _, urls := range [][]string{payload.ResultURLs, payload.FirstFrameURL, payload.LastFrameURL} { + for _, u := range urls { + if strings.TrimSpace(u) != "" { + return u + } + } + } + return "" +} diff --git a/relay/channel/task/kie/adaptor_test.go b/relay/channel/task/kie/adaptor_test.go new file mode 100644 index 000000000000..f9007238f70a --- /dev/null +++ b/relay/channel/task/kie/adaptor_test.go @@ -0,0 +1,338 @@ +package kie + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/gin-gonic/gin" +) + +func TestBuildRequestURLUsesConfiguredBaseURL(t *testing.T) { + a := &TaskAdaptor{} + a.Init(&relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ChannelBaseUrl: "https://example.kie.ai/", ApiKey: "test-key"}}) + + got, err := a.BuildRequestURL(&relaycommon.RelayInfo{}) + if err != nil { + t.Fatal(err) + } + + if got != "https://example.kie.ai/api/v1/jobs/createTask" { + t.Fatalf("BuildRequestURL = %q", got) + } +} + +func TestConvertSeedance2RequestPayloadFromUnifiedRequest(t *testing.T) { + a := &TaskAdaptor{} + req := relaycommon.TaskSubmitReq{ + Model: ModelSeedance2, + Prompt: "make a video", + Image: "https://example.com/first.png", + Size: "1280x720", + Duration: 6, + Metadata: map[string]any{ + "last_frame_url": "https://example.com/last.png", + "generate_audio": false, + "web_search": true, + }, + } + + body, err := a.convertToRequestPayload(&req, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: ModelSeedance2}}) + if err != nil { + t.Fatal(err) + } + + if body.Model != ModelSeedance2 { + t.Fatalf("model = %q", body.Model) + } + assertInput(t, body.Input, "prompt", "make a video") + assertInput(t, body.Input, "first_frame_url", "https://example.com/first.png") + assertInput(t, body.Input, "last_frame_url", "https://example.com/last.png") + assertInput(t, body.Input, "resolution", "720p") + assertInput(t, body.Input, "aspect_ratio", "16:9") + assertInput(t, body.Input, "duration", float64(6)) + assertInput(t, body.Input, "generate_audio", false) + assertInput(t, body.Input, "web_search", true) +} + +func TestConvertImageModelPayloadsFromUnifiedImages(t *testing.T) { + a := &TaskAdaptor{} + cases := []struct { + name string + modelName string + wantKey string + }{ + {name: "gpt image 2 image-to-image", modelName: ModelGPTImage2ImageToImage, wantKey: "input_urls"}, + {name: "nano banana 2", modelName: ModelNanoBanana2, wantKey: "image_input"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := relaycommon.TaskSubmitReq{ + Model: tc.modelName, + Prompt: "make an image", + Images: []string{"https://example.com/a.png", "https://example.com/b.png"}, + Size: "1024x1024", + Metadata: map[string]any{ + "resolution": "2K", + }, + } + + body, err := a.convertToRequestPayload(&req, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: tc.modelName}}) + if err != nil { + t.Fatal(err) + } + + assertInput(t, body.Input, "prompt", "make an image") + assertInput(t, body.Input, "aspect_ratio", "1:1") + assertInput(t, body.Input, "resolution", "2K") + got, ok := body.Input[tc.wantKey].([]string) + if !ok { + t.Fatalf("%s has type %T", tc.wantKey, body.Input[tc.wantKey]) + } + if len(got) != 2 || got[0] != "https://example.com/a.png" || got[1] != "https://example.com/b.png" { + t.Fatalf("%s = %#v", tc.wantKey, got) + } + }) + } +} + +func TestConvertImagePayloadIncludesTopLevelResolution(t *testing.T) { + var req relaycommon.TaskSubmitReq + if err := common.Unmarshal([]byte(`{ + "model":"gpt-image-2-text-to-image", + "prompt":"make an image", + "aspect_ratio":"1:1", + "resolution":"4K" + }`), &req); err != nil { + t.Fatal(err) + } + + a := &TaskAdaptor{} + body, err := a.convertToRequestPayload(&req, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: ModelGPTImage2TextToImage}}) + if err != nil { + t.Fatal(err) + } + + assertInput(t, body.Input, "aspect_ratio", "1:1") + assertInput(t, body.Input, "resolution", "4K") +} + +func TestResolveDefaultModelsForGenericFallbacks(t *testing.T) { + a := &TaskAdaptor{} + + imageBody, err := a.convertToRequestPayload(&relaycommon.TaskSubmitReq{ + Prompt: "make an image", + }, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "dall-e"}, TaskRelayInfo: &relaycommon.TaskRelayInfo{}, RequestURLPath: "/v1/images/generations/async"}) + if err != nil { + t.Fatal(err) + } + if imageBody.Model != ModelSeedream45TextToImage { + t.Fatalf("image default model = %q", imageBody.Model) + } + + imageEditBody, err := a.convertToRequestPayload(&relaycommon.TaskSubmitReq{ + Prompt: "edit image", + Image: "https://example.com/input.png", + }, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "dall-e"}, TaskRelayInfo: &relaycommon.TaskRelayInfo{}, RequestURLPath: "/v1/images/generations/async"}) + if err != nil { + t.Fatal(err) + } + if imageEditBody.Model != ModelSeedream45ImageToImage { + t.Fatalf("image edit default model = %q", imageEditBody.Model) + } + + videoBody, err := a.convertToRequestPayload(&relaycommon.TaskSubmitReq{ + Prompt: "make a video", + }, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "59_generate"}, TaskRelayInfo: &relaycommon.TaskRelayInfo{Action: "generate"}, RequestURLPath: "/v1/videos/generations"}) + if err != nil { + t.Fatal(err) + } + if videoBody.Model != ModelSeedance2 { + t.Fatalf("video default model = %q", videoBody.Model) + } +} + +func TestDoResponseStoresUpstreamTaskIDAndReturnsPublicTask(t *testing.T) { + gin.SetMode(gin.TestMode) + a := &TaskAdaptor{} + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + resp := &http.Response{ + Body: io.NopCloser(bytes.NewBufferString(`{"code":200,"msg":"success","data":{"taskId":"kie_task_123"}}`)), + } + + upstreamTaskID, rawBody, taskErr := a.DoResponse(c, resp, &relaycommon.RelayInfo{ + OriginModelName: ModelSeedance2, + TaskRelayInfo: &relaycommon.TaskRelayInfo{PublicTaskID: "task_public_123"}, + }) + if taskErr != nil { + t.Fatalf("DoResponse error = %+v", taskErr) + } + if upstreamTaskID != "kie_task_123" { + t.Fatalf("upstreamTaskID = %q", upstreamTaskID) + } + if len(rawBody) == 0 { + t.Fatal("expected raw response body") + } + + var got map[string]any + if err := common.Unmarshal(recorder.Body.Bytes(), &got); err != nil { + t.Fatal(err) + } + if got["id"] != "task_public_123" || got["task_id"] != "task_public_123" { + t.Fatalf("public task response = %s", recorder.Body.String()) + } +} + +func TestFetchTaskUsesRecordInfoEndpointAndBearerToken(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/jobs/recordInfo" { + t.Fatalf("path = %q", r.URL.Path) + } + if r.URL.Query().Get("taskId") != "task/id with space" { + t.Fatalf("taskId query = %q", r.URL.Query().Get("taskId")) + } + if r.Header.Get("Authorization") != "Bearer test-key" { + t.Fatalf("Authorization = %q", r.Header.Get("Authorization")) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":200,"msg":"success","data":{"state":"generating"}}`)) + })) + defer server.Close() + + a := &TaskAdaptor{} + resp, err := a.FetchTask(server.URL, "test-key", map[string]any{"task_id": "task/id with space"}, "") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + b, _ := io.ReadAll(resp.Body) + t.Fatalf("status = %d body = %s", resp.StatusCode, string(b)) + } +} + +func TestParseTaskResultMapsKieStatesAndResultURL(t *testing.T) { + a := &TaskAdaptor{} + info, err := a.ParseTaskResult([]byte(`{"code":200,"msg":"success","data":{"state":"success","resultJson":"{\"resultUrls\":[\"https://example.com/out.mp4\"]}"}}`)) + if err != nil { + t.Fatal(err) + } + + if info.Status != model.TaskStatusSuccess { + t.Fatalf("status = %q", info.Status) + } + if info.Progress != "100%" { + t.Fatalf("progress = %q", info.Progress) + } + if info.Url != "https://example.com/out.mp4" { + t.Fatalf("url = %q", info.Url) + } + + failed, err := a.ParseTaskResult([]byte(`{"code":200,"msg":"success","data":{"state":"fail","failMsg":"bad prompt"}}`)) + if err != nil { + t.Fatal(err) + } + if failed.Status != model.TaskStatusFailure || failed.Reason != "bad prompt" { + t.Fatalf("failed result = %+v", failed) + } +} + +func TestConvertToOpenAIAsyncImageUsesStoredResultURL(t *testing.T) { + a := &TaskAdaptor{} + task := &model.Task{ + TaskID: "task_public", + Status: model.TaskStatusSuccess, + Progress: "100%", + CreatedAt: 123, + UpdatedAt: 456, + Properties: model.Properties{ + OriginModelName: ModelNanoBanana2, + }, + PrivateData: model.TaskPrivateData{ResultURL: "https://example.com/image.png"}, + } + + data, err := a.ConvertToOpenAIAsyncImage(task) + if err != nil { + t.Fatal(err) + } + + var got map[string]any + if err := common.Unmarshal(data, &got); err != nil { + t.Fatal(err) + } + if got["url"] != "https://example.com/image.png" { + t.Fatalf("url = %#v", got["url"]) + } + if got["status"] != "completed" { + t.Fatalf("status = %#v", got["status"]) + } +} + +func TestEstimateBillingAppliesImageResolutionRatios(t *testing.T) { + gin.SetMode(gin.TestMode) + cases := []struct { + name string + modelName string + resolution string + wantRatio float64 + }{ + {name: "nano banana 1K uses configured base price", modelName: ModelNanoBanana2, resolution: "1K", wantRatio: 1}, + {name: "nano banana defaults to 1K base price", modelName: ModelNanoBanana2, resolution: "", wantRatio: 1}, + {name: "nano banana normalizes resolution", modelName: ModelNanoBanana2, resolution: " 2k ", wantRatio: 8.0 / 5.0}, + {name: "nano banana 2K scales from 1K base price", modelName: ModelNanoBanana2, resolution: "2K", wantRatio: 8.0 / 5.0}, + {name: "nano banana 4K scales from 1K base price", modelName: ModelNanoBanana2, resolution: "4K", wantRatio: 12.0 / 5.0}, + {name: "gpt image 2 text 2K scales from 1K base price", modelName: ModelGPTImage2TextToImage, resolution: "2K", wantRatio: 5.0 / 3.0}, + {name: "gpt image 2 image 4K scales from 1K base price", modelName: ModelGPTImage2ImageToImage, resolution: "4K", wantRatio: 8.0 / 3.0}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + a := &TaskAdaptor{} + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + req := relaycommon.TaskSubmitReq{Metadata: map[string]any{"resolution": tc.resolution}} + c.Set("task_request", req) + + ratios := a.EstimateBilling(c, &relaycommon.RelayInfo{OriginModelName: tc.modelName}) + got, ok := ratios["resolution"] + if !ok { + t.Fatalf("missing resolution ratio in %#v", ratios) + } + if got != tc.wantRatio { + t.Fatalf("resolution ratio = %v, want %v", got, tc.wantRatio) + } + }) + } +} + +func TestEstimateBillingIgnoresUnsupportedKieResolutionPricing(t *testing.T) { + gin.SetMode(gin.TestMode) + a := &TaskAdaptor{} + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Set("task_request", relaycommon.TaskSubmitReq{Metadata: map[string]any{"resolution": "4K"}}) + + ratios := a.EstimateBilling(c, &relaycommon.RelayInfo{OriginModelName: ModelSeedream45TextToImage}) + if len(ratios) != 0 { + t.Fatalf("ratios = %#v, want none", ratios) + } +} + +func assertInput(t *testing.T, input map[string]any, key string, want any) { + t.Helper() + got, ok := input[key] + if !ok { + t.Fatalf("missing input[%q] in %#v", key, input) + } + if got != want { + t.Fatalf("input[%q] = %#v, want %#v", key, got, want) + } +} diff --git a/relay/channel/task/kie/constants.go b/relay/channel/task/kie/constants.go new file mode 100644 index 000000000000..620df57356a5 --- /dev/null +++ b/relay/channel/task/kie/constants.go @@ -0,0 +1,69 @@ +package kie + +import "strings" + +const ( + ChannelName = "kie-ai" + DefaultBaseURL = "https://api.kie.ai" + + ModelSeedance2 = "bytedance/seedance-2" + ModelSeedream45TextToImage = "seedream-4.5-text-to-image" + ModelSeedream45ImageToImage = "seedream-4.5-image-to-image" + ModelGPTImage2TextToImage = "gpt-image-2-text-to-image" + ModelGPTImage2ImageToImage = "gpt-image-2-image-to-image" + ModelNanoBanana2 = "nano-banana-2" + ModelHappyHorseTextToVideo = "happyhorse/text-to-video" + ModelHappyHorseImageToVideo = "happyhorse/image-to-video" + + DefaultImageModel = ModelSeedream45TextToImage + DefaultVideoModel = ModelSeedance2 +) + +var ModelList = []string{ + ModelSeedance2, + ModelSeedream45TextToImage, + ModelSeedream45ImageToImage, + ModelGPTImage2TextToImage, + ModelGPTImage2ImageToImage, + ModelNanoBanana2, + ModelHappyHorseTextToVideo, + ModelHappyHorseImageToVideo, +} + +const ( + outputKindImage = "image" + outputKindVideo = "video" +) + +type modelConfig struct { + OutputKind string + ImageKey string +} + +var modelConfigs = map[string]modelConfig{ + ModelSeedance2: {OutputKind: outputKindVideo}, + ModelSeedream45TextToImage: {OutputKind: outputKindImage}, + ModelSeedream45ImageToImage: {OutputKind: outputKindImage, ImageKey: "input_urls"}, + ModelGPTImage2TextToImage: {OutputKind: outputKindImage}, + ModelGPTImage2ImageToImage: {OutputKind: outputKindImage, ImageKey: "input_urls"}, + ModelNanoBanana2: {OutputKind: outputKindImage, ImageKey: "image_input"}, + ModelHappyHorseTextToVideo: {OutputKind: outputKindVideo}, + ModelHappyHorseImageToVideo: {OutputKind: outputKindVideo, ImageKey: "image_urls"}, +} + +func getModelConfig(modelName string) modelConfig { + if cfg, ok := modelConfigs[modelName]; ok { + return cfg + } + return modelConfig{OutputKind: outputKindVideo} +} + +func DefaultModelForRequest(path string, hasImage bool) string { + if strings.HasPrefix(path, "/v1/images/") { + if hasImage { + return ModelSeedream45ImageToImage + } + return DefaultImageModel + } + return DefaultVideoModel +} diff --git a/relay/channel/task/pingxingshijie/adaptor.go b/relay/channel/task/pingxingshijie/adaptor.go new file mode 100644 index 000000000000..ed61c5b5750a --- /dev/null +++ b/relay/channel/task/pingxingshijie/adaptor.go @@ -0,0 +1,992 @@ +package pingxingshijie + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/QuantumNous/new-api/common" + + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "github.com/samber/lo" +) + +// ============================ +// Request / Response structures (video — Ark-compatible body forwarded to /v2/video/generations) +// ============================ + +// DraftTaskRef is the nested object for content items with type "draft_task" (Seedance draft-to-video). +type DraftTaskRef struct { + ID string `json:"id"` +} + +type ContentItem struct { + Type string `json:"type,omitempty"` + Text string `json:"text,omitempty"` + ImageURL *MediaURL `json:"image_url,omitempty"` + VideoURL *MediaURL `json:"video_url,omitempty"` + AudioURL *MediaURL `json:"audio_url,omitempty"` + Role string `json:"role,omitempty"` + DraftTask *DraftTaskRef `json:"draft_task,omitempty"` +} + +type MediaURL struct { + URL string `json:"url,omitempty"` +} + +type requestPayload struct { + Model string `json:"model"` + Content []ContentItem `json:"content,omitempty"` + CallbackURL string `json:"callback_url,omitempty"` + ReturnLastFrame *dto.BoolValue `json:"return_last_frame,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + ExecutionExpiresAfter *dto.IntValue `json:"execution_expires_after,omitempty"` + GenerateAudio *dto.BoolValue `json:"generate_audio,omitempty"` + Draft *dto.BoolValue `json:"draft,omitempty"` + Tools []struct { + Type string `json:"type,omitempty"` + } `json:"tools,omitempty"` + Resolution string `json:"resolution,omitempty"` + Ratio string `json:"ratio,omitempty"` + Duration *dto.IntValue `json:"duration,omitempty"` + Frames *dto.IntValue `json:"frames,omitempty"` + Seed *dto.IntValue `json:"seed,omitempty"` + CameraFixed *dto.BoolValue `json:"camera_fixed,omitempty"` + Watermark *dto.BoolValue `json:"watermark,omitempty"` +} + +type responsePayload struct { + ID string `json:"id"` +} + +type responseTask struct { + ID string `json:"id"` + Model string `json:"model"` + Status string `json:"status"` + Content struct { + VideoURL string `json:"video_url"` + } `json:"content"` + Seed int `json:"seed"` + Resolution string `json:"resolution"` + Duration int `json:"duration"` + Ratio string `json:"ratio"` + FramesPerSecond int `json:"framespersecond"` + ServiceTier string `json:"service_tier"` + Tools []struct { + Type string `json:"type"` + } `json:"tools"` + Usage struct { + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + ToolUsage struct { + WebSearch int `json:"web_search"` + } `json:"tool_usage"` + } `json:"usage"` + Error struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// imageResponseTask mirrors upstream image task polling (fields may vary; use loose parsing where needed). +// Seedream-style responses use status "done" and put URLs in data[].url instead of content.image_url. +type imageResponseTask struct { + ID string `json:"id"` + Model string `json:"model"` + Status string `json:"status"` + Content struct { + ImageURL string `json:"image_url"` + } `json:"content"` + // Data holds generated image entries (PingXingShiJie / Volc Seedream shape). + Data []struct { + Size string `json:"size"` + URL string `json:"url"` + } `json:"data"` + Error struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +func (img *imageResponseTask) resultImageURL() string { + if img == nil { + return "" + } + if img.Content.ImageURL != "" { + return img.Content.ImageURL + } + for _, d := range img.Data { + if strings.TrimSpace(d.URL) != "" { + return d.URL + } + } + return "" +} + +// ============================ +// Adaptor implementation +// ============================ + +type TaskAdaptor struct { + taskcommon.BaseBilling + ChannelType int + apiKey string + baseURL string +} + +func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { + a.ChannelType = info.ChannelType + a.baseURL = strings.TrimRight(info.ChannelBaseUrl, "/") + a.apiKey = info.ApiKey +} + +func requestPathFromRelay(info *relaycommon.RelayInfo) string { + if info == nil || info.RequestURLPath == "" { + return "" + } + u, err := url.Parse(info.RequestURLPath) + if err != nil || u.Path == "" { + return info.RequestURLPath + } + return u.Path +} + +// ValidateRequestAndSetAction parses body, validates fields and sets default action. +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { + path := requestPathFromRelay(info) + if path == "" { + path = c.Request.URL.Path + } + switch UpstreamKindFromPath(path) { + case UpstreamKindAsset: + var req relaycommon.TaskSubmitReq + if err := common.UnmarshalBodyReusable(c, &req); err != nil { + return service.TaskErrorWrapperLocal(err, "invalid_request", http.StatusBadRequest) + } + if err := ValidateAssetUploadModel(req.Model); err != nil { + return service.TaskErrorWrapperLocal(err, "invalid_request", http.StatusBadRequest) + } + if strings.TrimSpace(req.Prompt) == "" { + req.Prompt = "asset-upload" + } + info.Action = constant.TaskActionAssetUpload + c.Set("task_request", req) + return nil + case UpstreamKindImage: + return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) + default: + return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) + } +} + +// BuildRequestURL constructs the upstream URL. +func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) { + path := requestPathFromRelay(info) + switch UpstreamKindFromPath(path) { + case UpstreamKindAsset: + return fmt.Sprintf("%s/v2/asset/upload", a.baseURL), nil + case UpstreamKindImage: + return fmt.Sprintf("%s/v2/image/generations", a.baseURL), nil + default: + return fmt.Sprintf("%s/v2/video/generations", a.baseURL), nil + } +} + +// BuildRequestHeader sets required headers. +func (a *TaskAdaptor) BuildRequestHeader(_ *gin.Context, req *http.Request, _ *relaycommon.RelayInfo) error { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+a.apiKey) + return nil +} + +// EstimateBilling detects Seedance 2.0 resolution pricing and video-input discounts. +func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInfo) map[string]float64 { + path := requestPathFromRelay(info) + if UpstreamKindFromPath(path) != UpstreamKindVideo { + return nil + } + req, err := relaycommon.GetTaskRequest(c) + if err != nil { + return nil + } + ratioMap := make(map[string]float64) + resolution := requestResolution(req) + if ratio, ok := GetResolutionRatio(info.OriginModelName, resolution); ok { + ratioMap["resolution"] = ratio + } + if hasVideoInMetadata(req.Metadata) { + if ratio, ok := GetVideoInputRatioForResolution(info.OriginModelName, resolution); ok { + ratioMap["video_input"] = ratio + } + } + if len(ratioMap) == 0 { + return nil + } + return ratioMap +} + +func requestResolution(req relaycommon.TaskSubmitReq) string { + if req.Resolution != "" { + return strings.ToLower(strings.TrimSpace(req.Resolution)) + } + if req.Metadata == nil { + return "" + } + resolution, ok := req.Metadata["resolution"].(string) + if !ok { + return "" + } + return strings.ToLower(strings.TrimSpace(resolution)) +} + +func hasVideoInMetadata(metadata map[string]interface{}) bool { + if metadata == nil { + return false + } + contentRaw, ok := metadata["content"] + if !ok { + return false + } + contentSlice, ok := contentRaw.([]interface{}) + if !ok { + return false + } + for _, item := range contentSlice { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + if itemMap["type"] == "video_url" { + return true + } + if _, has := itemMap["video_url"]; has { + return true + } + } + return false +} + +// BuildRequestBody converts request into upstream JSON. +func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) { + path := requestPathFromRelay(info) + kind := UpstreamKindFromPath(path) + if kind == UpstreamKindAsset || kind == UpstreamKindImage { + storage, err := common.GetBodyStorage(c) + if err != nil { + return nil, err + } + raw, err := storage.Bytes() + if err != nil { + return nil, err + } + if kind == UpstreamKindAsset { + var m map[string]any + if err := common.Unmarshal(raw, &m); err != nil { + return nil, errors.Wrap(err, "unmarshal asset request") + } + delete(m, "model") + raw, err = common.Marshal(m) + if err != nil { + return nil, err + } + } + if kind == UpstreamKindImage { + var m map[string]any + if err := common.Unmarshal(raw, &m); err != nil { + return nil, errors.Wrap(err, "unmarshal image request") + } + m["model"] = info.UpstreamModelName + raw, err = common.Marshal(m) + if err != nil { + return nil, err + } + } + return bytes.NewReader(raw), nil + } + + req, err := relaycommon.GetTaskRequest(c) + if err != nil { + return nil, err + } + + body, err := a.convertToRequestPayload(&req) + if err != nil { + return nil, errors.Wrap(err, "convert request payload failed") + } + if info.IsModelMapped { + body.Model = info.UpstreamModelName + } else { + info.UpstreamModelName = body.Model + } + // Draft upscale (draft_task only): upstream shape omits generate_audio; sending default true can break create responses. + if body.GenerateAudio == nil && !contentHasDraftTask(body.Content) { + body.GenerateAudio = lo.ToPtr(dto.BoolValue(true)) + } + data, err := common.Marshal(body) + if err != nil { + return nil, err + } + return bytes.NewReader(data), nil +} + +// DoRequest delegates to common helper. +func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { + return channel.DoTaskApiRequest(a, c, info, requestBody) +} + +// DoResponse handles upstream response, returns taskID etc. +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + return + } + _ = resp.Body.Close() + + path := requestPathFromRelay(info) + kind := UpstreamKindFromPath(path) + + inner, err := UnmarshalEnvelope(responseBody) + if err != nil { + taskErr = service.TaskErrorWrapper(err, "invalid_response", http.StatusBadRequest) + return + } + + switch kind { + case UpstreamKindImage: + id := extractImageCreateTaskID(inner) + if id == "" { + taskErr = service.TaskErrorWrapper(fmt.Errorf("image task id is empty"), "invalid_response", http.StatusInternalServerError) + return + } + taskData = append([]byte(nil), responseBody...) + ov := dto.NewOpenAIVideo() + ov.ID = info.PublicTaskID + ov.TaskID = info.PublicTaskID + ov.CreatedAt = time.Now().Unix() + ov.Model = info.OriginModelName + if ux := jsonAnyFromBytes(responseBody); ux != nil { + ov.SetMetadata("upstream", ux) + } + c.JSON(http.StatusOK, ov) + return id, taskData, nil + + case UpstreamKindAsset: + id := extractAssetCreateID(inner) + if id == "" { + taskErr = service.TaskErrorWrapper(fmt.Errorf("asset id is empty"), "invalid_response", http.StatusInternalServerError) + return + } + taskData = append([]byte(nil), responseBody...) + resp := gin.H{ + "id": info.PublicTaskID, + "task_id": info.PublicTaskID, + "asset_id": id, + "object": "pingxingshijie.asset.upload", + } + if ux := jsonAnyFromBytes(responseBody); ux != nil { + resp["upstream"] = ux + } + c.JSON(http.StatusOK, resp) + return id, taskData, nil + + default: + var dResp responsePayload + if err := common.Unmarshal(inner, &dResp); err != nil { + taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", string(inner)), "unmarshal_response_body_failed", http.StatusInternalServerError) + return + } + upstreamID := strings.TrimSpace(dResp.ID) + if upstreamID == "" { + upstreamID = extractVideoCreateTaskID(inner) + } + if upstreamID == "" { + upstreamID = extractVideoCreateTaskID(responseBody) + } + if upstreamID == "" { + taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) + return + } + ov := dto.NewOpenAIVideo() + ov.ID = info.PublicTaskID + ov.TaskID = info.PublicTaskID + ov.CreatedAt = time.Now().Unix() + ov.Model = info.OriginModelName + if ux := jsonAnyFromBytes(responseBody); ux != nil { + ov.SetMetadata("upstream", ux) + } + c.JSON(http.StatusOK, ov) + return upstreamID, append([]byte(nil), responseBody...), nil + } +} + +// extractVideoCreateTaskID resolves upstream task id from POST /v2/video/generations inner JSON, +// full envelope JSON, or variants where id is nested (Volc/Ark-style) or data is a string id. +func extractVideoCreateTaskID(inner []byte) string { + inner = bytes.TrimSpace(inner) + if len(inner) == 0 { + return "" + } + // Inner "data" may be a JSON string task id + if inner[0] == '"' { + var s string + if common.Unmarshal(inner, &s) == nil { + if tid := strings.TrimSpace(s); isPlausibleUpstreamTaskID(tid) { + return tid + } + } + return "" + } + var raw any + if common.Unmarshal(inner, &raw) != nil { + return "" + } + if id := deepFindVideoTaskID(raw, 0); id != "" { + return id + } + return "" +} + +const maxVideoTaskIDDepth = 22 + +// isPlausibleUpstreamTaskID avoids treating human messages (e.g. msg text) as ids when walking JSON. +func isPlausibleUpstreamTaskID(s string) bool { + s = strings.TrimSpace(s) + if len(s) < 8 { + return false + } + if strings.HasPrefix(s, "cgt-") { + return true + } + // Image-style ids from same platform + if strings.HasPrefix(s, "I") && strings.Contains(s, "-") && len(s) >= 12 { + return true + } + // Hyphenated opaque ids (avoid short tokens like "ok") + if strings.Count(s, "-") >= 2 && len(s) >= 12 { + return true + } + return false +} + +// deepFindVideoTaskID walks nested maps/arrays to find a task id string. +func deepFindVideoTaskID(v any, depth int) string { + if depth > maxVideoTaskIDDepth || v == nil { + return "" + } + switch t := v.(type) { + case string: + s := strings.TrimSpace(t) + if isPlausibleUpstreamTaskID(s) { + return s + } + return "" + case map[string]any: + priority := []string{"id", "task_id", "taskId", "TaskId", "TaskID", "generation_id", "GenerationId"} + for _, k := range priority { + if s, ok := t[k].(string); ok { + if tid := strings.TrimSpace(s); tid != "" { + return tid + } + } + } + // Envelope or gateway: data may be the id string + if ds, ok := t["data"].(string); ok { + if tid := strings.TrimSpace(ds); isPlausibleUpstreamTaskID(tid) { + return tid + } + } + for _, k := range []string{"Result", "result", "data", "task", "Task", "output", "response", "Response"} { + if sub, ok := t[k]; ok { + if id := deepFindVideoTaskID(sub, depth+1); id != "" { + return id + } + } + } + for _, sub := range t { + if id := deepFindVideoTaskID(sub, depth+1); id != "" { + return id + } + } + case []any: + for _, el := range t { + if id := deepFindVideoTaskID(el, depth+1); id != "" { + return id + } + } + } + return "" +} + +func extractImageCreateTaskID(inner []byte) string { + var m map[string]any + if common.Unmarshal(inner, &m) != nil { + return "" + } + if d, ok := m["data"].(map[string]any); ok { + if d2, ok := d["data"].(map[string]any); ok { + if id, ok := d2["id"].(string); ok { + return id + } + } + if id, ok := d["id"].(string); ok { + return id + } + } + if id, ok := m["id"].(string); ok { + return id + } + return "" +} + +func extractAssetCreateID(inner []byte) string { + var m map[string]any + if common.Unmarshal(inner, &m) != nil { + return "" + } + for _, k := range []string{"asset_id", "id", "AssetId", "ID"} { + if v, ok := m[k].(string); ok && v != "" { + return v + } + } + if d, ok := m["data"].(map[string]any); ok { + for _, k := range []string{"asset_id", "id"} { + if v, ok := d[k].(string); ok && v != "" { + return v + } + } + } + for _, resultKey := range []string{"Result", "result"} { + if res, ok := m[resultKey].(map[string]any); ok { + for _, k := range []string{"Id", "id", "asset_id", "AssetId", "ID"} { + if v, ok := res[k].(string); ok && v != "" { + return v + } + } + } + } + return "" +} + +// FetchTask fetches task status (GET for video/image, POST JSON for asset). +func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any, proxy string) (*http.Response, error) { + taskID, ok := body["task_id"].(string) + if !ok || taskID == "" { + return nil, fmt.Errorf("invalid task_id") + } + kind, _ := body["upstream_kind"].(string) + if kind == "" { + kind = UpstreamKindVideo + } + baseUrl = strings.TrimRight(baseUrl, "/") + + var req *http.Request + var err error + switch kind { + case UpstreamKindAsset: + payload := map[string]any{"asset_id": taskID} + raw, mErr := common.Marshal(payload) + if mErr != nil { + return nil, mErr + } + req, err = http.NewRequest(http.MethodPost, baseUrl+"/v2/asset/status", bytes.NewReader(raw)) + default: + var uri string + if kind == UpstreamKindImage { + uri = fmt.Sprintf("%s/v2/image/generations/tasks/%s", baseUrl, url.PathEscape(taskID)) + } else { + uri = fmt.Sprintf("%s/v2/video/generations/tasks/%s", baseUrl, url.PathEscape(taskID)) + } + req, err = http.NewRequest(http.MethodGet, uri, nil) + } + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+key) + + client, err := service.GetHttpClientWithProxy(proxy) + if err != nil { + return nil, fmt.Errorf("new proxy http client failed: %w", err) + } + return client.Do(req) +} + +func (a *TaskAdaptor) GetModelList() []string { + return ModelList +} + +func (a *TaskAdaptor) GetChannelName() string { + return ChannelName +} + +func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) { + r := requestPayload{ + Model: req.Model, + Content: []ContentItem{}, + } + + if req.HasImage() { + for _, imgURL := range req.Images { + r.Content = append(r.Content, ContentItem{ + Type: "image_url", + ImageURL: &MediaURL{ + URL: imgURL, + }, + }) + } + } + + metadata := req.Metadata + if err := taskcommon.UnmarshalMetadata(metadata, &r); err != nil { + return nil, errors.Wrap(err, "unmarshal metadata failed") + } + if r.Resolution == "" { + r.Resolution = req.Resolution + } + + // Draft ID upscale (draft_task): upstream body is content + resolution (+ optional flags like watermark). + // No top-level seconds -> duration merge here. If client ever sets metadata "draft" (bool), clear it — + // draft_task reference is unrelated to that field; typical clients omit metadata.draft entirely. + if contentHasDraftTask(r.Content) { + r.Content = lo.Reject(r.Content, func(c ContentItem, _ int) bool { return c.Type == "text" }) + r.Resolution = normalizeSeedance15DraftUpscaleResolution(r.Model, r.Resolution) + r.Draft = nil + return &r, nil + } + + if sec, _ := strconv.Atoi(req.Seconds); sec > 0 { + r.Duration = lo.ToPtr(dto.IntValue(sec)) + } else if req.Duration > 0 && r.Duration == nil { + r.Duration = lo.ToPtr(dto.IntValue(req.Duration)) + } + + if !contentHasText(r.Content) { + r.Content = append(r.Content, ContentItem{ + Type: "text", + Text: req.Prompt, + }) + } + + return &r, nil +} + +func contentHasDraftTask(items []ContentItem) bool { + for _, c := range items { + if c.Type == "draft_task" { + return true + } + } + return false +} + +func contentHasText(items []ContentItem) bool { + for _, c := range items { + if c.Type == "text" { + return true + } + } + return false +} + +// normalizeSeedance15DraftUpscaleResolution maps draft-ID upscale to allowed outputs only (720p / 1080p). +// Downstream may send 480p from the draft preview tier; upstream rejects or returns empty id without this. +func normalizeSeedance15DraftUpscaleResolution(model, resolution string) string { + if !strings.Contains(strings.ToLower(model), "seedance-1-5-pro") { + return resolution + } + low := strings.ToLower(strings.TrimSpace(resolution)) + switch low { + case "1080p": + return "1080p" + case "720p": + return "720p" + default: + // 480p and any other value -> 720p (minimum supported upscale target per upstream rules) + return "720p" + } +} + +// unwrapInnerForTaskData returns the inner JSON from a PingXingShiJie envelope, or the original +// body if not wrapped. If the envelope has code==0 but an empty/missing "data" field, returns raw +// so callers never pass an empty slice to json.Unmarshal (which yields "unexpected end of JSON input"). +func unwrapInnerForTaskData(raw []byte) ([]byte, error) { + if len(bytes.TrimSpace(raw)) == 0 { + return nil, fmt.Errorf("empty task data") + } + inner, err := UnmarshalEnvelope(raw) + if err != nil { + return raw, nil + } + if len(bytes.TrimSpace(inner)) == 0 { + return raw, nil + } + return inner, nil +} + +func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) { + inner, err := unwrapInnerForTaskData(respBody) + if err != nil { + return nil, err + } + + // Asset status: Result.Status + if ti, ok := parseAssetStatus(inner); ok { + return ti, nil + } + + // Video-style + var vid responseTask + if common.Unmarshal(inner, &vid) == nil && (vid.Content.VideoURL != "" || vid.Status != "" || vid.ID != "") { + return mapVideoTaskResult(&vid) + } + + // Image-style + var img imageResponseTask + if common.Unmarshal(inner, &img) == nil && (img.resultImageURL() != "" || img.Status != "" || img.ID != "") { + return mapImageTaskResult(&img) + } + + // Fallback: try video struct without strict match + var v2 responseTask + if err := common.Unmarshal(inner, &v2); err != nil { + return nil, errors.Wrap(err, "unmarshal task result failed") + } + return mapVideoTaskResult(&v2) +} + +func parseAssetStatus(inner []byte) (*relaycommon.TaskInfo, bool) { + var m map[string]any + if common.Unmarshal(inner, &m) != nil { + return nil, false + } + var res map[string]any + if r, ok := m["Result"].(map[string]any); ok { + res = r + } else if r, ok := m["result"].(map[string]any); ok { + res = r + } + if res == nil { + return nil, false + } + status := "" + if s, ok := res["Status"].(string); ok { + status = s + } else if s, ok := res["status"].(string); ok { + status = s + } + st := strings.ToLower(strings.TrimSpace(status)) + tr := &relaycommon.TaskInfo{Code: 0} + switch st { + case "processing", "pending", "queued", "running": + tr.Status = model.TaskStatusInProgress + tr.Progress = "50%" + case "active", "succeeded", "success", "completed": + tr.Status = model.TaskStatusSuccess + tr.Progress = "100%" + if u := extractStringFromMap(m, "url", "asset_url", "AssetUrl"); u != "" { + tr.Url = u + } + case "failed", "failure": + tr.Status = model.TaskStatusFailure + tr.Progress = "100%" + tr.Reason = extractStringFromMap(res, "Message", "message", "reason") + default: + if st == "" { + return nil, false + } + tr.Status = model.TaskStatusInProgress + tr.Progress = "30%" + } + return tr, true +} + +func extractStringFromMap(m map[string]any, keys ...string) string { + for _, k := range keys { + if v, ok := m[k].(string); ok && v != "" { + return v + } + } + return "" +} + +func mapVideoTaskResult(resTask *responseTask) (*relaycommon.TaskInfo, error) { + taskResult := relaycommon.TaskInfo{ + Code: 0, + } + switch strings.ToLower(resTask.Status) { + case "pending", "queued": + taskResult.Status = model.TaskStatusQueued + taskResult.Progress = "10%" + case "processing", "running": + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = "50%" + case "succeeded", "success", "completed", "done": + taskResult.Status = model.TaskStatusSuccess + taskResult.Progress = "100%" + taskResult.Url = resTask.Content.VideoURL + taskResult.CompletionTokens = resTask.Usage.CompletionTokens + taskResult.TotalTokens = resTask.Usage.TotalTokens + case "failed", "failure": + taskResult.Status = model.TaskStatusFailure + taskResult.Progress = "100%" + taskResult.Reason = resTask.Error.Message + default: + if resTask.Status == "" { + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = "30%" + } else { + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = "30%" + } + } + return &taskResult, nil +} + +func mapImageTaskResult(resTask *imageResponseTask) (*relaycommon.TaskInfo, error) { + taskResult := relaycommon.TaskInfo{Code: 0} + switch strings.ToLower(resTask.Status) { + case "pending", "queued": + taskResult.Status = model.TaskStatusQueued + taskResult.Progress = "10%" + case "processing", "running": + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = "50%" + case "succeeded", "success", "completed", "done": + taskResult.Status = model.TaskStatusSuccess + taskResult.Progress = "100%" + taskResult.Url = resTask.resultImageURL() + case "failed", "failure": + taskResult.Status = model.TaskStatusFailure + taskResult.Progress = "100%" + taskResult.Reason = resTask.Error.Message + default: + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = "30%" + } + return &taskResult, nil +} + +func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) { + var dResp responseTask + if len(bytes.TrimSpace(originTask.Data)) > 0 { + inner, err := unwrapInnerForTaskData(originTask.Data) + if err != nil { + return nil, err + } + if err := common.Unmarshal(inner, &dResp); err != nil { + return nil, errors.Wrap(err, "unmarshal pingxingshijie task data failed") + } + } + + videoURL := strings.TrimSpace(dResp.Content.VideoURL) + if videoURL == "" { + videoURL = strings.TrimSpace(originTask.GetResultURL()) + } + + openAIVideo := dto.NewOpenAIVideo() + openAIVideo.ID = originTask.TaskID + openAIVideo.TaskID = originTask.TaskID + openAIVideo.Status = originTask.Status.ToVideoStatus() + openAIVideo.SetProgressStr(originTask.Progress) + openAIVideo.SetMetadata("url", videoURL) + openAIVideo.CreatedAt = originTask.CreatedAt + openAIVideo.CompletedAt = originTask.UpdatedAt + openAIVideo.Model = originTask.Properties.OriginModelName + if ux := jsonAnyFromBytes(originTask.Data); ux != nil { + openAIVideo.SetMetadata("upstream", ux) + } + + if strings.EqualFold(dResp.Status, "failed") || strings.EqualFold(dResp.Status, "failure") { + openAIVideo.Error = &dto.OpenAIVideoError{ + Message: dResp.Error.Message, + Code: dResp.Error.Code, + } + } + + return common.Marshal(openAIVideo) +} + +// ConvertToOpenAIAsyncImage implements channel.OpenAIAsyncImageConverter. +func (a *TaskAdaptor) ConvertToOpenAIAsyncImage(originTask *model.Task) ([]byte, error) { + inner, err := unwrapInnerForTaskData(originTask.Data) + if err != nil { + return nil, err + } + out := map[string]any{ + "object": "pingxingshijie.image.generation.task", + "id": originTask.TaskID, + "task_id": originTask.TaskID, + "status": originTask.Status.ToVideoStatus(), + "progress": originTask.Progress, + "model": originTask.Properties.OriginModelName, + "created_at": originTask.CreatedAt, + "updated_at": originTask.UpdatedAt, + } + if ux := jsonAnyFromBytes(originTask.Data); ux != nil { + out["upstream"] = ux + } + + var img imageResponseTask + if err := common.Unmarshal(inner, &img); err != nil { + if upstreamID := extractImageCreateTaskID(inner); upstreamID != "" { + out["upstream_task_id"] = upstreamID + return common.Marshal(out) + } + return nil, errors.Wrap(err, "unmarshal image task data failed") + } + if u := img.resultImageURL(); u != "" { + out["url"] = u + } + if strings.EqualFold(img.Status, "failed") || strings.EqualFold(img.Status, "failure") { + out["error"] = map[string]any{"message": img.Error.Message, "code": img.Error.Code} + } + return common.Marshal(out) +} + +// ConvertToOpenAIAssetTask implements channel.OpenAIAssetTaskConverter. +func (a *TaskAdaptor) ConvertToOpenAIAssetTask(originTask *model.Task) ([]byte, error) { + inner, err := unwrapInnerForTaskData(originTask.Data) + if err != nil { + return nil, err + } + var m map[string]any + if err := common.Unmarshal(inner, &m); err != nil { + return nil, err + } + out := map[string]any{ + "object": "pingxingshijie.asset.task", + "id": originTask.TaskID, + "task_id": originTask.TaskID, + "status": string(originTask.Status), + "progress": originTask.Progress, + "created_at": originTask.CreatedAt, + "updated_at": originTask.UpdatedAt, + "data": m, + } + if ux := jsonAnyFromBytes(originTask.Data); ux != nil { + out["upstream"] = ux + } + if originTask.FailReason != "" { + out["fail_reason"] = originTask.FailReason + } + return common.Marshal(out) +} diff --git a/relay/channel/task/pingxingshijie/asset_model_validation_test.go b/relay/channel/task/pingxingshijie/asset_model_validation_test.go new file mode 100644 index 000000000000..7a0da5fef389 --- /dev/null +++ b/relay/channel/task/pingxingshijie/asset_model_validation_test.go @@ -0,0 +1,56 @@ +package pingxingshijie + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/gin-gonic/gin" +) + +func newAssetUploadContext(body string) *gin.Context { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/assets/upload", strings.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + return c +} + +func TestValidateRequestAndSetActionRejectsNonAssetUploadModel(t *testing.T) { + c := newAssetUploadContext(`{"model":"doubao-seedream-4-5-251128","image_url":"https://example.com/a.jpg","asset_type":"Image"}`) + info := &relaycommon.RelayInfo{RequestURLPath: "/v1/assets/upload", TaskRelayInfo: &relaycommon.TaskRelayInfo{}} + + taskErr := (&TaskAdaptor{}).ValidateRequestAndSetAction(c, info) + + if taskErr == nil { + t.Fatal("expected non-asset model to be rejected for asset upload") + } +} + +func TestValidateRequestAndSetActionAllowsAssetPlaceholderModel(t *testing.T) { + c := newAssetUploadContext(`{"model":"pingxingshijie-asset","image_url":"https://example.com/a.jpg","asset_type":"Image"}`) + info := &relaycommon.RelayInfo{RequestURLPath: "/v1/assets/upload", TaskRelayInfo: &relaycommon.TaskRelayInfo{}} + + taskErr := (&TaskAdaptor{}).ValidateRequestAndSetAction(c, info) + + if taskErr != nil { + t.Fatalf("expected asset model to be accepted, got %#v", taskErr) + } + if info.Action != "assetUpload" { + t.Fatalf("expected asset upload action to avoid generate/image-to-video classification, got %q", info.Action) + } +} + +func TestValidateRequestAndSetActionRejectsBlankAssetUploadModel(t *testing.T) { + c := newAssetUploadContext(`{"image_url":"https://example.com/a.jpg","asset_type":"Image"}`) + info := &relaycommon.RelayInfo{RequestURLPath: "/v1/assets/upload", TaskRelayInfo: &relaycommon.TaskRelayInfo{}} + + taskErr := (&TaskAdaptor{}).ValidateRequestAndSetAction(c, info) + + if taskErr == nil { + t.Fatal("expected blank model to be rejected for asset upload") + } +} diff --git a/relay/channel/task/pingxingshijie/asset_request_body_test.go b/relay/channel/task/pingxingshijie/asset_request_body_test.go new file mode 100644 index 000000000000..35c7ad7b28d4 --- /dev/null +++ b/relay/channel/task/pingxingshijie/asset_request_body_test.go @@ -0,0 +1,33 @@ +package pingxingshijie + +import ( + "io" + "strings" + "testing" + + relaycommon "github.com/QuantumNous/new-api/relay/common" +) + +func TestBuildRequestBodyAssetUploadStripsGatewayModel(t *testing.T) { + c := newAssetUploadContext(`{"model":"pingxingshijie-asset","image_url":"https://example.com/a.jpg","asset_type":"Image"}`) + info := &relaycommon.RelayInfo{RequestURLPath: "/v1/assets/upload", TaskRelayInfo: &relaycommon.TaskRelayInfo{}} + + body, err := (&TaskAdaptor{}).BuildRequestBody(c, info) + if err != nil { + t.Fatal(err) + } + raw, err := io.ReadAll(body) + if err != nil { + t.Fatal(err) + } + + if strings.Contains(string(raw), "model") { + t.Fatalf("asset upstream body must not include gateway-only model: %s", string(raw)) + } + if !strings.Contains(string(raw), `"image_url":"https://example.com/a.jpg"`) { + t.Fatalf("asset upstream body lost image_url: %s", string(raw)) + } + if !strings.Contains(string(raw), `"asset_type":"Image"`) { + t.Fatalf("asset upstream body lost asset_type: %s", string(raw)) + } +} diff --git a/relay/channel/task/pingxingshijie/constants.go b/relay/channel/task/pingxingshijie/constants.go new file mode 100644 index 000000000000..4d686f913d28 --- /dev/null +++ b/relay/channel/task/pingxingshijie/constants.go @@ -0,0 +1,74 @@ +package pingxingshijie + +import ( + "fmt" + "strings" +) + +// AssetPlaceholderModel is the only model allowed for /v1/assets/upload routing and billing. +const AssetPlaceholderModel = "pingxingshijie-asset" + +func ValidateAssetUploadModel(modelName string) error { + modelName = strings.TrimSpace(modelName) + if modelName == "" { + return fmt.Errorf("model is required for asset upload; use %s", AssetPlaceholderModel) + } + if modelName != AssetPlaceholderModel { + return fmt.Errorf("invalid asset upload model %q; use %s", modelName, AssetPlaceholderModel) + } + return nil +} + +// ModelList contains the PingXingShiJie models documented in docs/pingxingshijie-api-reference.md. +var ModelList = []string{ + AssetPlaceholderModel, + "doubao-seedance-1-0-pro-fast-251015", + "doubao-seedance-1-5-pro-251215", + "doubao-seedance-2-0-fast-260128", + "doubao-seedance-2-0-260128", + "doubao-seedream-5-0-260128", + "doubao-seedream-4-5-251128", + "doubao-seedream-4-0-250828", +} + +var ChannelName = "pingxingshijie-video" + +// videoInputRatioMap discount when video input is present (with-video / without-video pricing). +// Admins should set ModelRatio to the higher "without video" rate; +// the system multiplies by this ratio when video input is detected. +var videoInputRatioMap = map[string]float64{ + "doubao-seedance-2-0-260128": 28.0 / 46.0, // ~0.6087 + "doubao-seedance-2-0-fast-260128": 22.0 / 37.0, // ~0.5946 +} + +const seedance20Model = "doubao-seedance-2-0-260128" + +var seedance20ResolutionRatioMap = map[string]float64{ + "1080p": 51.0 / 46.0, +} + +var seedance20VideoInputResolutionRatioMap = map[string]float64{ + "1080p": 31.0 / 51.0, +} + +func GetVideoInputRatio(modelName string) (float64, bool) { + r, ok := videoInputRatioMap[modelName] + return r, ok +} + +func GetVideoInputRatioForResolution(modelName, resolution string) (float64, bool) { + if modelName == seedance20Model { + if r, ok := seedance20VideoInputResolutionRatioMap[resolution]; ok { + return r, true + } + } + return GetVideoInputRatio(modelName) +} + +func GetResolutionRatio(modelName, resolution string) (float64, bool) { + if modelName != seedance20Model { + return 0, false + } + r, ok := seedance20ResolutionRatioMap[resolution] + return r, ok +} diff --git a/relay/channel/task/pingxingshijie/constants_test.go b/relay/channel/task/pingxingshijie/constants_test.go new file mode 100644 index 000000000000..8a1dba661f85 --- /dev/null +++ b/relay/channel/task/pingxingshijie/constants_test.go @@ -0,0 +1,25 @@ +package pingxingshijie + +import "testing" + +func TestModelListMatchesDocumentedPingXingShiJieModels(t *testing.T) { + want := []string{ + "pingxingshijie-asset", + "doubao-seedance-1-0-pro-fast-251015", + "doubao-seedance-1-5-pro-251215", + "doubao-seedance-2-0-fast-260128", + "doubao-seedance-2-0-260128", + "doubao-seedream-5-0-260128", + "doubao-seedream-4-5-251128", + "doubao-seedream-4-0-250828", + } + + if len(ModelList) != len(want) { + t.Fatalf("ModelList length = %d, want %d: %#v", len(ModelList), len(want), ModelList) + } + for i, model := range want { + if ModelList[i] != model { + t.Fatalf("ModelList[%d] = %q, want %q; full list: %#v", i, ModelList[i], model, ModelList) + } + } +} diff --git a/relay/channel/task/pingxingshijie/draft_upscale_test.go b/relay/channel/task/pingxingshijie/draft_upscale_test.go new file mode 100644 index 000000000000..390992d51c8f --- /dev/null +++ b/relay/channel/task/pingxingshijie/draft_upscale_test.go @@ -0,0 +1,309 @@ +package pingxingshijie + +import ( + "math" + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/gin-gonic/gin" +) + +func TestNormalizeSeedance15DraftUpscaleResolution(t *testing.T) { + m := "doubao-seedance-1-5-pro-251215" + if got := normalizeSeedance15DraftUpscaleResolution(m, "480p"); got != "720p" { + t.Fatalf("480p: got %q", got) + } + if got := normalizeSeedance15DraftUpscaleResolution(m, "720p"); got != "720p" { + t.Fatalf("720p: got %q", got) + } + if got := normalizeSeedance15DraftUpscaleResolution(m, "1080p"); got != "1080p" { + t.Fatalf("1080p: got %q", got) + } + if got := normalizeSeedance15DraftUpscaleResolution("other-model", "480p"); got != "480p" { + t.Fatalf("other model: got %q", got) + } +} + +func TestConvertToRequestPayload_DraftTaskClearsDraftAndSkipsTopLevelSeconds(t *testing.T) { + a := &TaskAdaptor{} + req := relaycommon.TaskSubmitReq{ + Model: "doubao-seedance-1-5-pro-251215", + Prompt: "Generate from draft", + Seconds: "10", + Metadata: map[string]interface{}{ + "content": []interface{}{ + map[string]interface{}{ + "type": "draft_task", + "draft_task": map[string]interface{}{"id": "cgt-20260416103233-xccct"}, + }, + }, + "draft": true, + "resolution": "1080p", + "watermark": false, + }, + } + body, err := a.convertToRequestPayload(&req) + if err != nil { + t.Fatal(err) + } + if body.Draft != nil { + t.Fatalf("draft_task upscale should omit draft flag from upstream body, got %+v", body.Draft) + } + if body.Duration != nil { + t.Fatalf("draft_task upscale should not merge top-level seconds into duration, got %+v", body.Duration) + } + if body.Resolution != "1080p" { + t.Fatalf("resolution: got %q", body.Resolution) + } +} + +// TestDraftTaskUpscale_UpstreamJSONShape documents the contract: downstream OpenAI-style body +// with metadata.content draft_task maps to upstream Ark body without generate_audio/duration/text. +func TestDraftTaskUpscale_UpstreamJSONShape(t *testing.T) { + req := relaycommon.TaskSubmitReq{ + Model: "doubao-seedance-1-5-pro-251215", + Prompt: "Generate from draft", + Metadata: map[string]interface{}{ + "content": []interface{}{ + map[string]interface{}{ + "type": "draft_task", + "draft_task": map[string]interface{}{ + "id": "cgt-20260416103233-xccct", + }, + }, + }, + "watermark": false, + "resolution": "720p", + "return_last_frame": true, + }, + } + a := &TaskAdaptor{} + body, err := a.convertToRequestPayload(&req) + if err != nil { + t.Fatal(err) + } + // Mirrors BuildRequestBody: default generate_audio is skipped when draft_task is present. + if body.GenerateAudio != nil { + t.Fatalf("generate_audio must not be set for draft_task before marshal, got %+v", body.GenerateAudio) + } + if !contentHasDraftTask(body.Content) { + t.Fatal("expected draft_task in content") + } + data, err := common.Marshal(body) + if err != nil { + t.Fatal(err) + } + var got map[string]interface{} + if err := common.Unmarshal(data, &got); err != nil { + t.Fatal(err) + } + for _, k := range []string{"generate_audio", "draft", "duration", "prompt"} { + if _, ok := got[k]; ok { + t.Errorf("upstream JSON must not include %q", k) + } + } + for _, k := range []string{"model", "content", "watermark", "resolution", "return_last_frame"} { + if _, ok := got[k]; !ok { + t.Errorf("missing expected key %q in %s", k, string(data)) + } + } + content, ok := got["content"].([]interface{}) + if !ok || len(content) != 1 { + t.Fatalf("content: got %#v", got["content"]) + } + item, ok := content[0].(map[string]interface{}) + if !ok || item["type"] != "draft_task" { + t.Fatalf("content[0]: %#v", content[0]) + } + dt, ok := item["draft_task"].(map[string]interface{}) + if !ok || dt["id"] != "cgt-20260416103233-xccct" { + t.Fatalf("draft_task: %#v", item["draft_task"]) + } +} + +func TestConvertToRequestPayload_TopLevelSeedanceContentAndParams(t *testing.T) { + const payload = `{ + "model": "doubao-seedance-1-5-pro-251215", + "prompt": "首帧过渡到尾帧", + "content": [ + {"type":"text","text":"首帧过渡到尾帧"}, + {"type":"image_url","image_url":{"url":"https://example.com/first.jpg"},"role":"first_frame"}, + {"type":"image_url","image_url":{"url":"https://example.com/last.jpg"},"role":"last_frame"} + ], + "generate_audio": true, + "ratio": "adaptive", + "duration": 6, + "watermark": false, + "resolution": "720p" + }` + var req relaycommon.TaskSubmitReq + if err := common.UnmarshalJsonStr(payload, &req); err != nil { + t.Fatal(err) + } + body, err := (&TaskAdaptor{}).convertToRequestPayload(&req) + if err != nil { + t.Fatal(err) + } + data, err := common.Marshal(body) + if err != nil { + t.Fatal(err) + } + var got map[string]interface{} + if err := common.Unmarshal(data, &got); err != nil { + t.Fatal(err) + } + content, ok := got["content"].([]interface{}) + if !ok || len(content) != 3 { + t.Fatalf("content: got %#v in %s", got["content"], string(data)) + } + for i, wantRole := range []string{"first_frame", "last_frame"} { + item, ok := content[i+1].(map[string]interface{}) + if !ok || item["role"] != wantRole { + t.Fatalf("content[%d] role: got %#v want %q", i+1, content[i+1], wantRole) + } + } + if got["duration"] != float64(6) { + t.Fatalf("duration: got %#v", got["duration"]) + } + if got["resolution"] != "720p" { + t.Fatalf("resolution: got %#v", got["resolution"]) + } + if got["ratio"] != "adaptive" { + t.Fatalf("ratio: got %#v", got["ratio"]) + } + if got["watermark"] != false { + t.Fatalf("watermark: got %#v", got["watermark"]) + } + if got["generate_audio"] != true { + t.Fatalf("generate_audio: got %#v", got["generate_audio"]) + } +} + +func TestEstimateBilling_Seedance20OnlyApplies1080PResolutionRatio(t *testing.T) { + gin.SetMode(gin.TestMode) + + tests := []struct { + name string + model string + resolution string + content []interface{} + want map[string]float64 + }{ + { + name: "seedance 2.0 1080p without video input uses 51 base price ratio", + model: "doubao-seedance-2-0-260128", + resolution: "1080p", + want: map[string]float64{"resolution": 51.0 / 46.0}, + }, + { + name: "seedance 2.0 1080p with video input uses corrected 31 over 51 combined ratio", + model: "doubao-seedance-2-0-260128", + resolution: "1080p", + content: []interface{}{ + map[string]interface{}{ + "type": "video_url", + "video_url": map[string]interface{}{"url": "https://example.com/input.mp4"}, + }, + }, + want: map[string]float64{"video_input": 31.0 / 51.0, "resolution": 51.0 / 46.0}, + }, + { + name: "seedance 2.0 720p without video input has no adjustment", + model: "doubao-seedance-2-0-260128", + resolution: "720p", + }, + { + name: "seedance 2.0 480p with video input keeps normal video input pricing", + model: "doubao-seedance-2-0-260128", + resolution: "480p", + content: []interface{}{ + map[string]interface{}{ + "type": "video_url", + "video_url": map[string]interface{}{"url": "https://example.com/input.mp4"}, + }, + }, + want: map[string]float64{"video_input": 28.0 / 46.0}, + }, + { + name: "seedance 2.0 720p with video input keeps normal video input pricing", + model: "doubao-seedance-2-0-260128", + resolution: "720p", + content: []interface{}{ + map[string]interface{}{ + "type": "video_url", + "video_url": map[string]interface{}{"url": "https://example.com/input.mp4"}, + }, + }, + want: map[string]float64{"video_input": 28.0 / 46.0}, + }, + { + name: "seedance 2.0 fast 1080p is not adjusted by seedance 2.0 pricing", + model: "doubao-seedance-2-0-fast-260128", + resolution: "1080p", + }, + { + name: "seedance 2.0 fast with video input keeps fast pricing", + model: "doubao-seedance-2-0-fast-260128", + resolution: "1080p", + content: []interface{}{ + map[string]interface{}{ + "type": "video_url", + "video_url": map[string]interface{}{"url": "https://example.com/input.mp4"}, + }, + }, + want: map[string]float64{"video_input": 22.0 / 37.0}, + }, + { + name: "seedance 1.5 pro 1080p is not adjusted by seedance 2.0 pricing", + model: "doubao-seedance-1-5-pro-251215", + resolution: "1080p", + }, + { + name: "seedance 1.0 is not adjusted by seedance 2.0 pricing", + model: "doubao-seedance-1-0-pro-fast-251015", + resolution: "1080p", + content: []interface{}{ + map[string]interface{}{ + "type": "video_url", + "video_url": map[string]interface{}{"url": "https://example.com/input.mp4"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + metadata := map[string]interface{}{"resolution": tt.resolution} + if tt.content != nil { + metadata["content"] = tt.content + } + c.Set("task_request", relaycommon.TaskSubmitReq{ + Model: tt.model, + Prompt: "test", + Metadata: metadata, + }) + got := (&TaskAdaptor{}).EstimateBilling(c, &relaycommon.RelayInfo{OriginModelName: tt.model}) + assertRatios(t, got, tt.want) + }) + } +} + +func assertRatios(t *testing.T, got, want map[string]float64) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("ratio count: got %#v want %#v", got, want) + } + for key, wantValue := range want { + gotValue, ok := got[key] + if !ok { + t.Fatalf("missing ratio %q in %#v", key, got) + } + if math.Abs(gotValue-wantValue) > 1e-12 { + t.Fatalf("ratio %q: got %.12f want %.12f", key, gotValue, wantValue) + } + } +} diff --git a/relay/channel/task/pingxingshijie/envelope.go b/relay/channel/task/pingxingshijie/envelope.go new file mode 100644 index 000000000000..d1c8ce948269 --- /dev/null +++ b/relay/channel/task/pingxingshijie/envelope.go @@ -0,0 +1,93 @@ +package pingxingshijie + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/QuantumNous/new-api/common" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" +) + +// APIEnvelope is the common PingXingShiJie response wrapper: {"code":0,"msg":"ok","data":...} +// Some routes (e.g. /v2/chat/completions) use "message" instead of "msg" for human-readable text. +type APIEnvelope struct { + Code int `json:"code"` + Msg string `json:"msg"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` +} + +func (e *APIEnvelope) text() string { + if e.Msg != "" { + return e.Msg + } + return e.Message +} + +// HTTPStatusForPingXingBizCode maps upstream business code (HTTP 200 body) to HTTP status for clients. +func HTTPStatusForPingXingBizCode(code int) int { + switch code { + case 401: + return http.StatusUnauthorized + case 403: + return http.StatusForbidden + case 429: + return http.StatusTooManyRequests + default: + return http.StatusBadRequest + } +} + +// NormalizePingXingOpenAIShapedSyncBody unwraps PingXing envelope when top-level "code" exists. +// Plain OpenAI JSON (no "code") is returned unchanged. On business failure returns bizCode != 0 and bizMsg. +func NormalizePingXingOpenAIShapedSyncBody(body []byte) (inner []byte, bizCode int, bizMsg string) { + if len(body) == 0 || !gjson.GetBytes(body, "code").Exists() { + return body, 0, "" + } + var env APIEnvelope + if err := common.Unmarshal(body, &env); err != nil { + return body, 0, "" + } + if env.Code != 0 { + return nil, env.Code, env.text() + } + if len(env.Data) > 0 && string(env.Data) != "null" { + return env.Data, 0, "" + } + return body, 0, "" +} + +// UnmarshalEnvelope parses the outer wrapper and returns the raw inner data JSON. +func UnmarshalEnvelope(body []byte) (data json.RawMessage, err error) { + var env APIEnvelope + if err := common.Unmarshal(body, &env); err != nil { + return nil, errors.Wrap(err, "unmarshal envelope failed") + } + if env.Code != 0 { + return nil, fmt.Errorf("upstream error code=%d msg=%s", env.Code, env.text()) + } + return env.Data, nil +} + +// UnmarshalEnvelopeData unmarshals envelope and decodes data into v. +func UnmarshalEnvelopeData(body []byte, v any) error { + raw, err := UnmarshalEnvelope(body) + if err != nil { + return err + } + if len(raw) == 0 || string(raw) == "null" { + return fmt.Errorf("empty envelope data") + } + return common.Unmarshal(raw, v) +} + +// UnmarshalDataOrEnvelope unmarshals PingXingShiJie envelope data into v, or raw body if not wrapped. +func UnmarshalDataOrEnvelope(body []byte, v any) error { + if err := UnmarshalEnvelopeData(body, v); err == nil { + return nil + } + return common.Unmarshal(body, v) +} diff --git a/relay/channel/task/pingxingshijie/envelope_test.go b/relay/channel/task/pingxingshijie/envelope_test.go new file mode 100644 index 000000000000..1d1ae0c2a8e2 --- /dev/null +++ b/relay/channel/task/pingxingshijie/envelope_test.go @@ -0,0 +1,54 @@ +package pingxingshijie + +import ( + "encoding/json" + "testing" + + "github.com/QuantumNous/new-api/common" +) + +func TestNormalizePingXingOpenAIShapedSyncBody_plainOpenAI(t *testing.T) { + raw := []byte(`{"id":"x","choices":[]}`) + inner, code, msg := NormalizePingXingOpenAIShapedSyncBody(raw) + if code != 0 || msg != "" { + t.Fatalf("unexpected biz err: code=%d msg=%q", code, msg) + } + if string(inner) != string(raw) { + t.Fatalf("expected unchanged body, got %s", string(inner)) + } +} + +func TestNormalizePingXingOpenAIShapedSyncBody_bizErrorMessageField(t *testing.T) { + raw := []byte(`{"code":401,"message":"无效的令牌"}`) + inner, code, msg := NormalizePingXingOpenAIShapedSyncBody(raw) + if inner != nil { + t.Fatalf("expected nil inner, got %s", string(inner)) + } + if code != 401 || msg != "无效的令牌" { + t.Fatalf("got code=%d msg=%q", code, msg) + } +} + +func TestNormalizePingXingOpenAIShapedSyncBody_unwrapData(t *testing.T) { + innerObj := map[string]any{"choices": []any{}} + innerBytes, err := common.Marshal(innerObj) + if err != nil { + t.Fatal(err) + } + // Marshal nested object as JSON object for data, not string + outer2, err := common.Marshal(struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data json.RawMessage `json:"data"` + }{Code: 0, Msg: "ok", Data: innerBytes}) + if err != nil { + t.Fatal(err) + } + inner, code, msg := NormalizePingXingOpenAIShapedSyncBody(outer2) + if code != 0 || msg != "" { + t.Fatalf("unexpected biz err: code=%d msg=%q", code, msg) + } + if string(inner) != string(innerBytes) { + t.Fatalf("unwrap mismatch: got %s want %s", string(inner), string(innerBytes)) + } +} diff --git a/relay/channel/task/pingxingshijie/extract_video_id_test.go b/relay/channel/task/pingxingshijie/extract_video_id_test.go new file mode 100644 index 000000000000..aaffea187622 --- /dev/null +++ b/relay/channel/task/pingxingshijie/extract_video_id_test.go @@ -0,0 +1,28 @@ +package pingxingshijie + +import "testing" + +func TestExtractVideoCreateTaskID(t *testing.T) { + cases := []struct { + name string + json string + want string + }{ + {"top_id", `{"id":"cgt-a"}`, "cgt-a"}, + {"top_task_id", `{"task_id":"cgt-b"}`, "cgt-b"}, + {"data_id", `{"data":{"id":"cgt-c"}}`, "cgt-c"}, + {"data_task_id", `{"data":{"task_id":"cgt-d"}}`, "cgt-d"}, + {"nested_data", `{"data":{"data":{"id":"cgt-e"}}}`, "cgt-e"}, + {"Result_wrapper", `{"Result":{"id":"cgt-f"}}`, "cgt-f"}, + {"data_string", `{"code":0,"msg":"ok","data":"cgt-20260317165706-4lpxk"}`, "cgt-20260317165706-4lpxk"}, + {"deep_task", `{"response":{"task":{"id":"cgt-deep-20260101120000-abc12"}}}`, "cgt-deep-20260101120000-abc12"}, + {"ignores_msg_ok", `{"code":0,"msg":"ok","data":{"id":"cgt-from-data"}}`, "cgt-from-data"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := extractVideoCreateTaskID([]byte(tc.json)); got != tc.want { + t.Fatalf("got %q want %q", got, tc.want) + } + }) + } +} diff --git a/relay/channel/task/pingxingshijie/kind.go b/relay/channel/task/pingxingshijie/kind.go new file mode 100644 index 000000000000..71ca10806d9d --- /dev/null +++ b/relay/channel/task/pingxingshijie/kind.go @@ -0,0 +1,34 @@ +package pingxingshijie + +import ( + "strings" + + "github.com/gin-gonic/gin" +) + +// Upstream kind for PingXingShiJie async APIs (stored in task private data for polling). +const ( + UpstreamKindVideo = "video" + UpstreamKindImage = "image" + UpstreamKindAsset = "asset" +) + +// UpstreamKindFromPath returns video | image | asset from gateway path. +func UpstreamKindFromPath(path string) string { + if strings.Contains(path, "/v1/assets/upload") { + return UpstreamKindAsset + } + // POST /v1/images/generations/async or GET /v1/images/generations/:task_id + if strings.Contains(path, "/v1/images/generations/") { + return UpstreamKindImage + } + return UpstreamKindVideo +} + +// UpstreamKindFromGin is a convenience wrapper. +func UpstreamKindFromGin(c *gin.Context) string { + if c == nil || c.Request == nil { + return UpstreamKindVideo + } + return UpstreamKindFromPath(c.Request.URL.Path) +} diff --git a/relay/channel/task/pingxingshijie/unwrap_task_data_test.go b/relay/channel/task/pingxingshijie/unwrap_task_data_test.go new file mode 100644 index 000000000000..0d9db4de5a41 --- /dev/null +++ b/relay/channel/task/pingxingshijie/unwrap_task_data_test.go @@ -0,0 +1,90 @@ +package pingxingshijie + +import ( + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" +) + +func TestUnwrapInnerForTaskData_EnvelopeEmptyDataUsesRaw(t *testing.T) { + // Success envelope with missing/empty data must not return empty inner (would break json.Unmarshal). + const body = `{"code":0,"msg":"ok"}` + inner, err := unwrapInnerForTaskData([]byte(body)) + if err != nil { + t.Fatal(err) + } + if string(inner) != body { + t.Fatalf("expected full body fallback, got %q", string(inner)) + } +} + +func TestConvertToOpenAIVideo_EnvelopeShapeWithoutInnerTask(t *testing.T) { + a := &TaskAdaptor{} + task := &model.Task{ + TaskID: "task_test", + Status: model.TaskStatusInProgress, + Progress: "30%", + Data: []byte(`{"code":0,"msg":"ok"}`), + Properties: model.Properties{OriginModelName: "m"}, + } + b, err := a.ConvertToOpenAIVideo(task) + if err != nil { + t.Fatal(err) + } + var out map[string]any + if err := common.Unmarshal(b, &out); err != nil { + t.Fatal(err) + } +} + +func TestConvertToOpenAIAsyncImage_SubmitAckReturnsPendingResponse(t *testing.T) { + a := &TaskAdaptor{} + task := &model.Task{ + TaskID: "task_image_ack", + Status: model.TaskStatusSubmitted, + Progress: "10%", + Data: []byte(`{ + "code": 0, + "msg": "ok", + "data": {"data": {"id": "I20260401210457-4767-8dc442"}} + }`), + Properties: model.Properties{OriginModelName: "doubao-seedream-4-5-251128"}, + } + + b, err := a.ConvertToOpenAIAsyncImage(task) + if err != nil { + t.Fatal(err) + } + var out map[string]any + if err := common.Unmarshal(b, &out); err != nil { + t.Fatal(err) + } + if out["task_id"] != "task_image_ack" { + t.Fatalf("task_id: got %#v", out["task_id"]) + } + if out["status"] != model.TaskStatus(model.TaskStatusSubmitted).ToVideoStatus() { + t.Fatalf("status: got %#v", out["status"]) + } + if _, ok := out["url"]; ok { + t.Fatalf("submit ack must not invent url: %#v", out["url"]) + } + if _, ok := out["upstream"]; !ok { + t.Fatal("expected upstream submit ack metadata") + } +} + +func TestExtractAssetCreateID_ResponseMetadataResultShape(t *testing.T) { + const body = `{ + "ResponseMetadata": { + "RequestId": "20260319162446B5DF7E4FBBC56F78E6DA" + }, + "Result": { + "Id": "asset-20260319082447-qrrjp" + } + }` + + if got := extractAssetCreateID([]byte(body)); got != "asset-20260319082447-qrrjp" { + t.Fatalf("extractAssetCreateID() = %q", got) + } +} diff --git a/relay/channel/task/pingxingshijie/upstream_metadata.go b/relay/channel/task/pingxingshijie/upstream_metadata.go new file mode 100644 index 000000000000..ed99dcc9ecf7 --- /dev/null +++ b/relay/channel/task/pingxingshijie/upstream_metadata.go @@ -0,0 +1,19 @@ +package pingxingshijie + +import ( + "bytes" + + "github.com/QuantumNous/new-api/common" +) + +// jsonAnyFromBytes parses JSON into a generic value for downstream metadata (full upstream payload). +func jsonAnyFromBytes(b []byte) any { + if len(bytes.TrimSpace(b)) == 0 { + return nil + } + var v any + if err := common.Unmarshal(b, &v); err != nil { + return nil + } + return v +} diff --git a/relay/channel/task/pingxingshijie/upstream_metadata_test.go b/relay/channel/task/pingxingshijie/upstream_metadata_test.go new file mode 100644 index 000000000000..49c59e738fa4 --- /dev/null +++ b/relay/channel/task/pingxingshijie/upstream_metadata_test.go @@ -0,0 +1,46 @@ +package pingxingshijie + +import ( + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" +) + +func TestJsonAnyFromBytes(t *testing.T) { + v := jsonAnyFromBytes([]byte(`{"code":0,"msg":"ok","data":{"id":"x"}}`)) + m, ok := v.(map[string]any) + if !ok { + t.Fatalf("expected map, got %T", v) + } + if m["code"].(float64) != 0 { + t.Fatalf("code: %v", m["code"]) + } +} + +func TestConvertToOpenAIVideo_IncludesUpstreamMetadata(t *testing.T) { + a := &TaskAdaptor{} + task := &model.Task{ + TaskID: "task_x", + Status: model.TaskStatusSuccess, + Progress: "100%", + Data: []byte(`{"code":0,"msg":"ok","data":{"id":"u1","status":"succeeded","content":{"video_url":"https://ex/v.mp4"},"usage":{"total_tokens":1}}}`), + Properties: model.Properties{OriginModelName: "m1"}, + } + b, err := a.ConvertToOpenAIVideo(task) + if err != nil { + t.Fatal(err) + } + var out map[string]any + if err := common.Unmarshal(b, &out); err != nil { + t.Fatal(err) + } + meta, _ := out["metadata"].(map[string]any) + if meta == nil { + t.Fatal("missing metadata") + } + up, ok := meta["upstream"].(map[string]any) + if !ok || up["code"].(float64) != 0 { + t.Fatalf("metadata.upstream: %#v", meta["upstream"]) + } +} diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go index ba9f223bd2f6..cc4a849faac8 100644 --- a/relay/channel/volcengine/adaptor.go +++ b/relay/channel/volcengine/adaptor.go @@ -15,6 +15,7 @@ import ( "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/openai" + taskpxsj "github.com/QuantumNous/new-api/relay/channel/task/pingxingshijie" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/setting/model_setting" @@ -241,6 +242,18 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if baseUrl == "" { baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine] } + baseTrim := strings.TrimSuffix(strings.TrimSpace(baseUrl), "/") + // PingXingShiJie (58): text chat is served under /v2/* (same API family as video/image). /v1/chat/completions and /api/v3/* return HTTP 400 "接口不存在" on api.pingxingshijie.cn. + if info.ChannelMeta != nil && info.ChannelMeta.ChannelType == channelconstant.ChannelTypePingXingShiJie { + switch info.RelayMode { + case constant.RelayModeChatCompletions: + return baseTrim + "/v2/chat/completions", nil + case constant.RelayModeImagesGenerations: + return baseTrim + "/v2/image/generations", nil + default: + return "", fmt.Errorf("unsupported PingXingShiJie relay mode: %d", info.RelayMode) + } + } specialPlan, hasSpecialPlan := channelconstant.ChannelSpecialBases[baseUrl] switch info.RelayFormat { @@ -388,6 +401,32 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom return handleTTSResponse(c, resp, info, encoding) } + // PingXingShiJie (58) sync APIs often wrap JSON in {"code":0,"msg":"...","data":...}; unwrap for OpenAI-shaped inner body. + if info.ChannelMeta != nil && info.ChannelMeta.ChannelType == channelconstant.ChannelTypePingXingShiJie && + !info.IsStream && resp != nil && resp.Body != nil { + body, readErr := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if readErr != nil { + return nil, types.NewErrorWithStatusCode(readErr, types.ErrorCodeBadResponseBody, http.StatusBadGateway) + } + if len(body) > 0 { + inner, bizCode, bizMsg := taskpxsj.NormalizePingXingOpenAIShapedSyncBody(body) + if bizCode != 0 { + return nil, types.NewErrorWithStatusCode( + fmt.Errorf("%s", bizMsg), + types.ErrorCodeBadResponseBody, + taskpxsj.HTTPStatusForPingXingBizCode(bizCode), + ) + } + body = inner + resp.Body = io.NopCloser(bytes.NewReader(body)) + resp.ContentLength = int64(len(body)) + resp.Header.Del("Content-Encoding") + } else { + resp.Body = io.NopCloser(bytes.NewReader(body)) + } + } + adaptor := openai.Adaptor{} usage, err = adaptor.DoResponse(c, resp, info) return diff --git a/relay/channel/volcengine/adaptor_test.go b/relay/channel/volcengine/adaptor_test.go new file mode 100644 index 000000000000..f979bb9032f0 --- /dev/null +++ b/relay/channel/volcengine/adaptor_test.go @@ -0,0 +1,29 @@ +package volcengine + +import ( + "testing" + + channelconstant "github.com/QuantumNous/new-api/constant" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" +) + +func TestPingXingShiJieImageGenerationUsesDocumentedV2Endpoint(t *testing.T) { + a := &Adaptor{} + info := &relaycommon.RelayInfo{ + RelayMode: relayconstant.RelayModeImagesGenerations, + ChannelMeta: &relaycommon.ChannelMeta{ + ChannelType: channelconstant.ChannelTypePingXingShiJie, + ChannelBaseUrl: "https://api.pingxingshijie.cn", + }, + } + + got, err := a.GetRequestURL(info) + if err != nil { + t.Fatal(err) + } + const want = "https://api.pingxingshijie.cn/v2/image/generations" + if got != want { + t.Fatalf("GetRequestURL() = %q, want %q", got, want) + } +} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 64d4d4eedfaa..b091f086d435 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -1,6 +1,7 @@ package common import ( + "bytes" "encoding/json" "errors" "fmt" @@ -310,24 +311,25 @@ func (info *RelayInfo) ToString() string { // 定义支持流式选项的通道类型 var streamSupportedChannels = map[int]bool{ - constant.ChannelTypeOpenAI: true, - constant.ChannelTypeAnthropic: true, - constant.ChannelTypeAws: true, - constant.ChannelTypeGemini: true, - constant.ChannelCloudflare: true, - constant.ChannelTypeAzure: true, - constant.ChannelTypeVolcEngine: true, - constant.ChannelTypeOllama: true, - constant.ChannelTypeXai: true, - constant.ChannelTypeDeepSeek: true, - constant.ChannelTypeBaiduV2: true, - constant.ChannelTypeZhipu_v4: true, - constant.ChannelTypeAli: true, - constant.ChannelTypeSubmodel: true, - constant.ChannelTypeCodex: true, - constant.ChannelTypeMoonshot: true, - constant.ChannelTypeMiniMax: true, - constant.ChannelTypeSiliconFlow: true, + constant.ChannelTypeOpenAI: true, + constant.ChannelTypeAnthropic: true, + constant.ChannelTypeAws: true, + constant.ChannelTypeGemini: true, + constant.ChannelCloudflare: true, + constant.ChannelTypeAzure: true, + constant.ChannelTypeVolcEngine: true, + constant.ChannelTypePingXingShiJie: true, + constant.ChannelTypeOllama: true, + constant.ChannelTypeXai: true, + constant.ChannelTypeDeepSeek: true, + constant.ChannelTypeBaiduV2: true, + constant.ChannelTypeZhipu_v4: true, + constant.ChannelTypeAli: true, + constant.ChannelTypeSubmodel: true, + constant.ChannelTypeCodex: true, + constant.ChannelTypeMoonshot: true, + constant.ChannelTypeMiniMax: true, + constant.ChannelTypeSiliconFlow: true, } func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { @@ -487,7 +489,14 @@ func genBaseRelayInfo(c *gin.Context, request dto.Request) *RelayInfo { }, } - if info.RelayMode == relayconstant.RelayModeUnknown { + rmVal, rmExists := c.Get("relay_mode") + // Prefer relay_mode from Distribute() when set — Path2RelayMode may map GET /v1/images/generations/:id + // to RelayModeImagesGenerations, but task fetch must use RelayModeVideoFetchByID (see fetchRespBuilders). + if rmExists { + if v, ok := rmVal.(int); ok { + info.RelayMode = v + } + } else if info.RelayMode == relayconstant.RelayModeUnknown { info.RelayMode = c.GetInt("relay_mode") } @@ -680,6 +689,7 @@ type TaskSubmitReq struct { Image string `json:"image,omitempty"` Images []string `json:"images,omitempty"` Size string `json:"size,omitempty"` + Resolution string `json:"resolution,omitempty"` // 分辨率参数(如 720p/1080p) Duration int `json:"duration,omitempty"` Seconds string `json:"seconds,omitempty"` InputReference string `json:"input_reference,omitempty"` @@ -691,10 +701,40 @@ func (t *TaskSubmitReq) GetPrompt() string { } func (t *TaskSubmitReq) HasImage() bool { - return len(t.Images) > 0 + return len(t.Images) > 0 || strings.TrimSpace(t.Image) != "" } func (t *TaskSubmitReq) UnmarshalJSON(data []byte) error { + // "image" may be a single URL string (OpenAI-style) or a list of reference URLs (Seedream / client convention). + var raw map[string]json.RawMessage + if err := common.Unmarshal(data, &raw); err != nil { + return err + } + var imageURLs []string + var imageStr string + if imgRaw, ok := raw["image"]; ok { + imgRaw = bytes.TrimSpace(imgRaw) + if len(imgRaw) > 0 && string(imgRaw) != "null" { + switch imgRaw[0] { + case '[': + if err := common.Unmarshal(imgRaw, &imageURLs); err != nil { + return err + } + case '"': + if err := common.Unmarshal(imgRaw, &imageStr); err != nil { + return err + } + default: + return fmt.Errorf("json: field \"image\" must be a string or array of strings") + } + } + delete(raw, "image") + } + stitched, err := common.Marshal(raw) + if err != nil { + return err + } + type Alias TaskSubmitReq aux := &struct { Metadata json.RawMessage `json:"metadata,omitempty"` @@ -704,10 +744,20 @@ func (t *TaskSubmitReq) UnmarshalJSON(data []byte) error { Alias: (*Alias)(t), } - if err := common.Unmarshal(data, &aux); err != nil { + if err := common.Unmarshal(stitched, &aux); err != nil { return err } + if len(imageURLs) > 0 { + if len(t.Images) == 0 { + t.Images = imageURLs + } else { + t.Images = append(append([]string{}, t.Images...), imageURLs...) + } + } else if imageStr != "" { + t.Image = imageStr + } + if len(aux.Duration) > 0 { var durationInt int if err := common.Unmarshal(aux.Duration, &durationInt); err == nil { @@ -722,24 +772,56 @@ func (t *TaskSubmitReq) UnmarshalJSON(data []byte) error { } } + metadataObj := make(map[string]interface{}) + for key, rawValue := range raw { + if isTaskSubmitReqKnownJSONField(key) { + continue + } + var value interface{} + if err := common.Unmarshal(rawValue, &value); err != nil { + return err + } + metadataObj[key] = value + } + if len(aux.Metadata) > 0 { var metadataStr string if err := common.Unmarshal(aux.Metadata, &metadataStr); err == nil && metadataStr != "" { - var metadataObj map[string]interface{} - if err := common.Unmarshal([]byte(metadataStr), &metadataObj); err == nil { - t.Metadata = metadataObj + var explicitMetadata map[string]interface{} + if err := common.Unmarshal([]byte(metadataStr), &explicitMetadata); err == nil { + for key, value := range explicitMetadata { + metadataObj[key] = value + } + if len(metadataObj) > 0 { + t.Metadata = metadataObj + } return nil } } - var metadataObj map[string]interface{} - if err := common.Unmarshal(aux.Metadata, &metadataObj); err == nil { - t.Metadata = metadataObj + var explicitMetadata map[string]interface{} + if err := common.Unmarshal(aux.Metadata, &explicitMetadata); err == nil { + for key, value := range explicitMetadata { + metadataObj[key] = value + } } } + if len(metadataObj) > 0 { + t.Metadata = metadataObj + } return nil } + +func isTaskSubmitReqKnownJSONField(field string) bool { + switch field { + case "prompt", "model", "mode", "image", "images", "size", "resolution", "duration", "seconds", "input_reference", "metadata": + return true + default: + return false + } +} + func (t *TaskSubmitReq) UnmarshalMetadata(v any) error { metadata := t.Metadata if metadata != nil { diff --git a/relay/common/task_submit_req_test.go b/relay/common/task_submit_req_test.go new file mode 100644 index 000000000000..1628d2e30de8 --- /dev/null +++ b/relay/common/task_submit_req_test.go @@ -0,0 +1,121 @@ +package common + +import ( + "testing" + + "github.com/QuantumNous/new-api/common" +) + +func TestTaskSubmitReq_UnmarshalJSON_ImageArray(t *testing.T) { + const payload = `{ + "model": "doubao-seedream-4-5-251128", + "prompt": "test", + "image": ["https://example.com/a.jpg", "https://example.com/b.jpg"] + }` + var req TaskSubmitReq + if err := common.UnmarshalJsonStr(payload, &req); err != nil { + t.Fatal(err) + } + if len(req.Images) != 2 { + t.Fatalf("Images: got %d want 2", len(req.Images)) + } + if req.Images[0] != "https://example.com/a.jpg" { + t.Fatalf("unexpected first URL: %q", req.Images[0]) + } + if !req.HasImage() { + t.Fatal("HasImage() should be true") + } +} + +func TestTaskSubmitReq_UnmarshalJSON_ImageString(t *testing.T) { + const payload = `{"prompt":"x","image":"https://example.com/one.png"}` + var req TaskSubmitReq + if err := common.UnmarshalJsonStr(payload, &req); err != nil { + t.Fatal(err) + } + if req.Image != "https://example.com/one.png" { + t.Fatalf("Image: %q", req.Image) + } + if !req.HasImage() { + t.Fatal("HasImage() should be true for single image string") + } +} + +func TestTaskSubmitReq_UnmarshalJSON_PreservesProviderTopLevelFieldsAsMetadata(t *testing.T) { + const payload = `{ + "model": "doubao-seedance-1-5-pro-251215", + "prompt": "首帧过渡到尾帧", + "content": [ + {"type":"text","text":"首帧过渡到尾帧"}, + {"type":"image_url","image_url":{"url":"https://example.com/first.jpg"},"role":"first_frame"}, + {"type":"image_url","image_url":{"url":"https://example.com/last.jpg"},"role":"last_frame"} + ], + "generate_audio": true, + "ratio": "adaptive", + "duration": 6, + "watermark": false, + "resolution": "720p" + }` + var req TaskSubmitReq + if err := common.UnmarshalJsonStr(payload, &req); err != nil { + t.Fatal(err) + } + if req.Duration != 6 { + t.Fatalf("Duration: got %d want 6", req.Duration) + } + if req.Resolution != "720p" { + t.Fatalf("Resolution: got %q", req.Resolution) + } + content, ok := req.Metadata["content"].([]interface{}) + if !ok || len(content) != 3 { + t.Fatalf("metadata content: got %#v", req.Metadata["content"]) + } + second, ok := content[1].(map[string]interface{}) + if !ok || second["role"] != "first_frame" { + t.Fatalf("second content item: %#v", content[1]) + } + if req.Metadata["generate_audio"] != true { + t.Fatalf("generate_audio metadata: %#v", req.Metadata["generate_audio"]) + } + if req.Metadata["ratio"] != "adaptive" { + t.Fatalf("ratio metadata: %#v", req.Metadata["ratio"]) + } + if req.Metadata["watermark"] != false { + t.Fatalf("watermark metadata: %#v", req.Metadata["watermark"]) + } +} + +func TestTaskSubmitReq_UnmarshalJSON_PreservesKieTopLevelFields(t *testing.T) { + const payload = `{ + "model": "gpt-image-2-text-to-image", + "prompt": "make an image", + "aspect_ratio": "1:1", + "resolution": "4K" + }` + var req TaskSubmitReq + if err := common.UnmarshalJsonStr(payload, &req); err != nil { + t.Fatal(err) + } + if req.Resolution != "4K" { + t.Fatalf("Resolution: got %q want 4K", req.Resolution) + } + if req.Metadata["aspect_ratio"] != "1:1" { + t.Fatalf("aspect_ratio metadata = %#v", req.Metadata["aspect_ratio"]) + } +} + +func TestTaskSubmitReq_UnmarshalJSON_ExplicitMetadataOverridesProviderTopLevelFields(t *testing.T) { + const payload = `{ + "model": "gpt-image-2-text-to-image", + "prompt": "make an image", + "aspect_ratio": "1:1", + "metadata": {"aspect_ratio": "16:9"} + }` + var req TaskSubmitReq + if err := common.UnmarshalJsonStr(payload, &req); err != nil { + t.Fatal(err) + } + if req.Metadata["aspect_ratio"] != "16:9" { + t.Fatalf("aspect_ratio metadata = %#v", req.Metadata["aspect_ratio"]) + } +} diff --git a/relay/helper/stream_scanner.go b/relay/helper/stream_scanner.go index a9bc5e16a720..6c5323fc5f4a 100644 --- a/relay/helper/stream_scanner.go +++ b/relay/helper/stream_scanner.go @@ -40,8 +40,9 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon return } - // 无条件新建 StreamStatus - info.StreamStatus = relaycommon.NewStreamStatus() + if info.StreamStatus == nil { + info.StreamStatus = relaycommon.NewStreamStatus() + } // 确保响应体总是被关闭 defer func() { diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 3139c9a2dd4a..350299263f34 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -35,7 +35,9 @@ import ( taskGemini "github.com/QuantumNous/new-api/relay/channel/task/gemini" "github.com/QuantumNous/new-api/relay/channel/task/hailuo" taskjimeng "github.com/QuantumNous/new-api/relay/channel/task/jimeng" + taskkie "github.com/QuantumNous/new-api/relay/channel/task/kie" "github.com/QuantumNous/new-api/relay/channel/task/kling" + taskpxsj "github.com/QuantumNous/new-api/relay/channel/task/pingxingshijie" tasksora "github.com/QuantumNous/new-api/relay/channel/task/sora" "github.com/QuantumNous/new-api/relay/channel/task/suno" taskvertex "github.com/QuantumNous/new-api/relay/channel/task/vertex" @@ -153,6 +155,10 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor { return &taskVidu.TaskAdaptor{} case constant.ChannelTypeDoubaoVideo, constant.ChannelTypeVolcEngine: return &taskdoubao.TaskAdaptor{} + case constant.ChannelTypePingXingShiJie: + return &taskpxsj.TaskAdaptor{} + case constant.ChannelTypeKieAI: + return &taskkie.TaskAdaptor{} case constant.ChannelTypeSora, constant.ChannelTypeOpenAI: return &tasksora.TaskAdaptor{} case constant.ChannelTypeGemini: diff --git a/relay/relay_adaptor_test.go b/relay/relay_adaptor_test.go new file mode 100644 index 000000000000..dcfc4a3673ac --- /dev/null +++ b/relay/relay_adaptor_test.go @@ -0,0 +1,35 @@ +package relay + +import ( + "strconv" + "testing" + + "github.com/QuantumNous/new-api/constant" + taskkie "github.com/QuantumNous/new-api/relay/channel/task/kie" +) + +func TestGetTaskAdaptorReturnsKieAdaptor(t *testing.T) { + adaptor := GetTaskAdaptor(constant.TaskPlatform(strconv.Itoa(constant.ChannelTypeKieAI))) + if adaptor == nil { + t.Fatal("expected KieAI task adaptor") + } + if adaptor.GetChannelName() != taskkie.ChannelName { + t.Fatalf("channel name = %q", adaptor.GetChannelName()) + } + if len(adaptor.GetModelList()) == 0 { + t.Fatal("expected default Kie model list") + } +} + +func TestKieFallbackTaskModelsAreDetected(t *testing.T) { + platform := constant.TaskPlatform(strconv.Itoa(constant.ChannelTypeKieAI)) + if !isKieFallbackTaskModel("59_generate", platform, constant.TaskActionGenerate) { + t.Fatal("expected numeric task fallback model to be treated as Kie default") + } + if !isKieFallbackTaskModel("dall-e", platform, constant.TaskActionGenerate) { + t.Fatal("expected image fallback model to be treated as Kie default") + } + if isKieFallbackTaskModel(taskkie.ModelSeedance2, platform, constant.TaskActionGenerate) { + t.Fatal("expected concrete Kie model to stay unchanged") + } +} diff --git a/relay/relay_task.go b/relay/relay_task.go index 098e23828b6c..0c5a5e652964 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -14,6 +14,7 @@ import ( "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel" + taskkie "github.com/QuantumNous/new-api/relay/channel/task/kie" "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" @@ -163,6 +164,11 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe if modelName == "" { modelName = service.CoverTaskActionToModelName(platform, info.Action) } + if info.ChannelType == constant.ChannelTypeKieAI && isKieFallbackTaskModel(modelName, platform, info.Action) { + if taskReq, err := relaycommon.GetTaskRequest(c); err == nil { + modelName = taskkie.DefaultModelForRequest(info.RequestURLPath, taskReq.HasImage()) + } + } // 2.5 应用渠道的模型映射(与同步任务对齐) info.OriginModelName = modelName @@ -195,11 +201,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe // 6. 将 OtherRatios 应用到基础额度 if !common.StringsContains(constant.TaskPricePatches, modelName) { - for _, ra := range info.PriceData.OtherRatios { - if ra != 1.0 { - info.PriceData.Quota = int(float64(info.PriceData.Quota) * ra) - } - } + info.PriceData.Quota = applyOtherRatiosToQuota(info.PriceData.Quota, info.PriceData.OtherRatios) } // 7. 预扣费(仅首次 — 重试时 info.Billing 已存在,跳过) @@ -257,25 +259,44 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe }, nil } +func isKieFallbackTaskModel(modelName string, platform constant.TaskPlatform, action string) bool { + modelName = strings.TrimSpace(modelName) + if modelName == "" || modelName == "dall-e" { + return true + } + if modelName == service.CoverTaskActionToModelName(platform, action) { + return true + } + return modelName == taskkie.ChannelName+"_"+strings.ToLower(action) +} + // recalcQuotaFromRatios 根据 adjustedRatios 重新计算 quota。 // 公式: baseQuota × ∏(ratio) — 其中 baseQuota 是不含 OtherRatios 的基础额度。 func recalcQuotaFromRatios(info *relaycommon.RelayInfo, ratios map[string]float64) int { // 从 PriceData 获取不含 OtherRatios 的基础价格 - baseQuota := info.PriceData.Quota + baseQuota := float64(info.PriceData.Quota) // 先除掉原有的 OtherRatios 恢复基础额度 - for _, ra := range info.PriceData.OtherRatios { - if ra != 1.0 && ra > 0 { - baseQuota = int(float64(baseQuota) / ra) - } + oldMultiplier := otherRatiosProduct(info.PriceData.OtherRatios) + if oldMultiplier > 0 { + baseQuota /= oldMultiplier } // 应用新的 ratios - result := float64(baseQuota) + result := baseQuota * otherRatiosProduct(ratios) + return int(result) +} + +func applyOtherRatiosToQuota(quota int, ratios map[string]float64) int { + return int(float64(quota) * otherRatiosProduct(ratios)) +} + +func otherRatiosProduct(ratios map[string]float64) float64 { + multiplier := 1.0 for _, ra := range ratios { - if ra != 1.0 { - result *= ra + if ra != 1.0 && ra > 0 { + multiplier *= ra } } - return int(result) + return multiplier } var fetchRespBuilders = map[int]func(c *gin.Context) (respBody []byte, taskResp *dto.TaskError){ @@ -286,8 +307,9 @@ var fetchRespBuilders = map[int]func(c *gin.Context) (respBody []byte, taskResp func RelayTaskFetch(c *gin.Context, relayMode int) (taskResp *dto.TaskError) { respBuilder, ok := fetchRespBuilders[relayMode] - if !ok { + if !ok || respBuilder == nil { taskResp = service.TaskErrorWrapperLocal(errors.New("invalid_relay_mode"), "invalid_relay_mode", http.StatusBadRequest) + return } respBody, taskErr := respBuilder(c) @@ -361,12 +383,16 @@ func sunoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *dt func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *dto.TaskError) { taskId := c.Param("task_id") + if taskId == "" { + taskId = c.Param("asset_id") + } if taskId == "" { taskId = c.GetString("task_id") } userId := c.GetInt("id") + path := c.Request.URL.Path - originTask, exist, err := model.GetByTaskId(userId, taskId) + originTask, exist, err := getTaskForVideoFetch(userId, taskId, path) if err != nil { taskResp = service.TaskErrorWrapper(err, "get_task_failed", http.StatusInternalServerError) return @@ -376,7 +402,7 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d return } - isOpenAIVideoAPI := strings.HasPrefix(c.Request.RequestURI, "/v1/videos/") + isOpenAIVideoAPI := strings.HasPrefix(path, "/v1/videos/") || strings.HasPrefix(path, "/v1/video/generations/") // Gemini/Vertex 支持实时查询:用户 fetch 时直接从上游拉取最新状态 if realtimeResp := tryRealtimeFetch(originTask, isOpenAIVideoAPI); len(realtimeResp) > 0 { @@ -384,6 +410,43 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d return } + // PingXingShiJie async image task (GET /v1/images/generations/:task_id) + uk := originTask.PrivateData.UpstreamKind + if strings.HasPrefix(path, "/v1/images/generations/") && (uk == "" || uk == "image") { + adaptor := GetTaskAdaptor(originTask.Platform) + if adaptor == nil { + taskResp = service.TaskErrorWrapperLocal(fmt.Errorf("invalid channel id: %d", originTask.ChannelId), "invalid_channel_id", http.StatusBadRequest) + return + } + if converter, ok := adaptor.(channel.OpenAIAsyncImageConverter); ok { + data, err := converter.ConvertToOpenAIAsyncImage(originTask) + if err != nil { + taskResp = service.TaskErrorWrapper(err, "convert_to_openai_async_image_failed", http.StatusInternalServerError) + return + } + respBody = data + return + } + } + + // PingXingShiJie asset task (GET /v1/assets/:asset_id) + if strings.HasPrefix(path, "/v1/assets/") && (uk == "" || uk == "asset") { + adaptor := GetTaskAdaptor(originTask.Platform) + if adaptor == nil { + taskResp = service.TaskErrorWrapperLocal(fmt.Errorf("invalid channel id: %d", originTask.ChannelId), "invalid_channel_id", http.StatusBadRequest) + return + } + if converter, ok := adaptor.(channel.OpenAIAssetTaskConverter); ok { + data, err := converter.ConvertToOpenAIAssetTask(originTask) + if err != nil { + taskResp = service.TaskErrorWrapper(err, "convert_to_openai_asset_task_failed", http.StatusInternalServerError) + return + } + respBody = data + return + } + } + // OpenAI Video API 格式: 走各 adaptor 的 ConvertToOpenAIVideo if isOpenAIVideoAPI { adaptor := GetTaskAdaptor(originTask.Platform) @@ -415,6 +478,15 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d return } +func getTaskForVideoFetch(userId int, id string, path string) (*model.Task, bool, error) { + if strings.HasPrefix(path, "/v1/assets/") { + if task, exist, err := model.GetByUpstreamTaskId(userId, id); err != nil || exist { + return task, exist, err + } + } + return model.GetByTaskId(userId, id) +} + // tryRealtimeFetch 尝试从上游实时拉取 Gemini/Vertex 任务状态。 // 仅当渠道类型为 Gemini 或 Vertex 时触发;其他渠道或出错时返回 nil。 // 当非 OpenAI Video API 时,还会构建自定义格式的响应体。 @@ -437,10 +509,14 @@ func tryRealtimeFetch(task *model.Task, isOpenAIVideoAPI bool) []byte { return nil } - resp, err := adaptor.FetchTask(baseURL, channelModel.Key, map[string]any{ + ft := map[string]any{ "task_id": task.GetUpstreamTaskID(), "action": task.Action, - }, proxy) + } + if task.PrivateData.UpstreamKind != "" { + ft["upstream_kind"] = task.PrivateData.UpstreamKind + } + resp, err := adaptor.FetchTask(baseURL, channelModel.Key, ft, proxy) if err != nil || resp == nil { return nil } @@ -540,25 +616,26 @@ func mapTaskStatusToSimple(status model.TaskStatus) string { func TaskModel2Dto(task *model.Task) *dto.TaskDto { return &dto.TaskDto{ - ID: task.ID, - CreatedAt: task.CreatedAt, - UpdatedAt: task.UpdatedAt, - TaskID: task.TaskID, - Platform: string(task.Platform), - UserId: task.UserId, - Group: task.Group, - ChannelId: task.ChannelId, - Quota: task.Quota, - Action: task.Action, - Status: string(task.Status), - FailReason: task.FailReason, - ResultURL: task.GetResultURL(), - SubmitTime: task.SubmitTime, - StartTime: task.StartTime, - FinishTime: task.FinishTime, - Progress: task.Progress, - Properties: task.Properties, - Username: task.Username, - Data: task.Data, + ID: task.ID, + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt, + TaskID: task.TaskID, + Platform: string(task.Platform), + UserId: task.UserId, + Group: task.Group, + ChannelId: task.ChannelId, + Quota: task.Quota, + Action: task.Action, + Status: string(task.Status), + FailReason: task.FailReason, + ResultURL: task.GetResultURL(), + UpstreamKind: task.PrivateData.UpstreamKind, + SubmitTime: task.SubmitTime, + StartTime: task.StartTime, + FinishTime: task.FinishTime, + Progress: task.Progress, + Properties: task.Properties, + Username: task.Username, + Data: task.Data, } } diff --git a/relay/relay_task_test.go b/relay/relay_task_test.go new file mode 100644 index 000000000000..754e40826fcf --- /dev/null +++ b/relay/relay_task_test.go @@ -0,0 +1,73 @@ +package relay + +import ( + "testing" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + "github.com/glebarez/sqlite" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestApplyOtherRatiosToQuotaMultipliesBeforeTruncating(t *testing.T) { + ratios := map[string]float64{ + "resolution": 51.0 / 46.0, + "video_input": 31.0 / 51.0, + } + + if got := applyOtherRatiosToQuota(46, ratios); got != 31 { + t.Fatalf("quota: got %d want 31", got) + } +} + +func TestRecalcQuotaFromRatiosMultipliesBeforeTruncating(t *testing.T) { + info := &relaycommon.RelayInfo{ + PriceData: types.PriceData{ + Quota: 46, + }, + } + ratios := map[string]float64{ + "resolution": 51.0 / 46.0, + "video_input": 31.0 / 51.0, + } + + if got := recalcQuotaFromRatios(info, ratios); got != 31 { + t.Fatalf("quota: got %d want 31", got) + } +} + +func TestGetTaskForVideoFetchFindsAssetByUpstreamAssetID(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, db.AutoMigrate(&model.Task{})) + oldDB := model.DB + oldUsingSQLite := common.UsingSQLite + model.DB = db + common.UsingSQLite = true + t.Cleanup(func() { + model.DB = oldDB + common.UsingSQLite = oldUsingSQLite + }) + + createdAt := time.Now().Unix() + require.NoError(t, db.Create(&model.Task{ + TaskID: "task_public_asset", + UserId: 7, + CreatedAt: createdAt, + UpdatedAt: createdAt, + PrivateData: model.TaskPrivateData{ + UpstreamTaskID: "asset-20260319082447-qrrjp", + UpstreamKind: "asset", + }, + }).Error) + + task, exist, err := getTaskForVideoFetch(7, "asset-20260319082447-qrrjp", "/v1/assets/asset-20260319082447-qrrjp") + require.NoError(t, err) + require.True(t, exist) + require.NotNil(t, task) + require.Equal(t, "task_public_asset", task.TaskID) +} diff --git a/router/video-router.go b/router/video-router.go index 461451104520..5ab9a1e95810 100644 --- a/router/video-router.go +++ b/router/video-router.go @@ -22,6 +22,10 @@ func SetVideoRouter(router *gin.Engine) { { videoV1Router.POST("/video/generations", controller.RelayTask) videoV1Router.GET("/video/generations/:task_id", controller.RelayTaskFetch) + videoV1Router.POST("/images/generations/async", controller.RelayTask) + videoV1Router.GET("/images/generations/:task_id", controller.RelayTaskFetch) + videoV1Router.POST("/assets/upload", controller.RelayTask) + videoV1Router.GET("/assets/:asset_id", controller.RelayTaskFetch) videoV1Router.POST("/videos/:video_id/remix", controller.RelayTask) } // openai compatible API video routes diff --git a/service/file_service.go b/service/file_service.go index bcf4744224fd..90323d223716 100644 --- a/service/file_service.go +++ b/service/file_service.go @@ -349,6 +349,14 @@ func loadFromBase64(base64String string, providedMimeType string) (*types.Cached if err != nil { return nil, fmt.Errorf("failed to decode base64 data: %w", err) } + if mimeType == "" { + if sniffed := http.DetectContentType(decodedData); sniffed != "" && sniffed != "application/octet-stream" { + if idx := strings.Index(sniffed, ";"); idx != -1 { + sniffed = sniffed[:idx] + } + mimeType = sniffed + } + } base64Size := int64(len(cleanBase64)) var cachedData *types.CachedFileData diff --git a/service/task_polling.go b/service/task_polling.go index dc85e579e8cc..810b856d93bd 100644 --- a/service/task_polling.go +++ b/service/task_polling.go @@ -359,10 +359,14 @@ func updateVideoSingleTask(ctx context.Context, adaptor TaskPollingAdaptor, ch * if privateData.Key != "" { key = privateData.Key } - resp, err := adaptor.FetchTask(baseURL, key, map[string]any{ + fetchBody := map[string]any{ "task_id": task.GetUpstreamTaskID(), "action": task.Action, - }, proxy) + } + if task.PrivateData.UpstreamKind != "" { + fetchBody["upstream_kind"] = task.PrivateData.UpstreamKind + } + resp, err := adaptor.FetchTask(baseURL, key, fetchBody, proxy) if err != nil { return fmt.Errorf("fetchTask failed for task %s: %w", taskId, err) } @@ -445,6 +449,13 @@ func updateVideoSingleTask(ctx context.Context, adaptor TaskPollingAdaptor, ch * } else if taskResult.Url != "" { // Direct upstream URL (e.g. Kling, Ali, Doubao, etc.) task.PrivateData.ResultURL = taskResult.Url + } else if task.PrivateData.UpstreamKind == "image" { + // Image async: first success poll may have empty TaskInfo.Url; parse URL from raw response before video proxy fallback. + if imgURL := model.ExtractImageURLFromJSONBytes(responseBody); imgURL != "" { + task.PrivateData.ResultURL = imgURL + } else { + task.PrivateData.ResultURL = taskcommon.BuildProxyURL(task.TaskID) + } } else { // No URL from adaptor — construct proxy URL using public task ID task.PrivateData.ResultURL = taskcommon.BuildProxyURL(task.TaskID) diff --git a/web/classic/bun.lock b/web/classic/bun.lock index da3c1e452a9d..8976536a2a39 100644 --- a/web/classic/bun.lock +++ b/web/classic/bun.lock @@ -1,15 +1,17 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "react-template", "dependencies": { "@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-ui": "^2.69.1", - "@lobehub/icons": "^2.0.0", - "@visactor/react-vchart": "~1.8.8", - "@visactor/vchart": "~1.8.8", - "@visactor/vchart-semi-theme": "~1.8.8", + "@lobehub/icons": "^2.48.0", + "@visactor/react-vchart": "^2.0.21", + "@visactor/vchart": "^2.0.21", + "@visactor/vchart-semi-theme": "^1.12.3", + "antd": "^5.24.0", "axios": "1.15.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", @@ -192,6 +194,8 @@ "@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="], + "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], + "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], @@ -262,15 +266,15 @@ "@eslint/js": ["@eslint/js@8.57.0", "", {}, "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g=="], - "@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], - "@floating-ui/react": ["@floating-ui/react@0.27.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.9", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-EQJ4Th328y2wyHR3KzOUOoTW2UKjFk53fmyahfwExnFQ8vnsMYqKc+fFPOkeYtj5tcp1DUMiNJ7BFhed7e9ONw=="], + "@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="], @@ -340,13 +344,13 @@ "@lobehub/fluent-emoji": ["@lobehub/fluent-emoji@2.0.0", "", { "dependencies": { "@lobehub/emojilib": "^1.0.0", "@lobehub/ui": "^2.0.0", "antd-style": "^3.7.1", "emoji-regex": "^10.4.0", "lodash-es": "^4.17.21", "lucide-react": "^0.469.0", "react-layout-kit": "^1.9.1", "url-join": "^5.0.0" }, "peerDependencies": { "antd": "^5.23.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-bKjU3sf0+7NppvcdqD/raWvKGJIw8HDJVporNQ7oR8pIPoLeb9IUu/vqIYClOlwfu9qntji7FFySfbdNqXSiJw=="], - "@lobehub/icons": ["@lobehub/icons@2.1.0", "", { "dependencies": { "@lobehub/ui": "^2.0.0", "antd-style": "^3.7.1", "lucide-react": "^0.469.0", "polished": "^4.3.1", "react-layout-kit": "^1.9.1" }, "peerDependencies": { "antd": "^5.23.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-iHtIp8a05/YHxTDlOFXCTfvYXUjKi1Mbq5a9qsEN+zwJ5U+mR2WgKz5zUausIzZiMZo+P3pgxbhh3/eHf7Q1pw=="], + "@lobehub/icons": ["@lobehub/icons@2.48.0", "", { "dependencies": { "@lobehub/ui": "^2.24.1", "antd-style": "^3.7.1", "lucide-react": "^0.469.0", "polished": "^4.3.1", "react-layout-kit": "^2.0.1" }, "peerDependencies": { "antd": "^5.23.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-BeLa3pG0Bj1jUozN7k10oWqyfkCA5p5eBzMyfd1Y/WzUSyDwDOrPWtcIpLSS+Y8/4BLMiMJtWR/dQ/UXrA1+DA=="], - "@lobehub/ui": ["@lobehub/ui@2.1.10", "", { "dependencies": { "@ant-design/cssinjs": "^1.23.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@floating-ui/react": "^0.27.5", "@giscus/react": "^3.1.0", "@lobehub/fluent-emoji": "^2.0.0", "@lobehub/icons": "^2.0.0", "@mdx-js/mdx": "^3.1.0", "@mdx-js/react": "^3.1.0", "@radix-ui/react-slot": "^1.1.2", "@shikijs/transformers": "^3.2.1", "@splinetool/runtime": "0.9.526", "ahooks": "^3.8.4", "antd-style": "^3.7.1", "chroma-js": "^3.1.2", "class-variance-authority": "^0.7.1", "dayjs": "^1.11.13", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "framer-motion": "^12.6.3", "immer": "^10.1.1", "katex": "^0.16.9", "leva": "^0.10.0", "lodash-es": "^4.17.21", "lucide-react": "^0.484.0", "mermaid": "^11.6.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.1.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.11.1", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^13.0.2", "react-error-boundary": "^5.0.0", "react-hotkeys-hook": "^5.1.0", "react-layout-kit": "^1.9.1", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.2.1", "swr": "^2.3.3", "ts-md5": "^1.3.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^11.1.0" }, "peerDependencies": { "antd": "^5.25.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-R1/t5I8UAjvd5xoEDJXg6RzHmwhdOU45JQN297MlYB/sGqcvySfQL9POpDmySSs+QMyjkhwhum254cfXFKJIZA=="], + "@lobehub/ui": ["@lobehub/ui@2.25.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@floating-ui/react": "^0.27.16", "@giscus/react": "^3.1.0", "@lobehub/fluent-emoji": "^2.0.0", "@lobehub/icons": "^2.48.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^3.20.0", "@shikijs/transformers": "^3.20.0", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.6", "antd-style": "^3.7.1", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "dayjs": "^1.11.19", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "immer": "^11.0.1", "katex": "^0.16.27", "leva": "^0.10.1", "lodash-es": "^4.17.22", "lucide-react": "^0.562.0", "marked": "^17.0.1", "mermaid": "^11.12.2", "motion": "^12.23.26", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^14.0.0", "react-error-boundary": "^6.0.0", "react-hotkeys-hook": "^5.2.1", "react-layout-kit": "^2.0.1", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "shiki": "^3.20.0", "shiki-stream": "^0.1.3", "swr": "^2.3.8", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0" }, "peerDependencies": { "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-gH0OXgBPg1DtVvMP3FOkGyFw+VgCFFfOwy4G+sSCCmbDzpIHE6CH0dEJYCdPQqTaRMNN1h3FgyabUwEdlUvDCQ=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="], - "@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="], + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], "@mermaid-js/parser": ["@mermaid-js/parser@0.4.0", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA=="], @@ -386,45 +390,49 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], + "@primer/octicons": ["@primer/octicons@19.23.1", "", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-CzjGmxkmNhyst6EekrS3SJPdtzgIkUMP/LSJch65y99/kmiFXbO1a+q7zoYe3hnI9NaOM0IN+ydDIbOmd8YqcA=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-escape-keydown": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "0.7.2", "@radix-ui/react-arrow": "1.0.2", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0", "@radix-ui/react-use-rect": "1.0.0", "@radix-ui/react-use-size": "1.0.0", "@radix-ui/rect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4kY9IVa6+9nJPsYmngK5Uk2kUmZnv7ChhHAFeQ5oaj8jrR1bIi3xww8nH71pz1/Ve4d/cXO3YxT8eikt1B0a8w=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], - "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.0.5", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-dismissable-layer": "1.0.3", "@radix-ui/react-id": "1.0.0", "@radix-ui/react-popper": "1.1.1", "@radix-ui/react-portal": "1.0.2", "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-slot": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.0", "@radix-ui/react-visually-hidden": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/rect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg=="], + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], - "@radix-ui/rect": ["@radix-ui/rect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@rc-component/async-validator": ["@rc-component/async-validator@5.0.4", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg=="], @@ -444,6 +452,8 @@ "@rc-component/trigger": ["@rc-component/trigger@2.2.6", "", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q=="], + "@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + "@remix-run/router": ["@remix-run/router@1.21.0", "", {}, "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA=="], "@resvg/resvg-js": ["@resvg/resvg-js@2.4.1", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.4.1", "@resvg/resvg-js-android-arm64": "2.4.1", "@resvg/resvg-js-darwin-arm64": "2.4.1", "@resvg/resvg-js-darwin-x64": "2.4.1", "@resvg/resvg-js-linux-arm-gnueabihf": "2.4.1", "@resvg/resvg-js-linux-arm64-gnu": "2.4.1", "@resvg/resvg-js-linux-arm64-musl": "2.4.1", "@resvg/resvg-js-linux-x64-gnu": "2.4.1", "@resvg/resvg-js-linux-x64-musl": "2.4.1", "@resvg/resvg-js-win32-arm64-msvc": "2.4.1", "@resvg/resvg-js-win32-ia32-msvc": "2.4.1", "@resvg/resvg-js-win32-x64-msvc": "2.4.1" } }, "sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A=="], @@ -512,19 +522,19 @@ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - "@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], + "@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ=="], + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q=="], + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - "@shikijs/langs": ["@shikijs/langs@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA=="], + "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], - "@shikijs/themes": ["@shikijs/themes@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg=="], + "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - "@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], + "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], - "@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], + "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], @@ -660,6 +670,8 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], + "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -680,47 +692,39 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.2.1", "", {}, "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + "@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="], "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="], - "@visactor/react-vchart": ["@visactor/react-vchart@1.8.11", "", { "dependencies": { "@visactor/vchart": "1.8.11", "@visactor/vgrammar-core": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vutils": "~0.17.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-wHnCex9gOpnttTtSu04ozKJhTveUk8Ln2KX/7PZyCJxqlXq+eWvW4zvM6Ja8T8kGXfXtFYVVNh9zBMQ7y2T/Sw=="], - - "@visactor/vchart": ["@visactor/vchart@1.8.11", "", { "dependencies": { "@visactor/vdataset": "~0.17.3", "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-hierarchy": "0.10.11", "@visactor/vgrammar-projection": "0.10.11", "@visactor/vgrammar-sankey": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vgrammar-wordcloud": "0.10.11", "@visactor/vgrammar-wordcloud-shape": "0.10.11", "@visactor/vrender-components": "0.17.17", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3", "@visactor/vutils-extension": "1.8.11" } }, "sha512-RdQ822J02GgAQNXvO1LiT0T3O6FjdgPdcm9hVBFyrpBBmuI8MH02IE7Y1kGe9NiFTH4tDwP0ixRgBmqNSGSLZQ=="], - - "@visactor/vchart-semi-theme": ["@visactor/vchart-semi-theme@1.8.8", "", { "dependencies": { "@visactor/vchart-theme-utils": "1.8.8" }, "peerDependencies": { "@visactor/vchart": "~1.8.8" } }, "sha512-lm57CX3r6Bm7iGBYYyWhDY+1BvkyhNVLEckKx2PnlPKpJHikKSIK2ACyI5SmHuSOOdYzhY2QK6ZfYa2NShJ83w=="], - - "@visactor/vchart-theme-utils": ["@visactor/vchart-theme-utils@1.8.8", "", { "peerDependencies": { "@visactor/vchart": "~1.8.8" } }, "sha512-RdCey3/t0+82EYyFZvx210rgJJWti9rsgcL3ROZS7o9CtRW1CMj9u9LKLDNIcPLNcLNACFC0aoT03jpdD1BCpA=="], + "@visactor/react-vchart": ["@visactor/react-vchart@2.0.21", "", { "dependencies": { "@visactor/vchart": "2.0.21", "@visactor/vchart-extension": "2.0.21", "@visactor/vrender-core": "1.0.45", "@visactor/vrender-kits": "1.0.45", "@visactor/vutils": "~1.0.23", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-1qgPlW820ZQEbemKiddLRx+JIoEdVUAF4pEyIEAIYoh4W49RPpa8kncMTgKw7030gs3lGCQneqIeLoZMWfZJtA=="], - "@visactor/vdataset": ["@visactor/vdataset@0.17.5", "", { "dependencies": { "@turf/flatten": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/rewind": "^6.5.0", "@visactor/vutils": "0.17.5", "d3-dsv": "^2.0.0", "d3-geo": "^1.12.1", "d3-hexbin": "^0.2.2", "d3-hierarchy": "^3.1.1", "eventemitter3": "^4.0.7", "geobuf": "^3.0.1", "geojson-dissolve": "^3.1.0", "path-browserify": "^1.0.1", "pbf": "^3.2.1", "point-at-length": "^1.1.0", "simple-statistics": "^7.7.3", "simplify-geojson": "^1.0.4", "topojson-client": "^3.1.0" } }, "sha512-zVBdLWHWrhldGc8JDjSYF9lvpFT4ZEFQDB0b6yvfSiHzHKHiSco+rWmUFvA7r4ObT6j2QWF1vZAV9To8Ml4vHw=="], + "@visactor/vchart": ["@visactor/vchart@2.0.21", "", { "dependencies": { "@visactor/vdataset": "~1.0.23", "@visactor/vlayouts": "~1.0.23", "@visactor/vrender-animate": "1.0.45", "@visactor/vrender-components": "1.0.45", "@visactor/vrender-core": "1.0.45", "@visactor/vrender-kits": "1.0.45", "@visactor/vscale": "~1.0.23", "@visactor/vutils": "~1.0.23", "@visactor/vutils-extension": "2.0.21" } }, "sha512-jYrSwTS8EkV2qB3c+/iW9YBta4dCJSsbuxQzacHnhVuH2XAn7b+33wLdFHbG1h84L1EbVPMle6LrUoXe+CZjYw=="], - "@visactor/vgrammar-coordinate": ["@visactor/vgrammar-coordinate@0.10.11", "", { "dependencies": { "@visactor/vgrammar-util": "0.10.11", "@visactor/vutils": "~0.17.3" } }, "sha512-XSUvEkaf/NQHFafmTwqoIMZicp9fF3o6NB2FDpuWrK4DI1lTuip/0RkqrC+kBAjc5erjt0em0TiITyqXpp4G6w=="], + "@visactor/vchart-extension": ["@visactor/vchart-extension@2.0.21", "", { "dependencies": { "@visactor/vchart": "2.0.21", "@visactor/vdataset": "~1.0.23", "@visactor/vlayouts": "~1.0.23", "@visactor/vrender-animate": "1.0.45", "@visactor/vrender-components": "1.0.45", "@visactor/vrender-core": "1.0.45", "@visactor/vrender-kits": "1.0.45", "@visactor/vutils": "~1.0.23" } }, "sha512-c930SfptDFmRUVrc2tf5sX3uw6EOg7eUFHENYlMmN80ufjoSUqX6MqOXgK/fsZh3lnQi3vzBQI6+9TtY81T4wg=="], - "@visactor/vgrammar-core": ["@visactor/vgrammar-core@0.10.11", "", { "dependencies": { "@visactor/vdataset": "~0.17.3", "@visactor/vgrammar-coordinate": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-components": "0.17.17", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3" } }, "sha512-VL9vcLPDg1LrHl7EOx0Ga9ATsoaChKIaCGzxjrPEjWiIS5VPU9Rs0jBKP+ch8BjamAoSuqL5mKd0L/RaUBqlaA=="], + "@visactor/vchart-semi-theme": ["@visactor/vchart-semi-theme@1.12.3", "", { "dependencies": { "@visactor/vchart-theme-utils": "1.12.3" }, "peerDependencies": { "@visactor/vchart": ">=1.10.4" } }, "sha512-px5cA7fEEiu/+mblZgR0iZtrjDNTF3urW1hMym/dhNI5IhEplFhnED0xEyxSBD26hEkUd4iNVPwSpL8hOPPX/w=="], - "@visactor/vgrammar-hierarchy": ["@visactor/vgrammar-hierarchy@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vutils": "~0.17.3" } }, "sha512-0r3k51pPlJHu63BduG3htsV/ul62aVcKJxFftRfvKkwGjm1KeHoOZEEAwIf78U2puio0BkLqVn2Ek2L4FYZaIg=="], + "@visactor/vchart-theme-utils": ["@visactor/vchart-theme-utils@1.12.3", "", { "peerDependencies": { "@visactor/vchart": ">=1.10.4" } }, "sha512-lgzxn9wPjgVLictneowoYPY8FSBN5mmORnlJ3epYNKlNUNpderfvb9qIZhgAXw/5xrpl1HZTzMOQRjLT3P9wfQ=="], - "@visactor/vgrammar-projection": ["@visactor/vgrammar-projection@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vutils": "~0.17.3", "d3-geo": "^1.12.1" } }, "sha512-yEiKsxdfs5+g60wv5xZ1kyS/EDrAsUzAxCMpFFASVUYbQObHvW+elm+UPq2TBX6KZqAM0gsd1inzaLvfsCrLSg=="], + "@visactor/vdataset": ["@visactor/vdataset@1.0.23", "", { "dependencies": { "@turf/flatten": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/rewind": "^6.5.0", "@visactor/vutils": "1.0.23", "d3-dsv": "^2.0.0", "d3-geo": "^1.12.1", "d3-hexbin": "^0.2.2", "d3-hierarchy": "^3.1.1", "eventemitter3": "^4.0.7", "geobuf": "^3.0.1", "geojson-dissolve": "^3.1.0", "path-browserify": "^1.0.1", "pbf": "^3.2.1", "point-at-length": "^1.1.0", "simple-statistics": "^7.7.3", "simplify-geojson": "^1.0.4", "topojson-client": "^3.1.0" } }, "sha512-zrLk9FBUWJoW6b30XnPKzXwAXl8USdLDfed6QZLsmdkylRU8V7yZeXE2aKwU8Lg1U4HmQngqmqOx7/QlbX44Tg=="], - "@visactor/vgrammar-sankey": ["@visactor/vgrammar-sankey@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vutils": "~0.17.3" } }, "sha512-BbJTPuyydsL/L5XtQv59Q82GgJeePY7Wleac798usx3GnDK0GAOrPsI3bubSsOESJ4pNk3V4HPGEQDG1vCPb4w=="], + "@visactor/vlayouts": ["@visactor/vlayouts@1.0.23", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@visactor/vscale": "1.0.23", "@visactor/vutils": "1.0.23", "eventemitter3": "^4.0.7" } }, "sha512-fK1f5LmuumhYanLArk5yrT4BZxu4IAmdc8WMwfB/KAvV+2dTPFuBUMWbWnDl0siQoU9SX9l/bLozUnI9n7BwBQ=="], - "@visactor/vgrammar-util": ["@visactor/vgrammar-util@0.10.11", "", { "dependencies": { "@visactor/vutils": "~0.17.3" } }, "sha512-cJZLmKZvN95Y+yGhX+28+UpZu3bhYYlXDlHJNvXHyonI76ZYgtceyon2b3lI6XIsUsBGcD4Uo777s949X5os3g=="], + "@visactor/vrender-animate": ["@visactor/vrender-animate@1.0.45", "", { "dependencies": { "@visactor/vrender-core": "1.0.45", "@visactor/vutils": "~1.0.12" } }, "sha512-6v7LRpr+zugxR8JH3RCnodYxrrzHkSp4GaBTNwXuJ7bwThi/YzXTvmBz2pfQNmUG/rRze8Bm7vqneHVdXuFhng=="], - "@visactor/vgrammar-wordcloud": ["@visactor/vgrammar-wordcloud@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vutils": "~0.17.3" } }, "sha512-JWDqjGhr9JlYkKVBeEkiOqLQk7C1x1BtnsZ+E8oN541gzUqHwfS9qZyhwI3OyoSLewJlsSSPu1vXLKSQzLzKPA=="], + "@visactor/vrender-components": ["@visactor/vrender-components@1.0.45", "", { "dependencies": { "@visactor/vrender-animate": "1.0.45", "@visactor/vrender-core": "1.0.45", "@visactor/vrender-kits": "1.0.45", "@visactor/vscale": "~1.0.12", "@visactor/vutils": "~1.0.12" } }, "sha512-rYsG/rncT5FgYYWqv09f6LkZEAk/IS45IEYWBD0SCgKguYnpEebYDmDba1muJGn+rRv/vGlqg1pYqXLw8mEU5w=="], - "@visactor/vgrammar-wordcloud-shape": ["@visactor/vgrammar-wordcloud-shape@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3" } }, "sha512-NsQOYJp+9WHnIApMvkcUOaajxIg5U/r6rD8LKnoXW/HqAN2TFYXcRR3Daqmk9rrpM5VztQimKOsA1yZWyzozrA=="], + "@visactor/vrender-core": ["@visactor/vrender-core@1.0.45", "", { "dependencies": { "@visactor/vutils": "~1.0.12", "color-convert": "2.0.1" } }, "sha512-kvYAsKGZ+dXbhOQzjjbMkRzI815KgaHoWOW7iyVorXTldQBTsxLtFIi/J3VfYqGdM3apUgsFoy8uvjS5DepPUA=="], - "@visactor/vrender-components": ["@visactor/vrender-components@0.17.17", "", { "dependencies": { "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3" } }, "sha512-7gYFQrozvBkyGF7s/JHXdWDZnATzymxzug63CZd4EB7A0OXKatVDImXRePqwzlPD3QamF7QMVWn0CuIx3gQ2gA=="], + "@visactor/vrender-kits": ["@visactor/vrender-kits@1.0.45", "", { "dependencies": { "@resvg/resvg-js": "2.4.1", "@visactor/vrender-core": "1.0.45", "@visactor/vutils": "~1.0.12", "gifuct-js": "2.1.2", "lottie-web": "^5.12.2", "roughjs": "4.6.6" } }, "sha512-iaeRitht8IqNvJwdKmcPKAYL0a4+f8xhK2bWUumSQjNbZJQx9UzGuDp73cIdNXzCNoCiJZElpsOiIGu3kymqiw=="], - "@visactor/vrender-core": ["@visactor/vrender-core@0.17.17", "", { "dependencies": { "@visactor/vutils": "~0.17.3", "color-convert": "2.0.1" } }, "sha512-pAZGaimunDAWOBdFhzPh0auH5ryxAHr+MVoz+QdASG+6RZXy8D02l8v2QYu4+e4uorxe/s2ZkdNDm81SlNkoHQ=="], + "@visactor/vscale": ["@visactor/vscale@1.0.23", "", { "dependencies": { "@visactor/vutils": "1.0.23" } }, "sha512-XePhYuRoNAp+8MeSMuEOOvhVAlOwvM1sDT2yFxE6zdwVB2GjZk8mH+5N2xQGQWk75YmGJjlJASFtgwjlb1yWxw=="], - "@visactor/vrender-kits": ["@visactor/vrender-kits@0.17.17", "", { "dependencies": { "@resvg/resvg-js": "2.4.1", "@visactor/vrender-core": "0.17.17", "@visactor/vutils": "~0.17.3", "roughjs": "4.5.2" } }, "sha512-noRP1hAHvPCv36nf2P6sZ930Tk+dJ8jpPWIUm1cFYmUNdcumgIS8Cug0RyeZ+saSqVt5FDTwIwifhOqupw5Zaw=="], + "@visactor/vutils": ["@visactor/vutils@1.0.23", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "eventemitter3": "^4.0.7" } }, "sha512-M8SLqgdHhKN8QmQKTWD1gzEaHptpIV9pvMYvC6+VeOsqYvZZ6UdhSCAAczTYVo+m/uwcEC2JHSUspbrs8rzlRQ=="], - "@visactor/vscale": ["@visactor/vscale@0.17.5", "", { "dependencies": { "@visactor/vutils": "0.17.5" } }, "sha512-2dkS1IlAJ/IdTp8JElbctOOv6lkHKBKPDm8KvwBo0NuGWQeYAebSeyN3QCdwKbj76gMlCub4zc+xWrS5YiA2zA=="], - - "@visactor/vutils": ["@visactor/vutils@0.17.5", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "eventemitter3": "^4.0.7" } }, "sha512-HFN6Pk1Wc1RK842g02MeKOlvdri5L7/nqxMVTqxIvi0XMhHXpmoqN4+/9H+h8LmJpVohyrI/MT85TRBV/rManw=="], - - "@visactor/vutils-extension": ["@visactor/vutils-extension@1.8.11", "", { "dependencies": { "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3" } }, "sha512-Hknzpy3+xh4sdL0iSn5N93BHiMJF4FdwSwhHYEibRpriZmWKG6wBxsJ0Bll4d7oS4f+svxt8Sg2vRYKzQEcIxQ=="], + "@visactor/vutils-extension": ["@visactor/vutils-extension@2.0.21", "", { "dependencies": { "@visactor/vdataset": "~1.0.23", "@visactor/vutils": "~1.0.23" } }, "sha512-E9owMC/COyfz481BgDrVPPOgK3O98NZtZldko2dCZBFWuByiRx+xcn4gvSO6vlt2SrBB0LzzyhLYkrNs9xb/0A=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], @@ -738,7 +742,7 @@ "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], - "ahooks": ["ahooks@3.8.5", "", { "dependencies": { "@babel/runtime": "^7.21.0", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw=="], + "ahooks": ["ahooks@3.9.7", "", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw=="], "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -830,7 +834,7 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chroma-js": ["chroma-js@3.1.2", "", {}, "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg=="], + "chroma-js": ["chroma-js@3.2.0", "", {}, "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -1114,7 +1118,7 @@ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], - "framer-motion": ["framer-motion@12.12.2", "", { "dependencies": { "motion-dom": "^12.12.1", "motion-utils": "^12.12.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-qCszZCiGWkilL40E3VuhIJJC/CS3SIBl2IHyGK8FU30nOUhTmhBNWPrNFyozAWH/bXxwzi19vJHIGVdALF0LCg=="], + "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -1146,6 +1150,8 @@ "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], + "gifuct-js": ["gifuct-js@2.1.2", "", { "dependencies": { "js-binary-schema-parser": "^2.0.3" } }, "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg=="], + "giscus": ["giscus@1.6.0", "", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="], "glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="], @@ -1226,7 +1232,7 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "immer": ["immer@10.1.1", "", {}, "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="], + "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], "immutable": ["immutable@5.1.2", "", {}, "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ=="], @@ -1270,6 +1276,8 @@ "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + "is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], @@ -1292,6 +1300,8 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-binary-schema-parser": ["js-binary-schema-parser@2.0.3", "", {}, "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="], + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1330,7 +1340,7 @@ "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "leva": ["leva@0.10.0", "", { "dependencies": { "@radix-ui/react-portal": "1.0.2", "@radix-ui/react-tooltip": "1.0.5", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-RiNJWmeqQdKIeHuVXgshmxIHu144a2AMYtLxKf8Nm1j93pisDPexuQDHKNdQlbo37wdyDQibLjY9JKGIiD7gaw=="], + "leva": ["leva@0.10.1", "", { "dependencies": { "@radix-ui/react-portal": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.8", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -1426,6 +1436,10 @@ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.2", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w=="], + "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="], + + "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="], + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], @@ -1512,9 +1526,11 @@ "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], - "motion-dom": ["motion-dom@12.12.1", "", { "dependencies": { "motion-utils": "^12.12.1" } }, "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ=="], + "motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="], - "motion-utils": ["motion-utils@12.12.1", "", {}, "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w=="], + "motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], + + "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1550,7 +1566,7 @@ "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -1664,7 +1680,7 @@ "quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="], - "query-string": ["query-string@9.2.0", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ=="], + "query-string": ["query-string@9.3.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1672,7 +1688,7 @@ "rc-checkbox": ["rc-checkbox@3.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="], - "rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], + "rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="], "rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="], @@ -1742,7 +1758,7 @@ "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], - "react-avatar-editor": ["react-avatar-editor@13.0.2", "", { "dependencies": { "@babel/plugin-transform-runtime": "^7.12.1", "@babel/runtime": "^7.12.5", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, "sha512-a4ajbi7lwDh98kgEtSEeKMu0vs0CHTczkq4Xcxr1EiwMFH1GlgHCEtwGU8q/H5W8SeLnH4KPK8LUjEEaZXklxQ=="], + "react-avatar-editor": ["react-avatar-editor@14.0.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NaQM3oo4u0a1/Njjutc2FjwKX35vQV+t6S8hovsbAlMpBN1ntIwP/g+Yr9eDIIfaNtRXL0AqboTnPmRxhD/i8A=="], "react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="], @@ -1752,13 +1768,13 @@ "react-dropzone": ["react-dropzone@14.3.5", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ=="], - "react-error-boundary": ["react-error-boundary@5.0.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ=="], + "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="], "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], "react-fireworks": ["react-fireworks@1.0.4", "", {}, "sha512-jj1a+HTicB4pR6g2lqhVyAox0GTE0TOrZK2XaJFRYOwltgQWeYErZxnvU9+zH/blY+Hpmu9IKyb39OD3KcCMJw=="], - "react-hotkeys-hook": ["react-hotkeys-hook@5.1.0", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-GCNGXjBzV9buOS3REoQFmSmE4WTvBhYQ0YrAeeMZI83bhXg3dRWsLHXDutcVDdEjwJqJCxk5iewWYX5LtFUd7g=="], + "react-hotkeys-hook": ["react-hotkeys-hook@5.2.4", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A=="], "react-i18next": ["react-i18next@13.5.0", "", { "dependencies": { "@babel/runtime": "^7.22.5", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA=="], @@ -1766,7 +1782,7 @@ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "react-layout-kit": ["react-layout-kit@1.9.1", "", { "dependencies": { "@babel/runtime": "^7", "@emotion/css": "^11" }, "peerDependencies": { "react": ">=18" } }, "sha512-tQO5J+Ajppu2JCdhgFaFbWCg01WJXXaQ5vg8cxzsv8vVeogJKGFgoJm9OI2saDFchfKP3RABd+aRY5vB++poqw=="], + "react-layout-kit": ["react-layout-kit@2.0.1", "", { "dependencies": { "@babel/runtime": "^7.28.2", "@emotion/css": "^11.13.5", "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "react": ">=19" } }, "sha512-MdzEviHXwCfDuUcYWiRUzbxUujW0Ft0XMrwvNbKxdxNY7Vgr9StT2CjT8ElPWSJMSkSSoXHhSyJflacKlFb6NA=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], @@ -1776,7 +1792,7 @@ "react-resizable": ["react-resizable@3.0.5", "", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.0.3" }, "peerDependencies": { "react": ">= 16.3" } }, "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w=="], - "react-rnd": ["react-rnd@10.5.2", "", { "dependencies": { "re-resizable": "6.11.2", "react-draggable": "4.4.6", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw=="], + "react-rnd": ["react-rnd@10.5.3", "", { "dependencies": { "re-resizable": "^6.11.2", "react-draggable": "^4.5.0", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q=="], "react-router": ["react-router@6.28.1", "", { "dependencies": { "@remix-run/router": "1.21.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA=="], @@ -1808,12 +1824,14 @@ "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], - "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "rehype-github-alerts": ["rehype-github-alerts@4.2.0", "", { "dependencies": { "@primer/octicons": "^19.20.0", "hast-util-from-html": "^2.0.3", "hast-util-is-element": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-6di6kEu9WUHKLKrkKG2xX6AOuaCMGghg0Wq7MEuM/jBYUPVIq6PJpMe00dxMfU+/YSBtDXhffpDimgDi+BObIQ=="], + "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], @@ -1824,8 +1842,12 @@ "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], + "remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + "remark-github": ["remark-github@12.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0", "mdast-util-to-string": "^4.0.0", "to-vfile": "^8.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-ByefQKFN184LeiGRCabfl7zUJsdlMYWEhiLX1gpmQ11yFg6xSuOTW7LVCv0oc1x+YvUMJW23NU36sJX2RWGgvg=="], + "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], "remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], @@ -1892,7 +1914,9 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="], + "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "shiki-stream": ["shiki-stream@0.1.4", "", { "dependencies": { "@shikijs/core": "^3.0.0" }, "peerDependencies": { "react": "^19.0.0", "solid-js": "^1.9.0", "vue": "^3.2.0" }, "optionalPeers": ["react", "solid-js", "vue"] }, "sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -1956,9 +1980,9 @@ "swc-walk": ["swc-walk@1.0.0", "", { "dependencies": { "acorn-walk": "^8.3.4" } }, "sha512-QnEvBZ/ZRsUrXCz/Z3Kto06xUsoqUTo3doj/UvOD0RfamEgqlhpgpyCykFAwiUcuDrODShzlxuDqDPf2Wc+DvQ=="], - "swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="], + "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], - "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], @@ -1976,6 +2000,8 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "to-vfile": ["to-vfile@8.0.0", "", { "dependencies": { "vfile": "^6.0.0" } }, "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg=="], + "toggle-selection": ["toggle-selection@1.0.6", "", {}, "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="], "topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="], @@ -1990,7 +2016,7 @@ "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - "ts-md5": ["ts-md5@1.3.1", "", {}, "sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg=="], + "ts-md5": ["ts-md5@2.0.1", "", {}, "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -2038,7 +2064,7 @@ "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="], - "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -2134,35 +2160,57 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@lobehub/fluent-emoji/@lobehub/ui": ["@lobehub/ui@2.1.10", "", { "dependencies": { "@ant-design/cssinjs": "^1.23.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@floating-ui/react": "^0.27.5", "@giscus/react": "^3.1.0", "@lobehub/fluent-emoji": "^2.0.0", "@lobehub/icons": "^2.0.0", "@mdx-js/mdx": "^3.1.0", "@mdx-js/react": "^3.1.0", "@radix-ui/react-slot": "^1.1.2", "@shikijs/transformers": "^3.2.1", "@splinetool/runtime": "0.9.526", "ahooks": "^3.8.4", "antd-style": "^3.7.1", "chroma-js": "^3.1.2", "class-variance-authority": "^0.7.1", "dayjs": "^1.11.13", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "framer-motion": "^12.6.3", "immer": "^10.1.1", "katex": "^0.16.9", "leva": "^0.10.0", "lodash-es": "^4.17.21", "lucide-react": "^0.484.0", "mermaid": "^11.6.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.1.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.11.1", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^13.0.2", "react-error-boundary": "^5.0.0", "react-hotkeys-hook": "^5.1.0", "react-layout-kit": "^1.9.1", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.2.1", "swr": "^2.3.3", "ts-md5": "^1.3.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^11.1.0" }, "peerDependencies": { "antd": "^5.25.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-R1/t5I8UAjvd5xoEDJXg6RzHmwhdOU45JQN297MlYB/sGqcvySfQL9POpDmySSs+QMyjkhwhum254cfXFKJIZA=="], + "@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], + "@lobehub/fluent-emoji/react-layout-kit": ["react-layout-kit@1.9.1", "", { "dependencies": { "@babel/runtime": "^7", "@emotion/css": "^11" }, "peerDependencies": { "react": ">=18" } }, "sha512-tQO5J+Ajppu2JCdhgFaFbWCg01WJXXaQ5vg8cxzsv8vVeogJKGFgoJm9OI2saDFchfKP3RABd+aRY5vB++poqw=="], + "@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], + "@lobehub/ui/@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="], + "@lobehub/ui/@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], - "@lobehub/ui/lucide-react": ["lucide-react@0.484.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ=="], + "@lobehub/ui/@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@lobehub/ui/dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], + + "@lobehub/ui/katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], - "@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + "@lobehub/ui/lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - "@radix-ui/react-popper/@floating-ui/react-dom": ["@floating-ui/react-dom@0.7.2", "", { "dependencies": { "@floating-ui/dom": "^0.5.3", "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg=="], + "@lobehub/ui/lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], - "@radix-ui/react-popper/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + "@lobehub/ui/marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], - "@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + "@lobehub/ui/mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], - "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + "@lobehub/ui/rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], - "@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + "@lobehub/ui/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], - "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@visactor/vrender-kits/roughjs": ["roughjs@4.5.2", "", { "dependencies": { "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg=="], + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@vue/compiler-core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "antd/rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="], + "ahooks/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "ahooks/dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], "antd/scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], @@ -2248,6 +2296,10 @@ "react-draggable/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + "react-layout-kit/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "react-rnd/react-draggable": ["react-draggable@4.5.0", "", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="], + "react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], "react-telegram-login/react": ["react@16.14.0", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2" } }, "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g=="], @@ -2260,6 +2312,8 @@ "shapefile/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "shiki-stream/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], + "simplify-geojson/concat-stream": ["concat-stream@1.4.11", "", { "dependencies": { "inherits": "~2.0.1", "readable-stream": "~1.1.9", "typedarray": "~0.0.5" } }, "sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw=="], "split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], @@ -2302,9 +2356,73 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - "@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@0.5.4", "", { "dependencies": { "@floating-ui/core": "^0.7.3" } }, "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg=="], + "@lobehub/fluent-emoji/@lobehub/ui/@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@floating-ui/react": ["@floating-ui/react@0.27.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.9", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-EQJ4Th328y2wyHR3KzOUOoTW2UKjFk53fmyahfwExnFQ8vnsMYqKc+fFPOkeYtj5tcp1DUMiNJ7BFhed7e9ONw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@lobehub/icons": ["@lobehub/icons@2.1.0", "", { "dependencies": { "@lobehub/ui": "^2.0.0", "antd-style": "^3.7.1", "lucide-react": "^0.469.0", "polished": "^4.3.1", "react-layout-kit": "^1.9.1" }, "peerDependencies": { "antd": "^5.23.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-iHtIp8a05/YHxTDlOFXCTfvYXUjKi1Mbq5a9qsEN+zwJ5U+mR2WgKz5zUausIzZiMZo+P3pgxbhh3/eHf7Q1pw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/ahooks": ["ahooks@3.8.5", "", { "dependencies": { "@babel/runtime": "^7.21.0", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/chroma-js": ["chroma-js@3.1.2", "", {}, "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/framer-motion": ["framer-motion@12.12.2", "", { "dependencies": { "motion-dom": "^12.12.1", "motion-utils": "^12.12.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-qCszZCiGWkilL40E3VuhIJJC/CS3SIBl2IHyGK8FU30nOUhTmhBNWPrNFyozAWH/bXxwzi19vJHIGVdALF0LCg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/immer": ["immer@10.1.1", "", {}, "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva": ["leva@0.10.0", "", { "dependencies": { "@radix-ui/react-portal": "1.0.2", "@radix-ui/react-tooltip": "1.0.5", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-RiNJWmeqQdKIeHuVXgshmxIHu144a2AMYtLxKf8Nm1j93pisDPexuQDHKNdQlbo37wdyDQibLjY9JKGIiD7gaw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/lucide-react": ["lucide-react@0.484.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/query-string": ["query-string@9.2.0", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/react-avatar-editor": ["react-avatar-editor@13.0.2", "", { "dependencies": { "@babel/plugin-transform-runtime": "^7.12.1", "@babel/runtime": "^7.12.5", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, "sha512-a4ajbi7lwDh98kgEtSEeKMu0vs0CHTczkq4Xcxr1EiwMFH1GlgHCEtwGU8q/H5W8SeLnH4KPK8LUjEEaZXklxQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/react-error-boundary": ["react-error-boundary@5.0.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/react-hotkeys-hook": ["react-hotkeys-hook@5.1.0", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-GCNGXjBzV9buOS3REoQFmSmE4WTvBhYQ0YrAeeMZI83bhXg3dRWsLHXDutcVDdEjwJqJCxk5iewWYX5LtFUd7g=="], - "@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + "@lobehub/fluent-emoji/@lobehub/ui/react-rnd": ["react-rnd@10.5.2", "", { "dependencies": { "re-resizable": "6.11.2", "react-draggable": "4.4.6", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="], + + "@lobehub/fluent-emoji/@lobehub/ui/ts-md5": ["ts-md5@1.3.1", "", {}, "sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg=="], + + "@lobehub/ui/@ant-design/cssinjs/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@lobehub/ui/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "@lobehub/ui/mermaid/@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], + + "@lobehub/ui/mermaid/cytoscape": ["cytoscape@3.33.2", "", {}, "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw=="], + + "@lobehub/ui/mermaid/dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + + "@lobehub/ui/mermaid/dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + + "@lobehub/ui/mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + + "@lobehub/ui/mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@vue/compiler-core/@babel/parser/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], @@ -2328,6 +2446,8 @@ "ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "shiki-stream/@shikijs/core/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], + "simplify-geojson/concat-stream/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="], "simplify-geojson/concat-stream/typedarray": ["typedarray@0.0.7", "", {}, "sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ=="], @@ -2362,7 +2482,47 @@ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], - "@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@0.7.3", "", {}, "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg=="], + "@lobehub/fluent-emoji/@lobehub/ui/@floating-ui/react/@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@floating-ui/react/@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@floating-ui/react/tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/framer-motion/motion-dom": ["motion-dom@12.12.1", "", { "dependencies": { "motion-utils": "^12.12.1" } }, "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/framer-motion/motion-utils": ["motion-utils@12.12.1", "", {}, "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.0.5", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-dismissable-layer": "1.0.3", "@radix-ui/react-id": "1.0.0", "@radix-ui/react-popper": "1.1.1", "@radix-ui/react-portal": "1.0.2", "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-slot": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.0", "@radix-ui/react-visually-hidden": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="], + + "@lobehub/fluent-emoji/@lobehub/ui/react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki/@shikijs/langs": ["@shikijs/langs@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki/@shikijs/themes": ["@shikijs/themes@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/swr/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + + "@lobehub/ui/mermaid/@iconify/utils/mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium": ["langium@4.2.2", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], "@vue/compiler-core/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -2375,5 +2535,101 @@ "sucrase/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-escape-keydown": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "0.7.2", "@radix-ui/react-arrow": "1.0.2", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0", "@radix-ui/react-use-rect": "1.0.0", "@radix-ui/react-use-size": "1.0.0", "@radix-ui/rect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/react-dropzone/file-selector": ["file-selector@0.5.0", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + + "@lobehub/ui/mermaid/@iconify/utils/mlly/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "@lobehub/ui/mermaid/@iconify/utils/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "@lobehub/ui/mermaid/@iconify/utils/mlly/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium/@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@floating-ui/react-dom": ["@floating-ui/react-dom@0.7.2", "", { "dependencies": { "@floating-ui/dom": "^0.5.3", "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/rect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + + "@lobehub/fluent-emoji/@lobehub/ui/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], + + "@lobehub/ui/mermaid/@iconify/utils/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "@lobehub/ui/mermaid/@iconify/utils/mlly/pkg-types/mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], + + "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@0.5.4", "", { "dependencies": { "@floating-ui/core": "^0.7.3" } }, "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg=="], + + "@lobehub/ui/mermaid/@iconify/utils/mlly/pkg-types/mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "@lobehub/ui/mermaid/@iconify/utils/mlly/pkg-types/mlly/ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + + "@lobehub/fluent-emoji/@lobehub/ui/leva/@radix-ui/react-tooltip/@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@0.7.3", "", {}, "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg=="], } } diff --git a/web/classic/package.json b/web/classic/package.json index 83b5d23049bb..eb433a7c4755 100644 --- a/web/classic/package.json +++ b/web/classic/package.json @@ -6,10 +6,11 @@ "dependencies": { "@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-ui": "^2.69.1", - "@lobehub/icons": "^2.0.0", - "@visactor/react-vchart": "~1.8.8", - "@visactor/vchart": "~1.8.8", - "@visactor/vchart-semi-theme": "~1.8.8", + "@lobehub/icons": "^2.48.0", + "@visactor/react-vchart": "^2.0.21", + "@visactor/vchart": "^2.0.21", + "@visactor/vchart-semi-theme": "^1.12.3", + "antd": "^5.24.0", "axios": "1.15.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", diff --git a/web/classic/public/kie-ai-logo.png b/web/classic/public/kie-ai-logo.png new file mode 100644 index 000000000000..439f8fbc65ac Binary files /dev/null and b/web/classic/public/kie-ai-logo.png differ diff --git a/web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx b/web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx index 4097545e5e62..7db7446b7944 100644 --- a/web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx +++ b/web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx @@ -31,10 +31,11 @@ import { Loader, List, Hash, - Video, Sparkles, + Image, } from 'lucide-react'; import { + TASK_ACTION_ASSET_UPLOAD, TASK_ACTION_FIRST_TAIL_GENERATE, TASK_ACTION_GENERATE, TASK_ACTION_REFERENCE_GENERATE, @@ -90,7 +91,7 @@ function renderDuration(submit_time, finishTime) { ); } -const renderType = (type, t) => { +const renderType = (type, t, record) => { switch (type) { case 'MUSIC': return ( @@ -104,7 +105,27 @@ const renderType = (type, t) => { {t('生成歌词')} ); + case TASK_ACTION_ASSET_UPLOAD: + return ( + }> + {t('素材上传')} + + ); case TASK_ACTION_GENERATE: + if (record?.upstream_kind === 'asset') { + return ( + }> + {t('素材上传')} + + ); + } + if (record?.upstream_kind === 'image') { + return ( + }> + {t('图像生成')} + + ); + } return ( }> {t('图生视频')} @@ -170,6 +191,70 @@ const renderPlatform = (platform, t) => { } }; +// Resolve preview URL: backend may expose corrected result_url; fallback to nested image URL in task data. +function extractImageUrlFromTaskData(data) { + if (data == null) return ''; + const walk = (v) => { + if (v == null || typeof v !== 'object') return ''; + if (typeof v.url === 'string' && /^https?:\/\//.test(v.url)) { + const lower = v.url.toLowerCase(); + if ( + /\.(jpe?g|png|webp|gif)(\?|$)/i.test(v.url) || + lower.includes('seedream') || + (lower.includes('tos-') && lower.includes('jpeg')) + ) { + return v.url; + } + } + if (Array.isArray(v)) { + for (const item of v) { + const found = walk(item); + if (found) return found; + } + return ''; + } + for (const k of Object.keys(v)) { + const found = walk(v[k]); + if (found) return found; + } + return ''; + }; + try { + const obj = typeof data === 'string' ? JSON.parse(data) : data; + return walk(obj); + } catch { + return ''; + } +} + +function resolveTaskPreviewUrl(record) { + const primary = record.result_url; + if (typeof primary !== 'string' || !/^https?:\/\//.test(primary)) { + return extractImageUrlFromTaskData(record.data) || ''; + } + if ( + record.upstream_kind === 'image' && + primary.includes('/v1/videos/') && + primary.includes('/content') + ) { + const fromData = extractImageUrlFromTaskData(record.data); + if (fromData) return fromData; + } + return primary; +} + +function isAsyncImageTaskForPreview(record) { + if (record.upstream_kind === 'image') return true; + const u = resolveTaskPreviewUrl(record); + if (!u || !/^https?:\/\//.test(u)) return false; + const lower = u.toLowerCase(); + return ( + /\.(jpe?g|png|webp|gif)(\?|$)/i.test(u) || + lower.includes('seedream') || + (lower.includes('tos-') && lower.includes('jpeg')) + ); +} + const renderStatus = (type, t) => { switch (type) { case 'SUCCESS': @@ -240,6 +325,7 @@ export const getTaskLogsColumns = ({ openContentModal, isAdminUser, openVideoModal, + openImageModal, openAudioModal, }) => { return [ @@ -301,15 +387,10 @@ export const getTaskLogsColumns = ({ const displayText = String(record.username || userId || '?'); return ( - + {displayText.slice(0, 1)} - - {displayText} - + {displayText} ); }, @@ -327,7 +408,7 @@ export const getTaskLogsColumns = ({ title: t('类型'), dataIndex: 'action', render: (text, record, index) => { - return
{renderType(text, t)}
; + return
{renderType(text, t, record)}
; }, }, { @@ -407,23 +488,42 @@ export const getTaskLogsColumns = ({ ); } - // 视频预览:优先使用 result_url,兼容旧数据 fail_reason 中的 URL - const isVideoTask = - record.action === TASK_ACTION_GENERATE || - record.action === TASK_ACTION_TEXT_GENERATE || - record.action === TASK_ACTION_FIRST_TAIL_GENERATE || - record.action === TASK_ACTION_REFERENCE_GENERATE || - record.action === TASK_ACTION_REMIX_GENERATE; const isSuccess = record.status === 'SUCCESS'; - const resultUrl = record.result_url; - const hasResultUrl = typeof resultUrl === 'string' && /^https?:\/\//.test(resultUrl); - if (isSuccess && isVideoTask && hasResultUrl) { + const previewUrl = resolveTaskPreviewUrl(record); + const hasPreviewUrl = + typeof previewUrl === 'string' && /^https?:\/\//.test(previewUrl); + + // Async image (e.g. PingXingShiJie OpenAI-compatible image generations) + if (isSuccess && isAsyncImageTaskForPreview(record) && hasPreviewUrl) { + return ( + { + e.preventDefault(); + openImageModal(previewUrl); + }} + > + {t('点击预览图片')} + + ); + } + + // Video preview: same action names as image async; exclude image and asset uploads. + const isVideoTask = + (record.action === TASK_ACTION_GENERATE || + record.action === TASK_ACTION_TEXT_GENERATE || + record.action === TASK_ACTION_FIRST_TAIL_GENERATE || + record.action === TASK_ACTION_REFERENCE_GENERATE || + record.action === TASK_ACTION_REMIX_GENERATE) && + record.upstream_kind !== 'image' && + record.upstream_kind !== 'asset'; + if (isSuccess && isVideoTask && hasPreviewUrl) { return ( { e.preventDefault(); - openVideoModal(resultUrl); + openVideoModal(previewUrl); }} > {t('点击预览视频')} diff --git a/web/classic/src/components/table/task-logs/TaskLogsTable.jsx b/web/classic/src/components/table/task-logs/TaskLogsTable.jsx index b3cec8ccc02c..31542b079f7a 100644 --- a/web/classic/src/components/table/task-logs/TaskLogsTable.jsx +++ b/web/classic/src/components/table/task-logs/TaskLogsTable.jsx @@ -40,6 +40,7 @@ const TaskLogsTable = (taskLogsData) => { copyText, openContentModal, openVideoModal, + openImageModal, openAudioModal, showUserInfoFunc, isAdminUser, @@ -55,11 +56,12 @@ const TaskLogsTable = (taskLogsData) => { copyText, openContentModal, openVideoModal, + openImageModal, openAudioModal, showUserInfoFunc, isAdminUser, }); - }, [t, COLUMN_KEYS, copyText, openContentModal, openVideoModal, openAudioModal, showUserInfoFunc, isAdminUser]); + }, [t, COLUMN_KEYS, copyText, openContentModal, openVideoModal, openImageModal, openAudioModal, showUserInfoFunc, isAdminUser]); // Filter columns based on visibility settings const getVisibleColumns = () => { diff --git a/web/classic/src/components/table/task-logs/index.jsx b/web/classic/src/components/table/task-logs/index.jsx index 07c387123a91..a8e293581fa0 100644 --- a/web/classic/src/components/table/task-logs/index.jsx +++ b/web/classic/src/components/table/task-logs/index.jsx @@ -38,13 +38,21 @@ const TaskLogsPage = () => { <> {/* Modals */} - + {/* 新增:视频预览弹窗 */} + { const { t } = useTranslation(); const [videoError, setVideoError] = useState(false); + const [imageError, setImageError] = useState(false); const [isLoading, setIsLoading] = useState(false); useEffect(() => { @@ -39,7 +41,11 @@ const ContentModal = ({ setVideoError(false); setIsLoading(true); } - }, [isModalOpen, isVideo]); + if (isModalOpen && isImage) { + setImageError(false); + setIsLoading(true); + } + }, [isModalOpen, isVideo, isImage]); const handleVideoError = () => { setVideoError(true); @@ -50,6 +56,15 @@ const ContentModal = ({ setIsLoading(false); }; + const handleImageError = () => { + setImageError(true); + setIsLoading(false); + }; + + const handleImageLoaded = () => { + setIsLoading(false); + }; + const handleCopyUrl = () => { navigator.clipboard.writeText(modalContent); }; @@ -152,6 +167,80 @@ const ContentModal = ({ ); }; + const renderImageContent = () => { + if (imageError) { + return ( +
+ + {t('图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。')} + +
+ + +
+
+ + {modalContent} + +
+
+ ); + } + + return ( +
+ {isLoading && ( +
+ +
+ )} + setIsLoading(true)} + /> +
+ ); + }; + + const isMediaModal = isVideo || isImage; + return ( setIsModalOpen(false)} closable={null} bodyStyle={{ - height: isVideo ? '70vh' : '400px', + height: isMediaModal ? '70vh' : '400px', maxHeight: '80vh', overflow: 'auto', - padding: isVideo && videoError ? '0' : '24px', + padding: + isMediaModal && (isVideo ? videoError : imageError) ? '0' : '24px', }} - width={isVideo ? '90vw' : 800} - style={isVideo ? { maxWidth: 960 } : undefined} + width={isMediaModal ? '90vw' : 800} + style={isMediaModal ? { maxWidth: 960 } : undefined} > {isVideo ? ( renderVideoContent() + ) : isImage ? ( + renderImageContent() ) : (

{modalContent}

)} diff --git a/web/classic/src/constants/channel.constants.js b/web/classic/src/constants/channel.constants.js index 9fa78779de8f..22fd490c3769 100644 --- a/web/classic/src/constants/channel.constants.js +++ b/web/classic/src/constants/channel.constants.js @@ -189,6 +189,16 @@ export const CHANNEL_OPTIONS = [ color: 'blue', label: 'Codex (OpenAI OAuth)', }, + { + value: 58, + color: 'blue', + label: '平行视界', + }, + { + value: 59, + color: 'purple', + label: 'Kie.ai', + }, ]; // Channel types that support upstream model list fetching in UI. diff --git a/web/classic/src/constants/common.constant.js b/web/classic/src/constants/common.constant.js index 316356631d77..629a2604a637 100644 --- a/web/classic/src/constants/common.constant.js +++ b/web/classic/src/constants/common.constant.js @@ -40,6 +40,7 @@ export const API_ENDPOINTS = [ ]; export const TASK_ACTION_GENERATE = 'generate'; +export const TASK_ACTION_ASSET_UPLOAD = 'assetUpload'; export const TASK_ACTION_TEXT_GENERATE = 'textGenerate'; export const TASK_ACTION_FIRST_TAIL_GENERATE = 'firstTailGenerate'; export const TASK_ACTION_REFERENCE_GENERATE = 'referenceGenerate'; diff --git a/web/classic/src/helpers/render.jsx b/web/classic/src/helpers/render.jsx index f785c085e63c..e14fdae694b4 100644 --- a/web/classic/src/helpers/render.jsx +++ b/web/classic/src/helpers/render.jsx @@ -402,7 +402,16 @@ export function getChannelIcon(channelType) { case 51: // 即梦 Jimeng return ; case 54: // 豆包视频 Doubao Video + case 58: // 平行视界 PingXingShiJie (fork of task/doubao) return ; + case 59: // Kie.ai + return ( + Kie.ai + ); case 56: // Replicate return ; case 8: // 自定义渠道 diff --git a/web/classic/src/hooks/task-logs/useTaskLogsData.js b/web/classic/src/hooks/task-logs/useTaskLogsData.js index 6ba3de3882fa..6e87a7a51ca5 100644 --- a/web/classic/src/hooks/task-logs/useTaskLogsData.js +++ b/web/classic/src/hooks/task-logs/useTaskLogsData.js @@ -72,6 +72,10 @@ export const useTaskLogsData = () => { const [isVideoModalOpen, setIsVideoModalOpen] = useState(false); const [videoUrl, setVideoUrl] = useState(''); + // Image preview (async image tasks, e.g. PingXingShiJie / OpenAI images generations) + const [isImageModalOpen, setIsImageModalOpen] = useState(false); + const [imageUrl, setImageUrl] = useState(''); + // Audio preview modal state const [isAudioModalOpen, setIsAudioModalOpen] = useState(false); const [audioClips, setAudioClips] = useState([]); @@ -281,6 +285,11 @@ export const useTaskLogsData = () => { setIsVideoModalOpen(true); }; + const openImageModal = (url) => { + setImageUrl(url); + setIsImageModalOpen(true); + }; + const openAudioModal = (clips) => { setAudioClips(clips); setIsAudioModalOpen(true); @@ -328,6 +337,10 @@ export const useTaskLogsData = () => { setIsVideoModalOpen, videoUrl, + isImageModalOpen, + setIsImageModalOpen, + imageUrl, + // Audio preview modal isAudioModalOpen, setIsAudioModalOpen, @@ -366,6 +379,7 @@ export const useTaskLogsData = () => { copyText, openContentModal, openVideoModal, + openImageModal, openAudioModal, enrichLogs, syncPageData, diff --git a/web/classic/src/i18n/locales/en.json b/web/classic/src/i18n/locales/en.json index dc8ad6cb9464..c9683c760802 100644 --- a/web/classic/src/i18n/locales/en.json +++ b/web/classic/src/i18n/locales/en.json @@ -2354,6 +2354,8 @@ "点击查看差异": "Click to view differences", "点击此处": "click here", "点击预览视频": "Click to preview video", + "点击预览图片": "Click to preview image", + "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "The image could not be loaded in this browser (often due to CORS or hotlink protection).", "点击预览音乐": "Click to preview music", "点击验证按钮,使用您的生物特征或安全密钥": "Click the verification button and use your biometrics or security key", "版权所有": "All rights reserved", diff --git a/web/classic/src/i18n/locales/fr.json b/web/classic/src/i18n/locales/fr.json index 8e7d143d0954..c7bc0d85f1b6 100644 --- a/web/classic/src/i18n/locales/fr.json +++ b/web/classic/src/i18n/locales/fr.json @@ -2344,6 +2344,8 @@ "点击查看差异": "Cliquez pour voir les différences", "点击此处": "cliquez ici", "点击预览视频": "Cliquez pour prévisualiser la vidéo", + "点击预览图片": "Cliquez pour prévisualiser l’image", + "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "L’image n’a pas pu être chargée (CORS ou protection anti-hotlink).", "点击预览音乐": "Cliquez pour écouter la musique", "点击验证按钮,使用您的生物特征或安全密钥": "Cliquez sur le bouton de vérification pour utiliser vos caractéristiques biométriques ou votre clé de sécurité", "版权所有": "Tous droits réservés", diff --git a/web/classic/src/i18n/locales/ja.json b/web/classic/src/i18n/locales/ja.json index 88d2899e17c6..4299dd4f1194 100644 --- a/web/classic/src/i18n/locales/ja.json +++ b/web/classic/src/i18n/locales/ja.json @@ -2315,6 +2315,8 @@ "点击查看差异": "差分を表示", "点击此处": "こちらをクリック", "点击预览视频": "動画をプレビュー", + "点击预览图片": "画像をプレビュー", + "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "画像を読み込めませんでした(CORS やホットリンク対策の可能性があります)。", "点击预览音乐": "音楽をプレビュー", "点击验证按钮,使用您的生物特征或安全密钥": "認証ボタンをクリックし、生体情報またはセキュリティキーを使用してください", "版权所有": "All rights reserved", diff --git a/web/classic/src/i18n/locales/ru.json b/web/classic/src/i18n/locales/ru.json index 2980af179f6f..1d118adb9dc8 100644 --- a/web/classic/src/i18n/locales/ru.json +++ b/web/classic/src/i18n/locales/ru.json @@ -2362,6 +2362,8 @@ "点击查看差异": "Нажмите для просмотра различий", "点击此处": "Нажмите здесь", "点击预览视频": "Нажмите для предварительного просмотра видео", + "点击预览图片": "Нажмите для предпросмотра изображения", + "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "Не удалось загрузить изображение (возможны CORS или защита от хотлинка).", "点击预览音乐": "Нажмите для прослушивания музыки", "点击验证按钮,使用您的生物特征或安全密钥": "Нажмите кнопку проверки, используйте ваши биометрические данные или ключ безопасности", "版权所有": "Все права защищены", diff --git a/web/classic/src/i18n/locales/vi.json b/web/classic/src/i18n/locales/vi.json index 4ca1a77f3122..2a39d603c313 100644 --- a/web/classic/src/i18n/locales/vi.json +++ b/web/classic/src/i18n/locales/vi.json @@ -2453,6 +2453,8 @@ "点击链接重置密码": "Nhấp vào liên kết để đặt lại mật khẩu", "点击阅读": "Nhấp để đọc", "点击预览视频": "Nhấp để xem trước video", + "点击预览图片": "Nhấp để xem trước ảnh", + "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "Không thể tải ảnh (có thể do CORS hoặc chống hotlink).", "点击预览音乐": "Nhấp để nghe nhạc", "点击验证按钮,使用您的生物特征或安全密钥": "Nhấp vào nút xác minh và sử dụng sinh trắc học hoặc khóa bảo mật của bạn", "版": "Phiên bản", diff --git a/web/classic/src/i18n/locales/zh-CN.json b/web/classic/src/i18n/locales/zh-CN.json index e54a1c0f9114..f49664050b2f 100644 --- a/web/classic/src/i18n/locales/zh-CN.json +++ b/web/classic/src/i18n/locales/zh-CN.json @@ -2313,6 +2313,8 @@ "点击查看差异": "点击查看差异", "点击此处": "点击此处", "点击预览视频": "点击预览视频", + "点击预览图片": "点击预览图片", + "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。", "点击预览音乐": "点击预览音乐", "点击验证按钮,使用您的生物特征或安全密钥": "点击验证按钮,使用您的生物特征或安全密钥", "版权所有": "版权所有", diff --git a/web/classic/src/i18n/locales/zh-TW.json b/web/classic/src/i18n/locales/zh-TW.json index b31c9e1e0eac..75beda3704bb 100644 --- a/web/classic/src/i18n/locales/zh-TW.json +++ b/web/classic/src/i18n/locales/zh-TW.json @@ -2323,6 +2323,8 @@ "点击查看差异": "點擊查看差異", "点击此处": "點擊此處", "点击预览视频": "點擊預覽影片", + "点击预览图片": "點擊預覽圖片", + "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "圖片無法在此瀏覽器中載入,可能是跨域或防盜鏈限制。", "点击预览音乐": "點擊預覽音樂", "点击验证按钮,使用您的生物特征或安全密钥": "點擊驗證按鈕,使用您的生物特徵或安全密鑰", "版权所有": "版權所有", diff --git a/web/classic/vite.config.js b/web/classic/vite.config.js index 73e46212a587..f3ccb78a4dcb 100644 --- a/web/classic/vite.config.js +++ b/web/classic/vite.config.js @@ -65,6 +65,7 @@ export default defineConfig({ }, }, build: { + reportCompressedSize: false, rollupOptions: { output: { manualChunks: { diff --git a/web/default/src/features/channels/constants.ts b/web/default/src/features/channels/constants.ts index 21e151615083..73efadbe4267 100644 --- a/web/default/src/features/channels/constants.ts +++ b/web/default/src/features/channels/constants.ts @@ -58,12 +58,14 @@ export const CHANNEL_TYPES = { 55: 'Sora', 56: 'Replicate', 57: 'Codex', + 58: 'PingXingShiJie', + 59: 'KieAI', } as const const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [ 1, 14, 33, 24, 43, 3, 41, 48, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, 46, 23, 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 22, 21, 44, 2, 5, 36, 50, - 51, 52, 53, 54, 55, 56, + 51, 52, 53, 54, 55, 56, 58, 59, ] export const CHANNEL_TYPE_OPTIONS: { value: number; label: string }[] = (() => { diff --git a/web/default/src/features/channels/lib/channel-utils.ts b/web/default/src/features/channels/lib/channel-utils.ts index 9d96b62e0f9f..db3dba510918 100644 --- a/web/default/src/features/channels/lib/channel-utils.ts +++ b/web/default/src/features/channels/lib/channel-utils.ts @@ -83,6 +83,8 @@ export function getChannelTypeIcon(type: number): string { 55: 'OpenAI', // Sora 54: 'Doubao', // DoubaoVideo 56: 'Replicate', // Replicate + 58: 'https://www.pingxingshijie.cn/favicon.ico', // PingXingShiJie + 59: 'https://kie.ai/logo.png', // KieAI // Tools & Platforms 37: 'Dify', // Dify diff --git a/web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx index e1f48d6b49e8..f849555e4a97 100644 --- a/web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx @@ -1,18 +1,20 @@ /* eslint-disable react-refresh/only-export-components */ import { useState, useMemo } from 'react' import type { ColumnDef } from '@tanstack/react-table' -import { Music } from 'lucide-react' +import { Images, Music, Video } from 'lucide-react' import { useTranslation } from 'react-i18next' import { formatTimestampToDate } from '@/lib/format' import { cn } from '@/lib/utils' import { DataTableColumnHeader } from '@/components/data-table' import { StatusBadge } from '@/components/status-badge' import { Avatar, AvatarFallback } from '@/components/ui/avatar' -import { TASK_ACTIONS, TASK_STATUS } from '../../constants' +import { TASK_STATUS } from '../../constants' import { taskActionMapper, taskStatusMapper, } from '../../lib/mappers' +import { getTaskActionLabel } from '../../lib/task-action-label' +import { extractTaskMediaResults } from '../../lib/task-media-results' import type { TaskLog } from '../../types' import { getLogAvatarStyle } from '../../lib/avatar-color' import { useUsageLogsContext } from '../usage-logs-provider' @@ -21,6 +23,7 @@ import { type AudioClip, } from '../dialogs/audio-preview-dialog' import { FailReasonDialog } from '../dialogs/fail-reason-dialog' +import { TaskMediaResultsDialog } from '../dialogs/task-media-results-dialog' import { createDurationColumn, createChannelColumn, @@ -74,6 +77,54 @@ function AudioPreviewCell({ log }: { log: TaskLog }) { ) } +function TaskMediaResultsCell({ + log, + results, +}: { + log: TaskLog + results: ReturnType +}) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + if (results.length === 0) return null + + const imageCount = results.filter((result) => result.type === 'image').length + const videoCount = results.length - imageCount + let label = t('View generated results') + if (results.length === 1) { + label = + results[0]?.type === 'image' + ? t('Click to preview image') + : t('Click to preview video') + } + + return ( + <> + + + + ) +} + export function useTaskLogsColumns(isAdmin: boolean): ColumnDef[] { const { t } = useTranslation() const columns: ColumnDef[] = [ @@ -180,7 +231,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef[] { className='max-w-full truncate rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono' /> - {t(log.platform)} · {t(taskActionMapper.getLabel(log.action))} + {t(log.platform)} · {t(getTaskActionLabel(log, taskActionMapper.getLabel(log.action)))} ) @@ -241,27 +292,13 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef[] { } } - const isVideoTask = - log.action === TASK_ACTIONS.GENERATE || - log.action === TASK_ACTIONS.TEXT_GENERATE || - log.action === TASK_ACTIONS.FIRST_TAIL_GENERATE || - log.action === TASK_ACTIONS.REFERENCE_GENERATE || - log.action === TASK_ACTIONS.REMIX_GENERATE const isSuccess = status === TASK_STATUS.SUCCESS - const isUrl = failReason?.startsWith('http') - if (isSuccess && isVideoTask && isUrl) { - const videoUrl = `/v1/videos/${log.task_id}/content` - return ( -
- {t('Click to preview video')} - - ) + if (isSuccess) { + const mediaResults = extractTaskMediaResults(log) + if (mediaResults.length > 0) { + return + } } if (!failReason) { diff --git a/web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx b/web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx new file mode 100644 index 000000000000..4545e0beca28 --- /dev/null +++ b/web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx @@ -0,0 +1,129 @@ +import { Copy, ExternalLink, ImageIcon, Video } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { ScrollArea } from '@/components/ui/scroll-area' +import { StatusBadge } from '@/components/status-badge' +import type { TaskMediaResult } from '../../lib/task-media-results' + +interface TaskMediaResultsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + results: TaskMediaResult[] + taskId?: string +} + +function copyUrl(url: string, successMessage: string): void { + void navigator.clipboard.writeText(url).then(() => { + toast.success(successMessage) + }) +} + +function openExternalUrl(url: string): void { + window.open(url, '_blank', 'noopener,noreferrer') +} + +function TaskMediaCard({ result }: { result: TaskMediaResult }) { + const { t } = useTranslation() + const isImage = result.type === 'image' + const title = isImage ? t('Generated image') : t('Generated video') + + return ( +
+
+ {isImage ? ( + {title} + ) : ( +
+
+
+ +
+ + +
+
+

+ {result.url} +

+
+
+ ) +} + +export function TaskMediaResultsDialog(props: TaskMediaResultsDialogProps) { + const { t } = useTranslation() + const results = Array.isArray(props.results) ? props.results : [] + + return ( + + + + {t('Generated Results')} + + {props.taskId + ? `${t('Task ID:')} ${props.taskId}` + : t('View generated media results')} + + + + +
+ {results.map((result) => ( + + ))} +
+ {results.length === 0 && ( +
+
+ +
+ {t('No generated media results')} +
+ )} +
+
+
+ ) +} diff --git a/web/default/src/features/usage-logs/constants.ts b/web/default/src/features/usage-logs/constants.ts index c032de410ea7..f1b78ab33e5a 100644 --- a/web/default/src/features/usage-logs/constants.ts +++ b/web/default/src/features/usage-logs/constants.ts @@ -147,6 +147,7 @@ export const TASK_ACTIONS = { // Video generation (camelCase) GENERATE: 'generate', // 图生视频 + ASSET_UPLOAD: 'assetUpload', // 素材上传 TEXT_GENERATE: 'textGenerate', // 文生视频 FIRST_TAIL_GENERATE: 'firstTailGenerate', // 首尾生视频 REFERENCE_GENERATE: 'referenceGenerate', // 参照生视频 @@ -253,6 +254,7 @@ export const TASK_ACTION_MAPPINGS: Record = { [TASK_ACTIONS.MUSIC]: { label: 'Generate Music', variant: 'neutral' }, [TASK_ACTIONS.LYRICS]: { label: 'Generate Lyrics', variant: 'pink' }, [TASK_ACTIONS.GENERATE]: { label: 'Image to Video', variant: 'blue' }, + [TASK_ACTIONS.ASSET_UPLOAD]: { label: 'Asset Upload', variant: 'cyan' }, [TASK_ACTIONS.TEXT_GENERATE]: { label: 'Text to Video', variant: 'blue' }, [TASK_ACTIONS.FIRST_TAIL_GENERATE]: { label: 'First/Last Frame to Video', diff --git a/web/default/src/features/usage-logs/lib/index.ts b/web/default/src/features/usage-logs/lib/index.ts index 271d8a38e057..12407ad83460 100644 --- a/web/default/src/features/usage-logs/lib/index.ts +++ b/web/default/src/features/usage-logs/lib/index.ts @@ -41,5 +41,7 @@ export { taskPlatformMapper, } from './mappers' +export { getTaskActionLabel } from './task-action-label' + // Column utilities export { useColumnsByCategory } from './columns' diff --git a/web/default/src/features/usage-logs/lib/task-action-label.test.ts b/web/default/src/features/usage-logs/lib/task-action-label.test.ts new file mode 100644 index 000000000000..9fcae548bcd8 --- /dev/null +++ b/web/default/src/features/usage-logs/lib/task-action-label.test.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict' + +import { getTaskActionLabel } from './task-action-label.ts' + +assert.equal( + getTaskActionLabel({ upstream_kind: 'asset' }, 'Image to Video'), + 'Asset Upload' +) + +assert.equal( + getTaskActionLabel({ action: 'assetUpload' }, 'Image to Video'), + 'Asset Upload' +) + +assert.equal( + getTaskActionLabel({ upstream_kind: 'image' }, 'Image to Video'), + 'Image Generation' +) + +assert.equal( + getTaskActionLabel({}, 'Image to Video'), + 'Image to Video' +) diff --git a/web/default/src/features/usage-logs/lib/task-action-label.ts b/web/default/src/features/usage-logs/lib/task-action-label.ts new file mode 100644 index 000000000000..47bba02f7691 --- /dev/null +++ b/web/default/src/features/usage-logs/lib/task-action-label.ts @@ -0,0 +1,17 @@ +interface TaskActionLabelInput { + action?: string + upstream_kind?: string +} + +export function getTaskActionLabel( + log: TaskActionLabelInput, + fallbackLabel: string +): string { + if (log.upstream_kind === 'asset' || log.action === 'assetUpload') { + return 'Asset Upload' + } + if (log.upstream_kind === 'image') { + return 'Image Generation' + } + return fallbackLabel +} diff --git a/web/default/src/features/usage-logs/lib/task-media-results.ts b/web/default/src/features/usage-logs/lib/task-media-results.ts new file mode 100644 index 000000000000..89848867632f --- /dev/null +++ b/web/default/src/features/usage-logs/lib/task-media-results.ts @@ -0,0 +1,188 @@ +import { TASK_ACTIONS, TASK_STATUS } from '../constants' +import type { TaskLog } from '../types' + +export type TaskMediaResult = { + type: 'image' | 'video' + url: string +} + +type TaskMediaSource = Pick< + TaskLog, + | 'action' + | 'data' + | 'fail_reason' + | 'result_url' + | 'status' + | 'task_id' + | 'upstream_kind' +> + +const HTTP_URL_PATTERN = /^https?:\/\//i +const IMAGE_URL_PATTERN = /\.(jpe?g|png|webp|gif|bmp|avif)(\?|#|$)/i +const VIDEO_URL_PATTERN = /\.(mp4|webm|mov|m4v|avi|mkv|m3u8)(\?|#|$)/i +const VIDEO_ACTIONS = new Set([ + TASK_ACTIONS.GENERATE, + TASK_ACTIONS.TEXT_GENERATE, + TASK_ACTIONS.FIRST_TAIL_GENERATE, + TASK_ACTIONS.REFERENCE_GENERATE, + TASK_ACTIONS.REMIX_GENERATE, +]) +const RESULT_KEY_PATTERN = /(result|output|generated|media|asset|content)/i +const IMAGE_KEY_PATTERN = /image|img|thumbnail|cover|first_frame|last_frame/i +const VIDEO_KEY_PATTERN = /video/i +const INPUT_KEY_PATTERN = /(request|input|prompt|source|reference|mask)/i + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value) +} + +function parseTaskData(data: unknown): unknown { + if (typeof data !== 'string') return data + const trimmed = data.trim() + if (!trimmed) return undefined + + try { + return JSON.parse(trimmed) as unknown + } catch { + return undefined + } +} + +function isHttpUrl(value: unknown): value is string { + return typeof value === 'string' && HTTP_URL_PATTERN.test(value.trim()) +} + +function looksLikeImageUrl(url: string): boolean { + const lower = url.toLowerCase() + return ( + IMAGE_URL_PATTERN.test(url) || + lower.includes('seedream') || + (lower.includes('tos-') && lower.includes('jpeg')) + ) +} + +function looksLikeVideoUrl(url: string): boolean { + return VIDEO_URL_PATTERN.test(url) +} + +function isStaleImageProxyUrl(url: string, source: TaskMediaSource): boolean { + return ( + source.upstream_kind === 'image' && + url.includes('/v1/videos/') && + url.includes('/content') + ) +} + +function isTaskVideoProxyUrl(url: string, source: TaskMediaSource): boolean { + return Boolean( + source.task_id && url.includes(`/v1/videos/${source.task_id}/content`) + ) +} + +function inferMediaType( + url: string, + keyHint: string | undefined, + source: TaskMediaSource, + allowTaskFallback: boolean +): TaskMediaResult['type'] | undefined { + const normalizedKey = keyHint?.toLowerCase() ?? '' + if (INPUT_KEY_PATTERN.test(normalizedKey)) return undefined + + if (IMAGE_KEY_PATTERN.test(normalizedKey) || looksLikeImageUrl(url)) { + return 'image' + } + if (VIDEO_KEY_PATTERN.test(normalizedKey) || looksLikeVideoUrl(url)) { + return 'video' + } + if (allowTaskFallback) { + if (source.upstream_kind === 'image') return 'image' + if (source.upstream_kind === 'video') return 'video' + if (VIDEO_ACTIONS.has(source.action)) return 'video' + } + return undefined +} + +function addMediaResult( + results: TaskMediaResult[], + seen: Set, + source: TaskMediaSource, + urlValue: unknown, + keyHint?: string, + allowTaskFallback: boolean = false +): void { + if (!isHttpUrl(urlValue)) return + + const url = urlValue.trim() + if (isStaleImageProxyUrl(url, source)) return + if ( + isTaskVideoProxyUrl(url, source) && + results.some((result) => result.type === 'image') + ) { + return + } + if (seen.has(url)) return + + const type = inferMediaType(url, keyHint, source, allowTaskFallback) + if (!type) return + + seen.add(url) + results.push({ type, url }) +} + +function walkTaskData( + value: unknown, + source: TaskMediaSource, + results: TaskMediaResult[], + seen: Set, + keyHint?: string +): void { + if (Array.isArray(value)) { + for (const item of value) { + walkTaskData(item, source, results, seen, keyHint) + } + return + } + + if (!isRecord(value)) { + addMediaResult( + results, + seen, + source, + value, + keyHint, + RESULT_KEY_PATTERN.test(keyHint ?? '') + ) + return + } + + for (const [key, nestedValue] of Object.entries(value)) { + const nestedKeyHint = keyHint ? `${keyHint}.${key}` : key + const allowTaskFallback = RESULT_KEY_PATTERN.test(nestedKeyHint) + if (isHttpUrl(nestedValue)) { + addMediaResult( + results, + seen, + source, + nestedValue, + nestedKeyHint, + allowTaskFallback + ) + continue + } + + walkTaskData(nestedValue, source, results, seen, nestedKeyHint) + } +} + +export function extractTaskMediaResults(source: TaskMediaSource): TaskMediaResult[] { + if (source.status !== TASK_STATUS.SUCCESS) return [] + + const results: TaskMediaResult[] = [] + const seen = new Set() + + addMediaResult(results, seen, source, source.result_url, 'result_url', true) + addMediaResult(results, seen, source, source.fail_reason, 'fail_reason', true) + walkTaskData(parseTaskData(source.data), source, results, seen) + + return results +} diff --git a/web/default/src/features/usage-logs/types.ts b/web/default/src/features/usage-logs/types.ts index 9fcda2258455..4931d67ab61a 100644 --- a/web/default/src/features/usage-logs/types.ts +++ b/web/default/src/features/usage-logs/types.ts @@ -225,8 +225,10 @@ export interface TaskLog { finish_time?: number // seconds progress?: string progress_message_en?: string - data?: string // JSON string + data?: unknown // JSON payload from task providers fail_reason?: string + result_url?: string + upstream_kind?: string status: string // NOT_START, SUBMITTED, IN_PROGRESS, SUCCESS, FAILURE, QUEUED, UNKNOWN other?: string created_at?: number diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 1db6334dca07..d3775477a336 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -638,6 +638,7 @@ "Click the button below to bind your Telegram account": "Click the button below to bind your Telegram account", "Click to open deployment": "Click to open deployment", "Click to preview audio": "Click to preview audio", + "Click to preview image": "Click to preview image", "Click to preview video": "Click to preview video", "Click to update balance": "Click to update balance", "Click to view Codex usage": "Click to view Codex usage", @@ -1639,7 +1640,9 @@ "Generate Music": "Generate Music", "Generate new backup codes for account recovery": "Generate new backup codes for account recovery", "Generate New Codes": "Generate New Codes", + "Generated Results": "Generated Results", "Generated image": "Generated image", + "Generated video": "Generated video", "Generating new codes will invalidate all existing backup codes.": "Generating new codes will invalidate all existing backup codes.", "Generating...": "Generating...", "Generic cache": "Generic cache", @@ -2253,6 +2256,7 @@ "No mappings configured. Click \"Add Row\" to get started.": "No mappings configured. Click \"Add Row\" to get started.", "No matches found": "No matches found", "No matching results": "No matching results", + "No generated media results": "No generated media results", "No matching rules": "No matching rules", "No messages yet": "No messages yet", "No missing models found.": "No missing models found.", @@ -3782,6 +3786,8 @@ "View the complete error message and details": "View the complete error message and details", "View the complete prompt and its English translation": "View the complete prompt and its English translation", "View the generated image": "View the generated image", + "View generated media results": "View generated media results", + "View generated results": "View generated results", "View user consumption statistics and charts": "View user consumption statistics and charts", "View your topup transaction records and payment history": "View your topup transaction records and payment history", "Violation Code": "Violation Code", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 63f25de058e8..2dcae6eefffe 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -638,6 +638,7 @@ "Click the button below to bind your Telegram account": "Cliquez sur le bouton ci-dessous pour lier votre compte Telegram", "Click to open deployment": "Cliquez pour ouvrir le déploiement", "Click to preview audio": "Cliquer pour prévisualiser l'audio", + "Click to preview image": "Cliquer pour prévisualiser l’image", "Click to preview video": "Cliquer pour prévisualiser la vidéo", "Click to update balance": "Cliquez pour mettre à jour le solde", "Click to view Codex usage": "Cliquer pour voir l'utilisation Codex", @@ -1639,7 +1640,9 @@ "Generate Music": "Générer de la musique", "Generate new backup codes for account recovery": "Générer de nouveaux codes de secours pour la récupération du compte", "Generate New Codes": "Générer de nouveaux codes", + "Generated Results": "Résultats générés", "Generated image": "Image générée", + "Generated video": "Vidéo générée", "Generating new codes will invalidate all existing backup codes.": "La génération de nouveaux codes invalidera tous les codes de sauvegarde existants.", "Generating...": "Génération...", "Generic cache": "Cache générique", @@ -2253,6 +2256,7 @@ "No mappings configured. Click \"Add Row\" to get started.": "Aucun mappage configuré. Cliquez sur « Ajouter une ligne » pour commencer.", "No matches found": "Aucune correspondance trouvée", "No matching results": "Aucun résultat correspondant", + "No generated media results": "Aucun média généré", "No matching rules": "Aucune règle correspondante", "No messages yet": "Pas encore de messages", "No missing models found.": "Aucun modèle manquant trouvé.", @@ -3782,6 +3786,8 @@ "View the complete error message and details": "Voir le message d'erreur et les détails complets", "View the complete prompt and its English translation": "Voir l'invite complète et sa traduction anglaise", "View the generated image": "Voir l'image générée", + "View generated media results": "Voir les médias générés", + "View generated results": "Voir les résultats générés", "View user consumption statistics and charts": "Voir les statistiques et graphiques de consommation", "View your topup transaction records and payment history": "Afficher vos enregistrements de transactions de recharge et votre historique de paiement", "Violation Code": "Code de violation", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index d7d314368a1a..e13e05bc83df 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -638,6 +638,7 @@ "Click the button below to bind your Telegram account": "下のボタンをクリックしてTelegramアカウントをバインドしてください", "Click to open deployment": "クリックして展開を開く", "Click to preview audio": "クリックして音声をプレビュー", + "Click to preview image": "クリックして画像をプレビュー", "Click to preview video": "クリックして動画をプレビュー", "Click to update balance": "クリックして残高を更新", "Click to view Codex usage": "Codex利用状況を見る", @@ -1639,7 +1640,9 @@ "Generate Music": "音楽を生成", "Generate new backup codes for account recovery": "アカウント復旧用の新しいバックアップコードを生成", "Generate New Codes": "新しいコードを生成", + "Generated Results": "生成結果", "Generated image": "生成された画像", + "Generated video": "生成された動画", "Generating new codes will invalidate all existing backup codes.": "新しいコードを生成すると、既存のすべてのバックアップコードが無効になります。", "Generating...": "生成中...", "Generic cache": "汎用キャッシュ", @@ -2253,6 +2256,7 @@ "No mappings configured. Click \"Add Row\" to get started.": "マッピングが設定されていません。「行を追加」をクリックして開始してください。", "No matches found": "一致するものが見つかりません", "No matching results": "一致する結果がありません", + "No generated media results": "生成されたメディア結果はありません", "No matching rules": "一致するルールがありません", "No messages yet": "まだメッセージがありません", "No missing models found.": "不足しているモデルは見つかりません。", @@ -3782,6 +3786,8 @@ "View the complete error message and details": "エラーメッセージと詳細を表示", "View the complete prompt and its English translation": "プロンプト全文と英語訳を表示", "View the generated image": "生成された画像を表示", + "View generated media results": "生成されたメディア結果を表示", + "View generated results": "生成結果を表示", "View user consumption statistics and charts": "ユーザー消費統計とチャートを表示", "View your topup transaction records and payment history": "チャージ取引記録と支払い履歴を表示", "Violation Code": "違反コード", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 10bed5398a41..8ddd7773e98b 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -638,6 +638,7 @@ "Click the button below to bind your Telegram account": "Нажмите кнопку ниже, чтобы привязать ваш аккаунт Telegram", "Click to open deployment": "Нажмите, чтобы открыть развертывание", "Click to preview audio": "Нажмите для предпросмотра аудио", + "Click to preview image": "Нажмите, чтобы просмотреть изображение", "Click to preview video": "Нажмите, чтобы просмотреть видео", "Click to update balance": "Нажмите, чтобы обновить баланс", "Click to view Codex usage": "Нажмите, чтобы посмотреть использование Codex", @@ -1639,7 +1640,9 @@ "Generate Music": "Создать музыку", "Generate new backup codes for account recovery": "Сгенерировать новые резервные коды для восстановления аккаунта", "Generate New Codes": "Сгенерировать новые коды", + "Generated Results": "Сгенерированные результаты", "Generated image": "Сгенерированное изображение", + "Generated video": "Сгенерированное видео", "Generating new codes will invalidate all existing backup codes.": "Генерация новых кодов аннулирует все существующие резервные коды.", "Generating...": "Создание...", "Generic cache": "Общий кэш", @@ -2253,6 +2256,7 @@ "No mappings configured. Click \"Add Row\" to get started.": "Нет настроенных сопоставлений. Нажмите \"Добавить строку\", чтобы начать.", "No matches found": "Совпадений не найдено", "No matching results": "Нет совпадений", + "No generated media results": "Нет сгенерированных медиафайлов", "No matching rules": "Нет совпадающих правил", "No messages yet": "Сообщений пока нет", "No missing models found.": "Недостающие модели не найдены.", @@ -3782,6 +3786,8 @@ "View the complete error message and details": "Просмотр полного сообщения об ошибке и деталей", "View the complete prompt and its English translation": "Просмотр полного промпта и его перевода на английский", "View the generated image": "Просмотр сгенерированного изображения", + "View generated media results": "Просмотреть сгенерированные медиафайлы", + "View generated results": "Просмотреть сгенерированные результаты", "View user consumption statistics and charts": "Просмотр статистики и графиков потребления", "View your topup transaction records and payment history": "Просмотреть записи о пополнении счета и историю платежей", "Violation Code": "Код нарушения", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index ba127d8b0c14..5a5e0aafce4c 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -638,6 +638,7 @@ "Click the button below to bind your Telegram account": "Nhấp vào nút bên dưới để liên kết tài khoản Telegram của bạn", "Click to open deployment": "Nhấp để mở triển khai", "Click to preview audio": "Nhấp để xem trước âm thanh", + "Click to preview image": "Nhấp để xem trước hình ảnh", "Click to preview video": "Nhấp để xem trước video", "Click to update balance": "Nhấp để cập nhật số dư", "Click to view Codex usage": "Nhấp để xem mức sử dụng Codex", @@ -1639,7 +1640,9 @@ "Generate Music": "Tạo nhạc", "Generate new backup codes for account recovery": "Tạo mã dự phòng mới để khôi phục tài khoản", "Generate New Codes": "Tạo mã mới", + "Generated Results": "Kết quả đã tạo", "Generated image": "Ảnh được tạo", + "Generated video": "Video được tạo", "Generating new codes will invalidate all existing backup codes.": "Tạo mã mới sẽ vô hiệu hóa tất cả các mã dự phòng hiện có.", "Generating...": "Đang tạo...", "Generic cache": "Bộ đệm chung", @@ -2253,6 +2256,7 @@ "No mappings configured. Click \"Add Row\" to get started.": "Chưa có ánh xạ nào được cấu hình. Nhấp vào \"Thêm hàng\" để bắt đầu.", "No matches found": "Không tìm thấy kết quả nào", "No matching results": "Không có kết quả phù hợp", + "No generated media results": "Không có kết quả phương tiện được tạo", "No matching rules": "Không có quy tắc phù hợp", "No messages yet": "Chưa có tin nhắn", "No missing models found.": "Không tìm thấy mô hình nào bị thiếu.", @@ -3782,6 +3786,8 @@ "View the complete error message and details": "Xem toàn bộ thông báo lỗi và chi tiết", "View the complete prompt and its English translation": "Xem toàn bộ lời nhắc và bản dịch tiếng Anh", "View the generated image": "Xem ảnh đã tạo", + "View generated media results": "Xem kết quả phương tiện đã tạo", + "View generated results": "Xem kết quả đã tạo", "View user consumption statistics and charts": "Xem thống kê và biểu đồ tiêu thụ", "View your topup transaction records and payment history": "Xem lịch sử giao dịch nạp tiền và lịch sử thanh toán của bạn", "Violation Code": "Mã vi phạm", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 87bb917f4b96..fba154c6a49c 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -638,6 +638,7 @@ "Click the button below to bind your Telegram account": "点击下方按钮绑定您的 Telegram 账户", "Click to open deployment": "点击打开部署", "Click to preview audio": "点击预览音乐", + "Click to preview image": "点击预览图片", "Click to preview video": "点击预览视频", "Click to update balance": "点击更新余额", "Click to view Codex usage": "点击查看 Codex 使用量", @@ -1639,7 +1640,9 @@ "Generate Music": "生成音乐", "Generate new backup codes for account recovery": "生成新的备份代码用于账户恢复", "Generate New Codes": "生成新代码", + "Generated Results": "生成结果", "Generated image": "生成的图像", + "Generated video": "生成的视频", "Generating new codes will invalidate all existing backup codes.": "生成新代码将使所有现有备份代码失效。", "Generating...": "生成中...", "Generic cache": "通用缓存", @@ -2253,6 +2256,7 @@ "No mappings configured. Click \"Add Row\" to get started.": "未配置映射。点击 \"添加行\" 开始。", "No matches found": "未找到匹配项", "No matching results": "无匹配结果", + "No generated media results": "没有生成的媒体结果", "No matching rules": "没有匹配的规则", "No messages yet": "暂无消息", "No missing models found.": "未找到缺失的模型。", @@ -3782,6 +3786,8 @@ "View the complete error message and details": "查看完整错误信息与详情", "View the complete prompt and its English translation": "查看完整提示词及其英文翻译", "View the generated image": "查看生成的图片", + "View generated media results": "查看生成的媒体结果", + "View generated results": "查看生成结果", "View user consumption statistics and charts": "查看用户消耗统计和图表", "View your topup transaction records and payment history": "查看您的充值交易记录和付款历史", "Violation Code": "违规代码", diff --git a/web/default/src/lib/lobe-icon.tsx b/web/default/src/lib/lobe-icon.tsx index c224652a7293..d496d5dceb8d 100644 --- a/web/default/src/lib/lobe-icon.tsx +++ b/web/default/src/lib/lobe-icon.tsx @@ -9,6 +9,22 @@ */ import * as LobeIcons from '@lobehub/icons' +const EXTERNAL_ICON_SUFFIXES = ['.Color'] as const + +function normalizeExternalIconUrl(iconName: string): string | null { + if (!/^https?:\/\//.test(iconName)) { + return null + } + + for (const suffix of EXTERNAL_ICON_SUFFIXES) { + if (iconName.endsWith(suffix)) { + return iconName.slice(0, -suffix.length) + } + } + + return iconName +} + /** * Parse a property value from string to appropriate type * @param raw - Raw string value @@ -81,6 +97,19 @@ export function getLobeIcon( ) } + const externalIconUrl = normalizeExternalIconUrl(trimmedName) + if (externalIconUrl) { + return ( + + ) + } + // Parse component path and chained properties const segments = trimmedName.split('.') const baseKey = segments[0] diff --git a/web/default/tests/channel-utils.test.ts b/web/default/tests/channel-utils.test.ts new file mode 100644 index 000000000000..be8517f94a7d --- /dev/null +++ b/web/default/tests/channel-utils.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from 'bun:test' + +import { + getChannelTypeIcon, + getChannelTypeLabel, +} from '../src/features/channels/lib/channel-utils' + +describe('channel type mappings', () => { + test('maps PingXingShiJie channel type to its label and favicon URL', () => { + expect(getChannelTypeLabel(58)).toBe('PingXingShiJie') + expect(getChannelTypeIcon(58)).toBe( + 'https://www.pingxingshijie.cn/favicon.ico' + ) + }) + + test('maps KieAI channel type to its label and logo URL', () => { + expect(getChannelTypeLabel(59)).toBe('KieAI') + expect(getChannelTypeIcon(59)).toBe('https://kie.ai/logo.png') + }) +}) diff --git a/web/default/tests/lobe-icon.test.tsx b/web/default/tests/lobe-icon.test.tsx new file mode 100644 index 000000000000..df47af3ab355 --- /dev/null +++ b/web/default/tests/lobe-icon.test.tsx @@ -0,0 +1,27 @@ +import { describe, expect, test } from 'bun:test' +import { isValidElement, type ReactElement, type ReactNode } from 'react' + +import { getLobeIcon } from '../src/lib/lobe-icon' + +interface ImageIconProps { + alt?: string + src?: string +} + +function getImageProps(node: ReactNode): ImageIconProps { + if (!isValidElement(node)) { + throw new Error('Expected a valid React element') + } + + const element: ReactElement = node + return element.props +} + +describe('getLobeIcon', () => { + test('renders external logo URLs even when channel callers append color suffix', () => { + const props = getImageProps(getLobeIcon('https://kie.ai/logo.png.Color', 16)) + + expect(props.src).toBe('https://kie.ai/logo.png') + expect(props.alt).toBe('') + }) +}) diff --git a/web/default/tests/task-media-results.test.ts b/web/default/tests/task-media-results.test.ts new file mode 100644 index 000000000000..df9188d31d47 --- /dev/null +++ b/web/default/tests/task-media-results.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, test } from 'bun:test' + +import { extractTaskMediaResults } from '../src/features/usage-logs/lib/task-media-results' + +describe('extractTaskMediaResults', () => { + test('extracts multiple image URLs from task data and result URL', () => { + const results = extractTaskMediaResults({ + action: 'GENERATE', + data: JSON.stringify({ + data: [ + { url: 'https://example.com/first.png' }, + { image_url: 'https://example.com/second.webp' }, + ], + }), + result_url: 'https://example.com/cover.jpg', + status: 'SUCCESS', + task_id: 'task-image', + upstream_kind: 'image', + }) + + expect(results).toEqual([ + { type: 'image', url: 'https://example.com/cover.jpg' }, + { type: 'image', url: 'https://example.com/first.png' }, + { type: 'image', url: 'https://example.com/second.webp' }, + ]) + }) + + test('extracts multiple video URLs from nested task payloads', () => { + const results = extractTaskMediaResults({ + action: 'TEXT_GENERATE', + data: { + content: { video_url: 'https://example.com/generated.mp4' }, + videos: [{ url: 'https://cdn.example.com/alt.webm' }], + }, + result_url: 'https://example.com/generated.mp4', + status: 'SUCCESS', + task_id: 'task-video', + upstream_kind: 'video', + }) + + expect(results).toEqual([ + { type: 'video', url: 'https://example.com/generated.mp4' }, + { type: 'video', url: 'https://cdn.example.com/alt.webm' }, + ]) + }) + + test('uses legacy fail reason URL when result URL is absent', () => { + const results = extractTaskMediaResults({ + action: 'GENERATE', + fail_reason: 'https://legacy.example.com/result.mp4', + status: 'SUCCESS', + task_id: 'task-legacy', + }) + + expect(results).toEqual([ + { type: 'video', url: 'https://legacy.example.com/result.mp4' }, + ]) + }) + + test('ignores stale video proxy URL for image tasks and avoids input-only URLs', () => { + const results = extractTaskMediaResults({ + action: 'GENERATE', + data: { + request: { + input_image: 'https://uploads.example.com/user-input.png', + }, + data: [{ url: 'https://example.com/generated-seedream.jpeg' }], + }, + result_url: 'https://api.example.com/v1/videos/task-image/content', + status: 'SUCCESS', + task_id: 'task-image', + upstream_kind: 'image', + }) + + expect(results).toEqual([ + { type: 'image', url: 'https://example.com/generated-seedream.jpeg' }, + ]) + }) + + test('ignores legacy video proxy fail reason when image result already exists', () => { + const results = extractTaskMediaResults({ + action: 'GENERATE', + fail_reason: 'https://api.example.com/v1/videos/task-legacy-image/content', + result_url: 'https://example.com/generated-image.png', + status: 'SUCCESS', + task_id: 'task-legacy-image', + }) + + expect(results).toEqual([ + { type: 'image', url: 'https://example.com/generated-image.png' }, + ]) + }) +})