mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 07:13:17 +08:00
feat(video): DashScope video specialist with correct i2v request bodies
- Add video_generation_client: async video-synthesis, t2v vs i2v (Wan 2.7 input.media first_frame vs legacy img_url). - Coerce *-t2v* to *-i2v* when first frame present; AIA_VIDEO_I2V_INPUT_STYLE / I2V_MODEL overrides. - Gateway/direct_loop early return for video specialist; workspace video + factory allowlist. - Normalize WS and admin chat attachments (image_ref parity); mp4 attachment store roundtrip tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
398d85ba06
commit
4335040a27
21 changed files with 1152 additions and 56 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import queue
|
||||
import threading
|
||||
|
|
@ -28,8 +27,8 @@ from oclaw.platform.files.file_attachments import (
|
|||
DEFAULT_MAX_EXCEL_SHEETS,
|
||||
DEFAULT_TABULAR_ROWS_READ,
|
||||
clear_attachment_limits_cache,
|
||||
process_file_data,
|
||||
)
|
||||
from oclaw.interfaces.ws.common import normalize_ws_attachments
|
||||
from oclaw.platform.files.session_export import export_session_json, export_session_markdown
|
||||
from oclaw.platform.persistence.sqlite_store import SqliteStore
|
||||
from oclaw.runtime.gateway import OclawGateway
|
||||
|
|
@ -270,45 +269,9 @@ def _clear_stop(session_id: str) -> None:
|
|||
_CHAT_STOP_EVENTS.pop(str(session_id), None)
|
||||
|
||||
|
||||
def _decode_base64_payload(s: str) -> bytes | None:
|
||||
"""Decode base64 from browser (may lack padding; URL-safe variants)."""
|
||||
raw = (s or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
if raw.startswith("data:") and "," in raw:
|
||||
raw = raw.split(",", 1)[1].strip()
|
||||
raw = raw.replace("-", "+").replace("_", "/")
|
||||
pad = (-len(raw)) % 4
|
||||
if pad:
|
||||
raw += "=" * pad
|
||||
try:
|
||||
return base64.b64decode(raw, validate=False)
|
||||
except Exception:
|
||||
try:
|
||||
return base64.standard_b64decode(raw)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_attachments_payload(raw: Any) -> list[dict[str, Any]] | None:
|
||||
if not raw:
|
||||
return None
|
||||
if not isinstance(raw, list):
|
||||
return None
|
||||
out: list[dict[str, Any]] = []
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = str(item.get("name") or "file").strip() or "file"
|
||||
b64 = item.get("data_base64") if "data_base64" in item else item.get("data")
|
||||
if not isinstance(b64, str) or not b64.strip():
|
||||
continue
|
||||
data = _decode_base64_payload(b64)
|
||||
if not data:
|
||||
continue
|
||||
got = process_file_data(name, data)
|
||||
if got:
|
||||
out.extend(got)
|
||||
"""Match WebSocket :func:`normalize_ws_attachments`: base64 uploads plus pass-through typed refs."""
|
||||
out = normalize_ws_attachments(raw)
|
||||
return out if out else None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ const I18N = {
|
|||
"chat.specialistGeneralistShort": "通用",
|
||||
"chat.specialistOpsShort": "运维",
|
||||
"chat.specialistImageShort": "图像",
|
||||
"chat.specialistVideoShort": "视频",
|
||||
"chat.specialistMemoryShort": "记忆",
|
||||
"chat.specialistManagerSelfShort": "全能者",
|
||||
"chat.attachment.download": "下载",
|
||||
|
|
@ -344,6 +345,7 @@ const I18N = {
|
|||
"chat.specialistGeneralistShort": "Generalist",
|
||||
"chat.specialistOpsShort": "Ops",
|
||||
"chat.specialistImageShort": "Image",
|
||||
"chat.specialistVideoShort": "Video",
|
||||
"chat.specialistMemoryShort": "Memory",
|
||||
"chat.specialistManagerSelfShort": "Manager",
|
||||
"chat.attachment.download": "Download",
|
||||
|
|
@ -649,6 +651,7 @@ function specialistLabel(specialist) {
|
|||
if (s === "manager_self") return t("chat.specialistManagerSelfShort");
|
||||
if (s === "ops") return t("chat.specialistOpsShort");
|
||||
if (s === "image") return t("chat.specialistImageShort");
|
||||
if (s === "video") return t("chat.specialistVideoShort");
|
||||
if (s === "memory") return t("chat.specialistMemoryShort");
|
||||
return t("chat.specialistGeneralistShort");
|
||||
}
|
||||
|
|
@ -2190,6 +2193,39 @@ async function renderAttachmentsEl(raw) {
|
|||
text: t("chat.attachment.download"),
|
||||
}),
|
||||
);
|
||||
if (String(typ || "") === "video_ref" && mime.toLowerCase().startsWith("video/")) {
|
||||
card.appendChild(
|
||||
el("video", {
|
||||
class: "chat-att-video",
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
style: "max-width:100%;max-height:420px;margin-top:8px;border-radius:8px;",
|
||||
src: url,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const remote = String(att.url || "").trim();
|
||||
if (remote && String(typ || "") === "video_ref" && mime.toLowerCase().startsWith("video/")) {
|
||||
card.appendChild(
|
||||
el("video", {
|
||||
class: "chat-att-video",
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
style: "max-width:100%;max-height:420px;margin-top:8px;border-radius:8px;",
|
||||
src: remote,
|
||||
}),
|
||||
);
|
||||
card.appendChild(
|
||||
el("a", {
|
||||
class: "chat-att-ref__link",
|
||||
href: remote,
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
text: t("chat.attachment.download"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
const canPreviewText = !!aid && (String(typ || "") === "text_ref" || isTextLikeMime(mime));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue