{@code QueryHunyuanTo3DRapidJob} — polls Status ∈ {WAIT, RUN, DONE, FAIL}
+ *
Region: {@code ap-guangzhou} (the only documented region for ai3d)
+ *
Concurrency: 1 task by default — {@link vip.mate.tool.ConcurrencyUnsafe} on the tool ensures the agent doesn't fire two in parallel
+ *
+ *
+ *
Result delivery contract
+ * On {@code DONE}, {@link #checkStatus} returns
+ * {@code TaskPollResult.succeeded(null, null, )} where {@code } is
+ * {@code {"modelUrl": "", "format": "glb"}}. The
+ * {@code Model3dGenerationService.handleCompletion} parses that out, downloads
+ * the asset, and persists a {@code model3d} MessageContentPart.
+ */
+@Slf4j
+@Component
+public class HunyuanModel3dProvider implements Model3dGenerationProvider {
+
+ private final ObjectMapper objectMapper;
+ private final ModelProviderService modelProviderService;
+
+ public HunyuanModel3dProvider(ObjectMapper objectMapper,
+ ModelProviderService modelProviderService) {
+ this.objectMapper = objectMapper;
+ this.modelProviderService = modelProviderService;
+ }
+
+ private static final String PROVIDER_ID = "hunyuan-3d";
+ private static final String SERVICE = "ai3d";
+ private static final String VERSION = "2025-05-13";
+ private static final String REGION = "ap-guangzhou";
+ private static final String DEFAULT_HOST = "ai3d.tencentcloudapi.com";
+
+ // Two action families exist on the same ai3d service. Model name routes
+ // the request to the right one — see V72 migration for the model -> action
+ // mapping. We dispatch QueryHunyuanTo3DRapidJob vs Pro the same way: the
+ // task entity's `provider_task_id` carries a "rapid:" or "pro:" prefix so
+ // checkStatus knows which Action to call without re-reading model config.
+ private static final String SUBMIT_RAPID = "SubmitHunyuanTo3DRapidJob";
+ private static final String QUERY_RAPID = "QueryHunyuanTo3DRapidJob";
+ private static final String SUBMIT_PRO = "SubmitHunyuanTo3DProJob";
+ private static final String QUERY_PRO = "QueryHunyuanTo3DProJob";
+
+ private static final String DEFAULT_MODEL = "HY-3D-3.1";
+
+ @Override public String id() { return PROVIDER_ID; }
+ @Override public String label() { return "Tencent Hunyuan 3D"; }
+ @Override public boolean requiresCredential() { return true; }
+ @Override public int autoDetectOrder() { return 100; }
+
+ @Override
+ public Set capabilities() {
+ return Set.of(Model3dCapability.TEXT_TO_3D, Model3dCapability.IMAGE_TO_3D);
+ }
+
+ @Override
+ public Model3dProviderCapabilities detailedCapabilities() {
+ return Model3dProviderCapabilities.builder()
+ .modes(capabilities())
+ .supportedFormats(List.of("glb"))
+ .supportsTexture(true)
+ // PBR is supported on the Pro action only (HY-3D-3.0 / HY-3D-3.1);
+ // exposed at the capability level so future routing can negotiate.
+ .supportsPbr(true)
+ .defaultModel(DEFAULT_MODEL)
+ .models(List.of("HY-3D-3.1", "HY-3D-3.0", "HY-3D-Express"))
+ .build();
+ }
+
+ /** Whether the requested model variant goes through the Pro action family. */
+ private static boolean usePro(String model) {
+ if (model == null || model.isBlank()) return true; // default 3.1 -> Pro
+ String upper = model.toUpperCase();
+ if (upper.contains("EXPRESS") || upper.contains("RAPID")) return false;
+ return true; // HY-3D-3.x -> Pro
+ }
+
+ @Override
+ public boolean isAvailable(SystemSettingsDTO config) {
+ return resolveCredentials() != null;
+ }
+
+ private record Credentials(String secretId, String secretKey, String host) {}
+
+ private Credentials resolveCredentials() {
+ try {
+ if (!modelProviderService.isProviderConfigured(PROVIDER_ID)) {
+ return null;
+ }
+ ModelProviderEntity entity = modelProviderService.getProviderConfig(PROVIDER_ID);
+ String apiKey = entity.getApiKey();
+ if (!StringUtils.hasText(apiKey)) return null;
+ // api_key column packs both halves as "SecretId:SecretKey"
+ String[] parts = apiKey.split(":", 2);
+ if (parts.length != 2 || parts[0].isBlank() || parts[1].isBlank()) {
+ log.warn("[Hunyuan3D] Provider api_key must be \"SecretId:SecretKey\" (colon-joined)");
+ return null;
+ }
+ String host = StringUtils.hasText(entity.getBaseUrl())
+ ? extractHost(entity.getBaseUrl())
+ : DEFAULT_HOST;
+ return new Credentials(parts[0].trim(), parts[1].trim(), host);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /** Reduce a configured base_url to bare host (strip scheme + path). */
+ private static String extractHost(String baseUrl) {
+ try {
+ java.net.URI uri = java.net.URI.create(baseUrl.trim());
+ if (uri.getHost() != null) return uri.getHost();
+ } catch (Exception ignore) {
+ // fall through
+ }
+ return DEFAULT_HOST;
+ }
+
+ @Override
+ public Model3dSubmitResult submit(Model3dGenerationRequest request, SystemSettingsDTO config) {
+ Credentials creds = resolveCredentials();
+ if (creds == null) {
+ return Model3dSubmitResult.failure(id(),
+ "Hunyuan 3D 凭据未配置(在「模型与凭据」中以 SecretId:SecretKey 格式保存到 hunyuan-3d)");
+ }
+ try {
+ boolean pro = usePro(request.getModel());
+ ObjectNode body = objectMapper.createObjectNode();
+
+ // Both Rapid and Pro accept Prompt XOR ImageUrl. Pro additionally
+ // takes MultiViewImages and several quality knobs (EnablePBR,
+ // FaceCount, GenerateType). Pro also supports Prompt + ImageUrl
+ // together when GenerateType="Sketch", but we don't expose Sketch
+ // mode at the tool layer yet — keep XOR semantics for now.
+ if (StringUtils.hasText(request.getImageUrl())) {
+ body.put("ImageUrl", request.getImageUrl());
+ } else if (StringUtils.hasText(request.getPrompt())) {
+ body.put("Prompt", request.getPrompt());
+ } else {
+ return Model3dSubmitResult.failure(id(),
+ "缺少必要参数:请提供 prompt(文生 3D)或 imageUrl(图生 3D)");
+ }
+
+ if (pro) {
+ // Multi-view: map up to 3 extra views to ViewType={back,left,right}.
+ // Tencent limits each angle to one image; ordering of imageUrls
+ // beyond the primary determines the angle slot.
+ List extraViews = request.getImageUrls();
+ if (extraViews != null && !extraViews.isEmpty()) {
+ String[] viewTypes = {"back", "left", "right"};
+ com.fasterxml.jackson.databind.node.ArrayNode arr = body.putArray("MultiViewImages");
+ for (int i = 0; i < extraViews.size() && i < viewTypes.length; i++) {
+ String url = extraViews.get(i);
+ if (!StringUtils.hasText(url)) continue;
+ ObjectNode view = arr.addObject();
+ view.put("ViewType", viewTypes[i]);
+ view.put("ViewImageUrl", url);
+ }
+ }
+ if (Boolean.TRUE.equals(request.getEnablePbr())) {
+ body.put("EnablePBR", true);
+ }
+ // White-model toggle: when texture is explicitly disabled, ask
+ // for the geometry-only generate type.
+ if (Boolean.FALSE.equals(request.getEnableTexture())) {
+ body.put("GenerateType", "Geometry");
+ }
+ }
+
+ String action = pro ? SUBMIT_PRO : SUBMIT_RAPID;
+ JsonNode resp = invoke(creds, action, body.toString());
+ JsonNode response = resp.path("Response");
+ if (response.has("Error")) {
+ String code = response.path("Error").path("Code").asText("UnknownError");
+ String msg = response.path("Error").path("Message").asText("Unknown error");
+ log.warn("[Hunyuan3D] {} failed: {} ({})", action, msg, code);
+ return Model3dSubmitResult.failure(id(), msg + " (" + code + ")");
+ }
+ String jobId = response.path("JobId").asText(null);
+ if (!StringUtils.hasText(jobId)) {
+ return Model3dSubmitResult.failure(id(), "Tencent 未返回 JobId");
+ }
+ // Embed the action family in the providerTaskId so checkStatus
+ // dispatches to the matching Query action without re-reading the
+ // model from the request entity (which is JSON-serialized and
+ // would require an extra DB round-trip).
+ String taggedJobId = (pro ? "pro:" : "rapid:") + jobId;
+ log.info("[Hunyuan3D] {} submitted job: {} (model={})",
+ action, jobId, request.getModel());
+ return Model3dSubmitResult.success(taggedJobId, id());
+ } catch (Exception e) {
+ log.error("[Hunyuan3D] Submit error: {}", e.getMessage(), e);
+ return Model3dSubmitResult.failure(id(), e.getMessage());
+ }
+ }
+
+ @Override
+ public TaskPollResult checkStatus(String providerTaskId, SystemSettingsDTO config) {
+ Credentials creds = resolveCredentials();
+ if (creds == null) return TaskPollResult.failed("Hunyuan 3D 凭据未配置");
+
+ try {
+ // providerTaskId carries a "rapid:" / "pro:" prefix from submit().
+ String queryAction;
+ String rawJobId;
+ if (providerTaskId != null && providerTaskId.startsWith("pro:")) {
+ queryAction = QUERY_PRO;
+ rawJobId = providerTaskId.substring(4);
+ } else if (providerTaskId != null && providerTaskId.startsWith("rapid:")) {
+ queryAction = QUERY_RAPID;
+ rawJobId = providerTaskId.substring(6);
+ } else {
+ // Backward-compat for any unprefixed jobs that may still be in
+ // flight from a previous version: default to Rapid.
+ queryAction = QUERY_RAPID;
+ rawJobId = providerTaskId;
+ }
+
+ ObjectNode body = objectMapper.createObjectNode();
+ body.put("JobId", rawJobId);
+
+ JsonNode resp = invoke(creds, queryAction, body.toString());
+ JsonNode response = resp.path("Response");
+ if (response.has("Error")) {
+ String msg = response.path("Error").path("Message").asText("Unknown error");
+ return TaskPollResult.failed(msg);
+ }
+
+ String status = response.path("Status").asText("");
+ switch (status) {
+ case "DONE":
+ PickedFile picked = pickBestResultFile(response);
+ if (picked == null || !StringUtils.hasText(picked.url())) {
+ return TaskPollResult.failed("Tencent 任务完成但未返回模型 URL");
+ }
+ // Stuff modelUrl + format into resultJson — the service layer parses it.
+ ObjectNode result = objectMapper.createObjectNode();
+ result.put("modelUrl", picked.url());
+ result.put("format", picked.type());
+ return TaskPollResult.succeeded(null, null, result.toString());
+ case "FAIL":
+ String errCode = response.path("ErrorCode").asText(null);
+ String errMsg = response.path("ErrorMessage").asText("任务失败");
+ return TaskPollResult.failed(errCode != null
+ ? errMsg + " (" + errCode + ")" : errMsg);
+ case "RUN":
+ return TaskPollResult.running(null);
+ case "WAIT":
+ default:
+ return TaskPollResult.pending(null);
+ }
+ } catch (Exception e) {
+ log.error("[Hunyuan3D] Poll error for job {}: {}", providerTaskId, e.getMessage());
+ return null;
+ }
+ }
+
+ private record PickedFile(String url, String type) {}
+
+ /**
+ * Pick the best file from {@code ResultFile3Ds[]} for our pipeline.
+ *
+ * Tencent Pro returns multiple {@code File3D} entries per job — typically
+ * one each of GLB / OBJ / FBX (and possibly STL/USDZ). The OBJ entry's
+ * {@code Url} actually points at a {@code .zip} bundle (obj + textures +
+ * mtl), which {@code } can't render directly. The GLB entry
+ * is a single self-contained binary that is renderable. So we
+ * prefer GLB → FBX → OBJ → first-available, ignoring the URL extension.
+ */
+ private static PickedFile pickBestResultFile(JsonNode response) {
+ JsonNode files = response.path("ResultFile3Ds");
+ if (!files.isArray() || files.isEmpty()) return null;
+
+ PickedFile glb = null, fbx = null, obj = null, anyFile = null;
+ for (JsonNode f : files) {
+ String type = f.path("Type").asText("").toLowerCase();
+ String url = f.path("Url").asText(null);
+ if (!StringUtils.hasText(url)) continue;
+ PickedFile entry = new PickedFile(url, type.isBlank() ? "glb" : type);
+ if (anyFile == null) anyFile = entry;
+ switch (type) {
+ case "glb" -> { if (glb == null) glb = entry; }
+ case "fbx" -> { if (fbx == null) fbx = entry; }
+ case "obj" -> { if (obj == null) obj = entry; }
+ default -> { /* stl / usdz / unknown — only used as last resort */ }
+ }
+ }
+ if (glb != null) return glb;
+ if (fbx != null) return fbx;
+ if (obj != null) return obj;
+ return anyFile;
+ }
+
+ /**
+ * Sign + send a TC3 v3 request. Tencent's gateway returns 200 OK with an
+ * {@code Error} body on logical failure, so we return the parsed
+ * {@code JsonNode} regardless of status — the caller inspects it.
+ */
+ private JsonNode invoke(Credentials creds, String action, String payload) throws Exception {
+ TencentCloudV3Signer.SignedHeaders sig =
+ TencentCloudV3Signer.sign(creds.secretId(), creds.secretKey(), SERVICE, creds.host(), payload);
+ Map common = TencentCloudV3Signer.commonHeaders(action, VERSION, REGION, sig.timestamp());
+
+ HttpRequest req = HttpRequest.post("https://" + creds.host())
+ .header("Content-Type", "application/json; charset=utf-8")
+ .header("Host", sig.host())
+ .header("Authorization", sig.authorization())
+ .body(payload)
+ .timeout(60_000);
+ for (Map.Entry e : common.entrySet()) {
+ req.header(e.getKey(), e.getValue());
+ }
+ try (HttpResponse response = req.execute()) {
+ return objectMapper.readTree(response.body());
+ }
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/tool/model3d/provider/TencentCloudV3Signer.java b/mateclaw-server/src/main/java/vip/mate/tool/model3d/provider/TencentCloudV3Signer.java
new file mode 100644
index 00000000..7965e287
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/tool/model3d/provider/TencentCloudV3Signer.java
@@ -0,0 +1,151 @@
+package vip.mate.tool.model3d.provider;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Tencent Cloud TC3-HMAC-SHA256 v3 request signer.
+ *
+ * Spec: 签名方法 v3.
+ * Implements only the JSON / POST flavor we need for the
+ * {@code ai3d} service. Stateless — each call to {@link #sign} produces a
+ * fresh {@code Authorization} header for the supplied request.
+ *
+ *
Why hand-rolled instead of {@code tencentcloud-sdk-java}: the SDK pulls in
+ * ~30 MB of dependencies covering 200+ products. We need exactly two endpoints
+ * on one product, and an isolated 80-line signer keeps the dependency surface
+ * minimal and testable.
+ */
+public final class TencentCloudV3Signer {
+
+ private TencentCloudV3Signer() {}
+
+ public record SignedHeaders(
+ String authorization,
+ String timestamp,
+ String host
+ ) {}
+
+ /**
+ * Compute the {@code Authorization} header for a Tencent Cloud v3 POST request.
+ *
+ * @param secretId Tencent Cloud SecretId
+ * @param secretKey Tencent Cloud SecretKey
+ * @param service Service name in lower-case, e.g. {@code "ai3d"}
+ * @param host API host, e.g. {@code "ai3d.tencentcloudapi.com"}
+ * @param payload Request body (JSON for POST)
+ * @return headers (Authorization, X-TC-Timestamp, Host) ready to attach to the HTTP request.
+ */
+ public static SignedHeaders sign(String secretId, String secretKey,
+ String service, String host, String payload) {
+ long timestamp = System.currentTimeMillis() / 1000L;
+ return signAt(secretId, secretKey, service, host, payload, timestamp);
+ }
+
+ /** Internal entry for tests — accepts a fixed timestamp. */
+ static SignedHeaders signAt(String secretId, String secretKey, String service,
+ String host, String payload, long timestamp) {
+ String date = utcDate(timestamp);
+
+ // ----- Step 1: canonical request -----
+ String hashedPayload = sha256Hex(payload == null ? "" : payload);
+ String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
+ + "host:" + host + "\n"
+ + "x-tc-action:"; // Action header value provided by caller via X-TC-Action; spec requires
+ // it to be lowercased here. We append it via the placeholder below.
+ // Note: TC3 does NOT include x-tc-action in the canonical headers — it's
+ // part of the request, not the signature. Recompute strictly:
+ canonicalHeaders = "content-type:application/json; charset=utf-8\n"
+ + "host:" + host + "\n";
+ String signedHeaders = "content-type;host";
+
+ String canonicalRequest =
+ "POST\n"
+ + "/\n"
+ + "\n"
+ + canonicalHeaders
+ + "\n"
+ + signedHeaders + "\n"
+ + hashedPayload;
+
+ // ----- Step 2: string to sign -----
+ String credentialScope = date + "/" + service + "/tc3_request";
+ String stringToSign =
+ "TC3-HMAC-SHA256\n"
+ + timestamp + "\n"
+ + credentialScope + "\n"
+ + sha256Hex(canonicalRequest);
+
+ // ----- Step 3: signature -----
+ byte[] secretDate = hmacSha256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
+ byte[] secretService = hmacSha256(secretDate, service);
+ byte[] secretSigning = hmacSha256(secretService, "tc3_request");
+ String signature = toHex(hmacSha256(secretSigning, stringToSign));
+
+ // ----- Step 4: Authorization header -----
+ String authorization = "TC3-HMAC-SHA256 "
+ + "Credential=" + secretId + "/" + credentialScope + ", "
+ + "SignedHeaders=" + signedHeaders + ", "
+ + "Signature=" + signature;
+
+ return new SignedHeaders(authorization, String.valueOf(timestamp), host);
+ }
+
+ // ===== primitives =====
+
+ private static String utcDate(long epochSeconds) {
+ SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
+ fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return fmt.format(new Date(epochSeconds * 1000L));
+ }
+
+ private static String sha256Hex(String s) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ return toHex(md.digest(s.getBytes(StandardCharsets.UTF_8)));
+ } catch (Exception e) {
+ throw new IllegalStateException("SHA-256 unavailable", e);
+ }
+ }
+
+ private static byte[] hmacSha256(byte[] key, String data) {
+ try {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(key, "HmacSHA256"));
+ return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ throw new IllegalStateException("HmacSHA256 unavailable", e);
+ }
+ }
+
+ private static String toHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+ for (byte b : bytes) sb.append(String.format("%02x", b));
+ return sb.toString();
+ }
+
+ /**
+ * Build the canonical {@code X-TC-*} header set a Tencent Cloud v3 request
+ * needs in addition to {@code Authorization}. Caller must add {@code Host}
+ * and {@code Content-Type: application/json; charset=utf-8} themselves
+ * (those participate in the signature and are part of the wire request).
+ */
+ public static Map commonHeaders(String action, String version,
+ String region, String timestamp) {
+ Map h = new LinkedHashMap<>();
+ h.put("X-TC-Action", action);
+ h.put("X-TC-Version", version);
+ h.put("X-TC-Timestamp", timestamp);
+ if (region != null && !region.isBlank()) {
+ h.put("X-TC-Region", region);
+ }
+ return h;
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/model/MessageContentPart.java b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/model/MessageContentPart.java
index 237e1126..181da4b4 100644
--- a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/model/MessageContentPart.java
+++ b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/model/MessageContentPart.java
@@ -94,6 +94,20 @@ public class MessageContentPart {
return part;
}
+ /**
+ * 3D model asset (.glb / .obj / .fbx). The frontend renders this with a
+ * <model-viewer> Web Component when contentType starts with
+ * {@code model/} (e.g. {@code model/gltf-binary} for glb).
+ */
+ public static MessageContentPart model3d(String mediaId, String fileName) {
+ MessageContentPart part = new MessageContentPart();
+ part.setType("model3d");
+ part.setMediaId(mediaId);
+ part.setFileName(fileName);
+ part.setContentType("model/gltf-binary");
+ return part;
+ }
+
public static MessageContentPart toolCall(String jsonPayload) {
MessageContentPart part = new MessageContentPart();
part.setType("tool_call");
diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V71__hunyuan_3d_provider.sql b/mateclaw-server/src/main/resources/db/migration/h2/V71__hunyuan_3d_provider.sql
new file mode 100644
index 00000000..f6edb4b7
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/h2/V71__hunyuan_3d_provider.sql
@@ -0,0 +1,27 @@
+-- V71: Register Tencent Hunyuan 3D provider for ai3d service.
+-- The api_key column carries "SecretId:SecretKey" (colon-joined) — same
+-- two-part credential pattern as Kling. base_url defaults to the auto-routed
+-- ai3d.tencentcloudapi.com host but can be pointed at a regional endpoint
+-- (e.g. ai3d.ap-guangzhou.tencentcloudapi.com) when the operator wants to pin.
+--
+-- chat_model is intentionally non-empty (a placeholder marker for the
+-- generic ProviderInitProbe) — Hunyuan 3D is not actually a chat-completions
+-- model, but the provider table currently requires the column. The probe
+-- skips providers tagged is_local=TRUE / freeze_url=TRUE for chat liveness,
+-- so the freeze_url=TRUE flag below also keeps the LLM failover chain from
+-- ever attempting to dispatch chat traffic here.
+
+MERGE INTO mate_model_provider (
+ provider_id, name, api_key_prefix, chat_model, api_key, base_url,
+ generate_kwargs, is_custom, is_local, support_model_discovery,
+ support_connection_check, freeze_url, require_api_key, auth_type,
+ create_time, update_time
+)
+KEY (provider_id)
+VALUES (
+ 'hunyuan-3d', '腾讯混元 3D', 'AKID', 'NotApplicable', '',
+ 'https://ai3d.tencentcloudapi.com',
+ '{"service":"ai3d","version":"2025-05-13","region":"ap-guangzhou"}',
+ FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, 'tc3_hmac_sha256',
+ NOW(), NOW()
+);
diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V72__hunyuan_3d_model_config.sql b/mateclaw-server/src/main/resources/db/migration/h2/V72__hunyuan_3d_model_config.sql
new file mode 100644
index 00000000..1aade616
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/h2/V72__hunyuan_3d_model_config.sql
@@ -0,0 +1,54 @@
+-- V72: Register the three Tencent Hunyuan 3D model variants in
+-- mate_model_config so the "Models & Credentials" provider card surfaces
+-- them in the picker. Hunyuan 3D is not a chat-completions model;
+-- model_type='model3d' (consistent with model_type='image' / 'video' used
+-- for generative non-chat providers — see V32 bailian-team).
+--
+-- Model -> backend Action mapping (see HunyuanModel3dProvider):
+-- HY-3D-Express -> SubmitHunyuanTo3DRapidJob (Prompt/ImageUrl only, fastest)
+-- HY-3D-3.0 -> SubmitHunyuanTo3DProJob (full feature set)
+-- HY-3D-3.1 -> SubmitHunyuanTo3DProJob (latest Pro behavior on
+-- X-TC-Version=2025-05-13;
+-- differs from 3.0 internally)
+
+MERGE INTO mate_model_config (
+ id, name, provider, model_name, description,
+ temperature, max_tokens, top_p, builtin, enabled, is_default,
+ model_type, create_time, update_time, deleted
+)
+KEY (id)
+VALUES (
+ 1000000500, 'HY-3D-3.1', 'hunyuan-3d', 'HY-3D-3.1',
+ '腾讯混元 3D 3.1 — 最高精度,支持 PBR / 多视角 / Geometry 白模等专业参数',
+ NULL, NULL, NULL,
+ TRUE, TRUE, TRUE,
+ 'model3d', NOW(), NOW(), 0
+);
+
+MERGE INTO mate_model_config (
+ id, name, provider, model_name, description,
+ temperature, max_tokens, top_p, builtin, enabled, is_default,
+ model_type, create_time, update_time, deleted
+)
+KEY (id)
+VALUES (
+ 1000000501, 'HY-3D-3.0', 'hunyuan-3d', 'HY-3D-3.0',
+ '腾讯混元 3D 3.0 — 老一代 Pro 模型,与 3.1 共享 SubmitHunyuanTo3DProJob 调用',
+ NULL, NULL, NULL,
+ TRUE, TRUE, FALSE,
+ 'model3d', NOW(), NOW(), 0
+);
+
+MERGE INTO mate_model_config (
+ id, name, provider, model_name, description,
+ temperature, max_tokens, top_p, builtin, enabled, is_default,
+ model_type, create_time, update_time, deleted
+)
+KEY (id)
+VALUES (
+ 1000000502, 'HY-3D-Express', 'hunyuan-3d', 'HY-3D-Express',
+ '腾讯混元 3D 极速版 — 走 SubmitHunyuanTo3DRapidJob 接口,速度最快但仅支持 Prompt / ImageUrl',
+ NULL, NULL, NULL,
+ TRUE, TRUE, FALSE,
+ 'model3d', NOW(), NOW(), 0
+);
diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V71__hunyuan_3d_provider.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V71__hunyuan_3d_provider.sql
new file mode 100644
index 00000000..90b5d959
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/mysql/V71__hunyuan_3d_provider.sql
@@ -0,0 +1,29 @@
+-- V71: Register Tencent Hunyuan 3D provider for ai3d service.
+-- See h2/V71 for full rationale.
+
+INSERT INTO mate_model_provider (
+ provider_id, name, api_key_prefix, chat_model, api_key, base_url,
+ generate_kwargs, is_custom, is_local, support_model_discovery,
+ support_connection_check, freeze_url, require_api_key, auth_type,
+ create_time, update_time
+) VALUES (
+ 'hunyuan-3d', '腾讯混元 3D', 'AKID', 'NotApplicable', '',
+ 'https://ai3d.tencentcloudapi.com',
+ '{"service":"ai3d","version":"2025-05-13","region":"ap-guangzhou"}',
+ 0, 0, 0, 0, 1, 1, 'tc3_hmac_sha256',
+ NOW(), NOW()
+)
+ON DUPLICATE KEY UPDATE
+ name = VALUES(name),
+ api_key_prefix = VALUES(api_key_prefix),
+ chat_model = VALUES(chat_model),
+ base_url = VALUES(base_url),
+ generate_kwargs = VALUES(generate_kwargs),
+ is_custom = VALUES(is_custom),
+ is_local = VALUES(is_local),
+ support_model_discovery = VALUES(support_model_discovery),
+ support_connection_check = VALUES(support_connection_check),
+ freeze_url = VALUES(freeze_url),
+ require_api_key = VALUES(require_api_key),
+ auth_type = VALUES(auth_type),
+ update_time = NOW();
diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V72__hunyuan_3d_model_config.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V72__hunyuan_3d_model_config.sql
new file mode 100644
index 00000000..1f0af74d
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/mysql/V72__hunyuan_3d_model_config.sql
@@ -0,0 +1,25 @@
+-- V72: Register Tencent Hunyuan 3D model variants. See h2/V72 for full rationale.
+
+INSERT INTO mate_model_config (
+ id, name, provider, model_name, description,
+ temperature, max_tokens, top_p, builtin, enabled, is_default,
+ model_type, create_time, update_time, deleted
+) VALUES
+ (1000000500, 'HY-3D-3.1', 'hunyuan-3d', 'HY-3D-3.1',
+ '腾讯混元 3D 3.1 — 最高精度,支持 PBR / 多视角 / Geometry 白模等专业参数',
+ NULL, NULL, NULL, 1, 1, 1, 'model3d', NOW(), NOW(), 0),
+ (1000000501, 'HY-3D-3.0', 'hunyuan-3d', 'HY-3D-3.0',
+ '腾讯混元 3D 3.0 — 老一代 Pro 模型,与 3.1 共享 SubmitHunyuanTo3DProJob 调用',
+ NULL, NULL, NULL, 1, 1, 0, 'model3d', NOW(), NOW(), 0),
+ (1000000502, 'HY-3D-Express', 'hunyuan-3d', 'HY-3D-Express',
+ '腾讯混元 3D 极速版 — 走 SubmitHunyuanTo3DRapidJob 接口,速度最快但仅支持 Prompt / ImageUrl',
+ NULL, NULL, NULL, 1, 1, 0, 'model3d', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE
+ name = VALUES(name),
+ model_name = VALUES(model_name),
+ description = VALUES(description),
+ builtin = VALUES(builtin),
+ enabled = VALUES(enabled),
+ is_default = VALUES(is_default),
+ model_type = VALUES(model_type),
+ update_time = NOW();
diff --git a/mateclaw-server/src/main/resources/messages.properties b/mateclaw-server/src/main/resources/messages.properties
index c091c9b5..a9c51900 100644
--- a/mateclaw-server/src/main/resources/messages.properties
+++ b/mateclaw-server/src/main/resources/messages.properties
@@ -77,6 +77,7 @@ tool.shell.error.exception=\u6267\u884c\u5f02\u5e38: {0}
tool.image_generate.desc=\u751f\u6210\u56fe\u7247\u3002\u6839\u636e\u6587\u5b57\u63cf\u8ff0\u521b\u4f5c\u56fe\u50cf\uff0c\u652f\u6301 DashScope\u3001OpenAI\u3001fal.ai\u3001\u667a\u8c31 CogView \u7b49 Provider\u3002\u4efb\u52a1\u5f02\u6b65\u6267\u884c\uff0c\u5b8c\u6210\u540e\u524d\u7aef\u4f1a\u81ea\u52a8\u63a5\u6536 SSE \u4e8b\u4ef6 async_task_completed \u5e76\u628a\u56fe\u7247\u63a8\u5230\u5bf9\u8bdd\u4e2d\uff0c\u65e0\u9700\u624b\u52a8\u5237\u65b0\u3002
tool.video_generate.desc=\u751f\u6210\u89c6\u9891\u3002\u652f\u6301\u6587\u751f\u89c6\u9891\u3001\u56fe\u751f\u89c6\u9891\u7b49\u6a21\u5f0f\uff0c\u652f\u6301 DashScope\u3001\u667a\u8c31 CogVideo\u3001Kling\u3001Runway\u3001MiniMax \u3001fal.ai \u7b49 Provider\u3002\u4efb\u52a1\u5f02\u6b65\u6267\u884c\uff08\u7ea6 1-5 \u5206\u949f\uff09\uff0c\u5b8c\u6210\u540e\u524d\u7aef\u4f1a\u81ea\u52a8\u63a5\u6536 SSE \u4e8b\u4ef6 async_task_completed \u5e76\u628a\u89c6\u9891\u63a8\u5230\u5bf9\u8bdd\u4e2d\uff0c\u65e0\u9700\u624b\u52a8\u5237\u65b0\u3002
tool.music_generate.desc=\u751f\u6210\u97f3\u4e50\u6216\u6b4c\u66f2\u3002\u652f\u6301\u6587\u5b57\u63cf\u8ff0\u751f\u6210\u97f3\u4e50\u3001\u6b4c\u8bcd\u8c31\u66f2\u3001\u7eaf\u97f3\u4e50\u7b49\u6a21\u5f0f\uff0c\u652f\u6301 Google Lyria \u548c MiniMax Music \u7b49 Provider\u3002\u4efb\u52a1\u5f02\u6b65\u6267\u884c\uff08\u7ea6 1-3 \u5206\u949f\uff09\uff0c\u5b8c\u6210\u540e\u524d\u7aef\u4f1a\u81ea\u52a8\u63a5\u6536 SSE \u4e8b\u4ef6 async_task_completed \u5e76\u628a\u97f3\u9891\u63a8\u5230\u5bf9\u8bdd\u4e2d\uff0c\u65e0\u9700\u624b\u52a8\u5237\u65b0\u3002
+tool.model3d_generate.desc=\u751f\u6210 3D \u6a21\u578b (.glb)\u3002\u652f\u6301\u6587\u751f 3D \u4e0e\u56fe\u751f 3D \u4e24\u79cd\u6a21\u5f0f\uff0c\u76ee\u524d Provider \u4e3a\u817e\u8baf\u6df7\u5143 3D Rapid\u3002\u4efb\u52a1\u5f02\u6b65\u6267\u884c\uff08\u7ea6 1-3 \u5206\u949f\uff09\uff0c\u5b8c\u6210\u540e\u524d\u7aef\u4f1a\u81ea\u52a8\u63a5\u6536 SSE \u4e8b\u4ef6 async_task_completed \u5e76\u628a\u6a21\u578b\u63a8\u5230\u5bf9\u8bdd\u4e2d\uff08\u5e26 model-viewer \u9884\u89c8\uff09\uff0c\u65e0\u9700\u624b\u52a8\u5237\u65b0\u3002
# --- Guard Rules ---
guard.SHELL_RM_RF_ROOT.name=\u9012\u5f52\u5f3a\u5236\u5220\u9664\u6839\u76ee\u5f55
diff --git a/mateclaw-server/src/main/resources/messages_en.properties b/mateclaw-server/src/main/resources/messages_en.properties
index 0cdd51d8..69fcb9d9 100644
--- a/mateclaw-server/src/main/resources/messages_en.properties
+++ b/mateclaw-server/src/main/resources/messages_en.properties
@@ -77,6 +77,7 @@ tool.shell.error.exception=Execution exception: {0}
tool.image_generate.desc=Generate an image from a text description. Supports DashScope, OpenAI, fal.ai, Zhipu CogView, etc. Runs asynchronously; the client receives the image automatically via the SSE async_task_completed event without a manual refresh.
tool.video_generate.desc=Generate a video. Supports text-to-video and image-to-video modes via DashScope, Zhipu CogVideo, Kling, Runway, MiniMax, fal.ai, etc. Runs asynchronously (1-5 minutes); the client receives the video automatically via the SSE async_task_completed event without a manual refresh.
tool.music_generate.desc=Generate music or a song. Supports text-to-music, lyrics composition, and instrumental modes via Google Lyria and MiniMax Music. Runs asynchronously (1-3 minutes); the client receives the audio automatically via the SSE async_task_completed event without a manual refresh.
+tool.model3d_generate.desc=Generate a 3D model (.glb). Supports text-to-3D and image-to-3D modes via Tencent Hunyuan 3D Rapid. Runs asynchronously (1-3 minutes); the client receives the model automatically via the SSE async_task_completed event with an inline model-viewer preview, no manual refresh required.
# --- Guard Rules ---
guard.SHELL_RM_RF_ROOT.name=Recursive force delete root directory
diff --git a/mateclaw-ui/package.json b/mateclaw-ui/package.json
index 335560fc..17532416 100644
--- a/mateclaw-ui/package.json
+++ b/mateclaw-ui/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
+ "@google/model-viewer": "^4.2.0",
"axios": "^1.7.9",
"dayjs": "^1.11.13",
"dompurify": "^3.3.3",
diff --git a/mateclaw-ui/pnpm-lock.yaml b/mateclaw-ui/pnpm-lock.yaml
index 45df5c01..1c2dda62 100644
--- a/mateclaw-ui/pnpm-lock.yaml
+++ b/mateclaw-ui/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@element-plus/icons-vue':
specifier: ^2.3.1
version: 2.3.2(vue@3.5.31(typescript@5.7.3))
+ '@google/model-viewer':
+ specifier: ^4.2.0
+ version: 4.2.0(three@0.182.0)
axios:
specifier: ^1.7.9
version: 1.14.0
@@ -343,6 +346,12 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+ '@google/model-viewer@4.2.0':
+ resolution: {integrity: sha512-RjpAI5cLs9CdvPcMRsOs8Bea/lNmGTTyaPyl16o9Fv6Qn8VSpgBMmXFr/11yb0hTrsojp2dOACEcY77R8hVUVA==}
+ engines: {node: '>=6.0.0'}
+ peerDependencies:
+ three: ^0.182.0
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -393,9 +402,20 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@lit-labs/ssr-dom-shim@1.5.1':
+ resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==}
+
+ '@lit/reactive-element@2.1.2':
+ resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==}
+
'@mermaid-js/parser@1.1.0':
resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==}
+ '@monogrid/gainmap-js@3.4.0':
+ resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==}
+ peerDependencies:
+ three: '>= 0.159.0'
+
'@rolldown/pluginutils@1.0.0-rc.2':
resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==}
@@ -1380,6 +1400,9 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
+ immediate@3.0.6:
+ resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -1403,6 +1426,9 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
+ is-promise@2.2.2:
+ resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
+
is-what@5.5.0:
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
engines: {node: '>=18'}
@@ -1451,6 +1477,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ lie@3.3.0:
+ resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
@@ -1525,6 +1554,15 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
+ lit-element@4.2.2:
+ resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==}
+
+ lit-html@3.3.2:
+ resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==}
+
+ lit@3.3.2:
+ resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -1698,6 +1736,9 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
+ promise-worker-transferable@1.0.4:
+ resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==}
+
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
@@ -1777,6 +1818,9 @@ packages:
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
engines: {node: '>=6'}
+ three@0.182.0:
+ resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==}
+
tinyexec@1.1.1:
resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
engines: {node: '>=18'}
@@ -2116,6 +2160,12 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
+ '@google/model-viewer@4.2.0(three@0.182.0)':
+ dependencies:
+ '@monogrid/gainmap-js': 3.4.0(three@0.182.0)
+ lit: 3.3.2
+ three: 0.182.0
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
@@ -2166,10 +2216,21 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@lit-labs/ssr-dom-shim@1.5.1': {}
+
+ '@lit/reactive-element@2.1.2':
+ dependencies:
+ '@lit-labs/ssr-dom-shim': 1.5.1
+
'@mermaid-js/parser@1.1.0':
dependencies:
langium: 4.2.2
+ '@monogrid/gainmap-js@3.4.0(three@0.182.0)':
+ dependencies:
+ promise-worker-transferable: 1.0.4
+ three: 0.182.0
+
'@rolldown/pluginutils@1.0.0-rc.2': {}
'@rollup/rollup-android-arm-eabi@4.60.1':
@@ -2448,8 +2509,7 @@ snapshots:
'@types/lodash@4.17.24': {}
- '@types/trusted-types@2.0.7':
- optional: true
+ '@types/trusted-types@2.0.7': {}
'@types/web-bluetooth@0.0.20': {}
@@ -3199,6 +3259,8 @@ snapshots:
ignore@5.3.2: {}
+ immediate@3.0.6: {}
+
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -3216,6 +3278,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
+ is-promise@2.2.2: {}
+
is-what@5.5.0: {}
isexe@2.0.0: {}
@@ -3260,6 +3324,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ lie@3.3.0:
+ dependencies:
+ immediate: 3.0.6
+
lightningcss-android-arm64@1.32.0:
optional: true
@@ -3309,6 +3377,22 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
+ lit-element@4.2.2:
+ dependencies:
+ '@lit-labs/ssr-dom-shim': 1.5.1
+ '@lit/reactive-element': 2.1.2
+ lit-html: 3.3.2
+
+ lit-html@3.3.2:
+ dependencies:
+ '@types/trusted-types': 2.0.7
+
+ lit@3.3.2:
+ dependencies:
+ '@lit/reactive-element': 2.1.2
+ lit-element: 4.2.2
+ lit-html: 3.3.2
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -3478,6 +3562,11 @@ snapshots:
prelude-ls@1.2.1: {}
+ promise-worker-transferable@1.0.4:
+ dependencies:
+ is-promise: 2.2.2
+ lie: 3.3.0
+
proxy-from-env@2.1.0: {}
punycode@2.3.1: {}
@@ -3560,6 +3649,8 @@ snapshots:
tapable@2.3.2: {}
+ three@0.182.0: {}
+
tinyexec@1.1.1: {}
tinyglobby@0.2.15:
diff --git a/mateclaw-ui/src/components/chat/MessageBubble.vue b/mateclaw-ui/src/components/chat/MessageBubble.vue
index 7f052da9..63b391fb 100644
--- a/mateclaw-ui/src/components/chat/MessageBubble.vue
+++ b/mateclaw-ui/src/components/chat/MessageBubble.vue
@@ -208,6 +208,25 @@
/>
{{ attachment.name }}
+
+