fix: 对齐 postid 参考图工作台行为

This commit is contained in:
XianYuDaXian committed 2026-03-15 14:50:05 +08:00
1 parent 708cd79f48
commit 8762c3816b
9 files changed
+260 -85

No files matched your search

+51 -24
View File
@@ -1150,31 +1150,58 @@ async def public_imagine_workbench_edit(data: ImagineWorkbenchEditRequest, reque
]
if effective_reference_items:
for item in effective_reference_items:
ref_parent = _extract_parent_post_id_from_url(str(item.get("parent_post_id") or ""))
if ref_parent and not str(item.get("source_image_url") or "").strip():
item["parent_post_id"] = ref_parent
item["source_image_url"] = await _canonicalize_parent_source_image_url(
token,
ref_parent,
str(item.get("image_url") or "").strip(),
)
if not str(item.get("image_url") or "").strip():
item["image_url"] = item["source_image_url"]
result = await edit_service.edit_with_reference_items(
token_mgr=token_mgr,
token=token,
model_info=model_info,
prompt=prompt,
reference_items=effective_reference_items,
root_parent_post_id=parent_post_id if use_parent_mode else "",
response_format="url",
stream=False,
return_all_images=True,
progress_cb=progress_cb,
first_ref = effective_reference_items[0] if effective_reference_items else {}
only_single_parent_ref = (
len(effective_reference_items) == 1
and bool(str(first_ref.get("parent_post_id") or "").strip())
)
mode = "parent_post" if use_parent_mode else "upload"
if only_single_parent_ref:
single_parent_post_id = str(first_ref.get("parent_post_id") or "").strip()
single_source_image_url = str(
first_ref.get("source_image_url")
or first_ref.get("image_url")
or current_source_image_url_input
or ""
).strip()
result = await edit_service.edit_with_parent_post(
token_mgr=token_mgr,
token=token,
model_info=model_info,
prompt=prompt,
parent_post_id=single_parent_post_id,
source_image_url=single_source_image_url,
response_format="url",
stream=False,
return_all_images=True,
progress_cb=progress_cb,
)
mode = "parent_post"
else:
for item in effective_reference_items:
ref_parent = _extract_parent_post_id_from_url(str(item.get("parent_post_id") or ""))
if ref_parent and not str(item.get("source_image_url") or "").strip():
item["parent_post_id"] = ref_parent
item["source_image_url"] = await _canonicalize_parent_source_image_url(
token,
ref_parent,
str(item.get("image_url") or "").strip(),
)
if not str(item.get("image_url") or "").strip():
item["image_url"] = item["source_image_url"]
result = await edit_service.edit_with_reference_items(
token_mgr=token_mgr,
token=token,
model_info=model_info,
prompt=prompt,
reference_items=effective_reference_items,
root_parent_post_id=parent_post_id if use_parent_mode else "",
response_format="url",
stream=False,
return_all_images=True,
progress_cb=progress_cb,
)
mode = "parent_post" if use_parent_mode else "upload"
elif use_parent_mode:
if current_source_image_url_input and not (
current_source_image_url_input.startswith("http://")
+7 -4
View File
@@ -362,14 +362,17 @@ async def public_video_start(data: VideoStartRequest):
raise HTTPException(status_code=400, detail="最多支持 7 张参考图")
parent_post_refs = [item for item in reference_items if item.get("parent_post_id")]
parent_post_id = str(parent_post_refs[0].get("parent_post_id") or "").strip() if parent_post_refs else ""
# 带 parent_post_id 的参考项按“基于已有 post 引用”处理,不再同时抽取 image_url,
# 避免单个 reference_item 既有 parent_post_id 又有 image_url 时被误判为冲突参数。
pure_image_refs = [item for item in reference_items if not item.get("parent_post_id")]
image_url = (
str(reference_items[0].get("image_url") or "").strip() or None
if reference_items
str(pure_image_refs[0].get("image_url") or "").strip() or None
if pure_image_refs
else None
)
source_image_url = (
str(reference_items[0].get("source_image_url") or "").strip() or None
if reference_items
str(pure_image_refs[0].get("source_image_url") or "").strip() or None
if pure_image_refs
else None
)
+38 -31
View File
@@ -772,6 +772,7 @@ class ImageEditService:
) -> List[dict[str, str]]:
prepared: List[dict[str, str]] = []
upload_service = UploadService()
primary_parent_consumed = False
try:
for item in reference_items:
raw_source = str(
@@ -794,13 +795,23 @@ class ImageEditService:
attachment_id = ""
resolved_url = ""
resolved_id = ""
current_parent_post_id = str(item.get("parent_post_id") or "").strip()
should_upload_for_attachment = _needs_image_edit_reference_upload(
item, raw_source
)
if current_parent_post_id:
if not primary_parent_consumed:
primary_parent_consumed = True
should_upload_for_attachment = False
else:
should_upload_for_attachment = True
if not should_upload_for_attachment:
normalized_raw = _normalize_asset_url(raw_source)
if normalized_raw and not _is_assets_content_url(normalized_raw):
should_upload_for_attachment = True
if current_parent_post_id and primary_parent_consumed:
should_upload_for_attachment = False
else:
should_upload_for_attachment = True
if should_upload_for_attachment:
file_id, file_uri = await upload_service.upload_file(raw_source, token)
resolved_url = _normalize_asset_url(file_uri)
@@ -808,10 +819,7 @@ class ImageEditService:
resolved_url
)
attachment_id = str(file_id or "").strip() or resolved_id
if raw_source.startswith("http://") or raw_source.startswith("https://"):
request_url = raw_source
else:
request_url = resolved_url
request_url = resolved_url
mention_id = resolved_id or mention_id
else:
if raw_source.startswith("/users/") or raw_source.startswith("users/"):
@@ -821,7 +829,7 @@ class ImageEditService:
resolved_url = _normalize_asset_url(raw_source)
resolved_id = _extract_image_post_id(resolved_url)
mention_id = mention_id or resolved_id
attachment_id = resolved_id
attachment_id = ""
prepared.append(
{
"source_url": raw_source,
@@ -831,7 +839,7 @@ class ImageEditService:
"resolved_id": resolved_id,
"mention_id": mention_id,
"attachment_id": attachment_id,
"parent_post_id": str(item.get("parent_post_id") or "").strip(),
"parent_post_id": current_parent_post_id if not should_upload_for_attachment else "",
"mention_alias": mention_alias,
}
)
@@ -917,19 +925,9 @@ class ImageEditService:
if str(item.get("request_url") or item.get("resolved_url") or "").strip()
]
file_attachments = [
str(
item.get("attachment_id")
or item.get("resolved_id")
or item.get("mention_id")
or ""
).strip()
str(item.get("attachment_id") or "").strip()
for item in prepared_refs
if str(
item.get("attachment_id")
or item.get("resolved_id")
or item.get("mention_id")
or ""
).strip()
if str(item.get("attachment_id") or "").strip()
]
if not request_urls:
raise AppException(
@@ -939,17 +937,23 @@ class ImageEditService:
status_code=400,
)
effective_parent_post_id = str(root_parent_post_id or "").strip()
if not effective_parent_post_id:
first_parent = str(
prepared_refs[0].get("parent_post_id") or ""
).strip()
if first_parent:
effective_parent_post_id = first_parent
if not effective_parent_post_id:
effective_parent_post_id = await VideoService().create_image_post(
current_token, request_urls[0]
)
parent_ids = [
str(item.get("parent_post_id") or "").strip()
for item in prepared_refs
if str(item.get("parent_post_id") or "").strip()
]
unique_parent_ids = list(dict.fromkeys(parent_ids))
effective_parent_post_id = ""
# 多张不同 parentPostId 的参考图不能再强行指定单个 parentPostId,
# 否则上游会把它视为互相冲突的编辑上下文并直接返回 400。
if len(unique_parent_ids) == 1:
effective_parent_post_id = unique_parent_ids[0]
elif not unique_parent_ids:
effective_parent_post_id = str(root_parent_post_id or "").strip()
if not effective_parent_post_id:
effective_parent_post_id = await VideoService().create_image_post(
current_token, request_urls[0]
)
await self._emit_progress(
progress_cb, "chat_request_start", 42, "已提交编辑请求"
@@ -960,10 +964,13 @@ class ImageEditService:
"imageEditModel": "imagine",
"imageEditModelConfig": {
"imageReferences": request_urls,
"parentPostId": effective_parent_post_id,
},
}
}
if effective_parent_post_id:
model_config_override["modelMap"]["imageEditModelConfig"][
"parentPostId"
] = effective_parent_post_id
_log_final_image_edit_payload(
prompt_text=prompt_text,
file_attachments=file_attachments,
+50 -3
View File
@@ -496,6 +496,51 @@ class VideoService:
await upload_service.close()
return uploaded
async def _prepare_reference_video_inputs(
self,
token: str,
reference_items: list[dict[str, Any]],
) -> tuple[str, list[dict[str, str]]]:
primary_parent_post_id = ""
prepared_items: list[dict[str, str]] = []
upload_candidates: list[dict[str, Any]] = []
for index, item in enumerate(reference_items):
normalized_item = dict(item or {})
source_url = await self._resolve_reference_source_url(token, normalized_item)
if not source_url:
raise ValidationException(f"第 {index + 1} 张参考图缺少可用来源")
current_parent_post_id = str(
normalized_item.get("parent_post_id") or ""
).strip()
mention_alias = str(normalized_item.get("mention_alias") or "").strip()
if current_parent_post_id and not primary_parent_post_id:
primary_parent_post_id = current_parent_post_id
prepared_items.append(
{
"file_id": current_parent_post_id,
"asset_url": source_url,
"source_url": source_url,
"parent_post_id": current_parent_post_id,
"mention_alias": mention_alias,
"direct_parent": "1",
}
)
continue
normalized_item["source_image_url"] = source_url
upload_candidates.append(normalized_item)
if upload_candidates:
uploaded_items = await self._upload_reference_items(token, upload_candidates)
for item in uploaded_items:
item["direct_parent"] = "0"
prepared_items.extend(uploaded_items)
return primary_parent_post_id, prepared_items
async def generate_from_reference_items(
self,
token: str,
@@ -507,7 +552,9 @@ class VideoService:
preset: str = "normal",
) -> AsyncGenerator[bytes, None]:
token_tag = _token_tag(token)
uploaded_refs = await self._upload_reference_items(token, reference_items)
primary_parent_post_id, uploaded_refs = await self._prepare_reference_video_inputs(
token, reference_items
)
if not uploaded_refs:
raise ValidationException("至少需要 1 张参考图")
@@ -539,13 +586,13 @@ class VideoService:
file_attachments = [
str(item.get("file_id") or "").strip()
for item in uploaded_refs
if str(item.get("file_id") or "").strip()
if str(item.get("file_id") or "").strip() and str(item.get("direct_parent") or "") != "1"
]
message = f"{prompt_text} --mode=custom".strip()
model_config_override = {
"modelMap": {
"videoGenModelConfig": {
"parentPostId": post_id,
"parentPostId": primary_parent_post_id or post_id,
"aspectRatio": aspect_ratio,
"videoLength": video_length,
"resolutionName": resolution,
+17 -4
View File
@@ -70,14 +70,28 @@
return raw;
}
function isImageLikeUrl(value) {
const raw = normalizeUrl(value);
if (!raw) return false;
if (raw.startsWith('data:image/')) return true;
if (/\/imagine-public\/images\/[0-9a-fA-F-]{32,36}(?:\.[a-z0-9]+|[/?#]|$)/i.test(raw)) return true;
if (/\/imagine-public\/share-images\/[0-9a-fA-F-]{32,36}(?:\.[a-z0-9]+|[/?#]|$)/i.test(raw)) return true;
if (/\/v1\/files\/image\//i.test(raw)) return true;
if (/\/users\/.+\.(?:jpg|jpeg|png|webp|gif)(?:[?#].*)?$/i.test(raw)) return true;
if (/\.(?:jpg|jpeg|png|webp|gif)(?:[?#].*)?$/i.test(raw)) return true;
return false;
}
function remember(entry) {
if (!entry || typeof entry !== 'object') return null;
const parentPostId = extractParentPostId(entry.parentPostId || entry.parent_post_id || entry.id || '');
if (!isParentPostId(parentPostId)) return null;
const sourceImageUrl = normalizeUrl(entry.sourceImageUrl || entry.source_image_url || '');
const imageUrl = normalizeUrl(entry.imageUrl || entry.image_url || entry.url || '');
const rawSourceImageUrl = normalizeUrl(entry.sourceImageUrl || entry.source_image_url || '');
const rawImageUrl = normalizeUrl(entry.imageUrl || entry.image_url || entry.url || '');
const sourceImageUrl = isImageLikeUrl(rawSourceImageUrl) ? rawSourceImageUrl : '';
const imageUrl = isImageLikeUrl(rawImageUrl) ? rawImageUrl : '';
const item = {
parentPostId,
sourceImageUrl: sourceImageUrl || imageUrl || buildImaginePublicUrl(parentPostId),
@@ -114,8 +128,7 @@
if (!parentPostId) return null;
const hit = getByParentPostId(parentPostId);
if (hit) return hit;
const looksLikeUrl = raw.startsWith('http://') || raw.startsWith('https://') || raw.startsWith('/');
const fallbackUrl = looksLikeUrl ? raw : buildImaginePublicUrl(parentPostId);
const fallbackUrl = isImageLikeUrl(raw) ? raw : buildImaginePublicUrl(parentPostId);
return {
parentPostId,
sourceImageUrl: fallbackUrl,
+26 -7
View File
@@ -159,6 +159,17 @@
return parentPostId ? buildImaginePublicUrl(parentPostId) : '';
}
function isImagePreviewUrl(value) {
const raw = String(value || '').trim();
if (!raw) return false;
if (raw.startsWith('data:image/')) return true;
if (/\/imagine-public\/images\/[0-9a-fA-F-]{32,36}(?:\.jpg|\/|$)/i.test(raw)) return true;
if (/\/v1\/files\/image\//i.test(raw)) return true;
if (/\/users\/.+\.(?:jpg|jpeg|png|webp)(?:[?#].*)?$/i.test(raw)) return true;
if (/\.(?:jpg|jpeg|png|webp)(?:[?#].*)?$/i.test(raw)) return true;
return false;
}
function pickPreviewUrl(hit, parentPostId) {
const candidates = [
hit && hit.imageUrl,
@@ -169,7 +180,7 @@
];
for (const candidate of candidates) {
const raw = String(candidate || '').trim();
if (raw) return raw;
if (isImagePreviewUrl(raw)) return raw;
}
const source = pickSourceImageUrl(hit, parentPostId);
return source || (parentPostId ? buildImaginePublicUrl(parentPostId) : '');
@@ -1438,15 +1449,23 @@
if (references.length) {
body.image_references = references;
body.reference_items = referenceItems;
const firstRef = references[0];
if (firstRef.startsWith('data:image/')) {
body.image_base64 = firstRef;
} else {
body.image_url = firstRef;
const firstRefItem = referenceItems[0] || null;
const firstRef = firstRefItem
? String(firstRefItem.source_image_url || firstRefItem.image_url || '').trim()
: '';
if (firstRefItem && firstRefItem.parent_post_id) {
body.parent_post_id = firstRefItem.parent_post_id;
body.source_image_url = String(firstRefItem.source_image_url || firstRef || '').trim();
} else if (firstRef) {
if (firstRef.startsWith('data:image/')) {
body.image_base64 = firstRef;
} else {
body.image_url = firstRef;
}
}
}
if (state.currentParentPostId) {
if (state.currentParentPostId && (!body.reference_items || !body.reference_items.length)) {
body.parent_post_id = state.currentParentPostId;
if (state.currentSourceImageUrl) {
body.source_image_url = state.currentSourceImageUrl;
+69 -10
View File
@@ -712,6 +712,17 @@
return `https://imagine-public.x.ai/imagine-public/images/${parentPostId}.jpg`;
}
function isImagePreviewUrl(value) {
const raw = String(value || '').trim();
if (!raw) return false;
if (raw.startsWith('data:image/')) return true;
if (/\/imagine-public\/images\/[0-9a-fA-F-]{32,36}(?:\.[A-Za-z0-9]+|[/?#]|$)/i.test(raw)) return true;
if (/\/v1\/files\/image\//i.test(raw)) return true;
if (/\/users\/.+\.(?:jpg|jpeg|png|webp)(?:[?#].*)?$/i.test(raw)) return true;
if (/\.(?:jpg|jpeg|png|webp)(?:[?#].*)?$/i.test(raw)) return true;
return false;
}
function pickPreviewUrl(hit, parentPostId, fallbackValue = '') {
const candidates = [
hit && hit.imageUrl,
@@ -722,7 +733,7 @@
];
for (const candidate of candidates) {
const raw = String(candidate || '').trim();
if (raw) return raw;
if (isImagePreviewUrl(raw)) return raw;
}
return pickSourceUrl(hit, parentPostId, fallbackValue);
}
@@ -923,6 +934,48 @@
syncPromptRichInputFromTextarea();
}
function appendReferenceItems(items, mode = 'upload') {
const candidates = (Array.isArray(items) ? items : [])
.filter((item) => item && (item.previewUrl || item.sourceUrl || item.url))
.map((item) => ({
...item,
parentPostId: String(item.parentPostId || '').trim(),
sourceUrl: String(item.sourceUrl || item.url || '').trim(),
url: String(item.url || item.sourceUrl || '').trim()
}));
if (!candidates.length) return false;
const merged = Array.isArray(referenceImages) ? referenceImages.slice() : [];
let appended = false;
for (const item of candidates) {
const itemParentId = String(item.parentPostId || '').trim();
const itemSourceUrl = String(item.sourceUrl || item.url || '').trim();
const exists = merged.some((current) => {
const currentParentId = String(current.parentPostId || '').trim();
const currentSourceUrl = String(current.sourceUrl || current.url || '').trim();
if (itemParentId && currentParentId && itemParentId === currentParentId) return true;
if (itemSourceUrl && currentSourceUrl && itemSourceUrl === currentSourceUrl) return true;
return false;
});
if (exists) {
continue;
}
if (merged.length >= REFERENCE_LIMIT) {
toast(`最多支持 ${REFERENCE_LIMIT} 张参考图`, 'warning');
break;
}
merged.push(item);
appended = true;
}
if (!appended) return false;
setReferenceItems(
merged,
merged.some((item) => String(item.parentPostId || '').trim()) ? 'parent_post' : mode
);
return true;
}
function hideReferenceMentionMenu() {
activeMentionIndex = -1;
if (!referenceMentionMenu) return;
@@ -1322,6 +1375,7 @@
function applyParentPostReference(text, options = {}) {
const silent = Boolean(options.silent);
const append = Boolean(options.append);
const resolved = resolveReferenceByText(text);
const raw = String(text || '').trim();
const fallbackId = extractParentPostId(raw);
@@ -1336,16 +1390,21 @@
}
return false;
}
setReferenceItems([{
const targetItem = {
id: makeReferenceId('parent'),
previewUrl: finalPreviewUrl,
sourceUrl: finalSourceUrl || finalPreviewUrl,
url: finalPreviewUrl,
parentPostId: finalParentId,
name: finalParentId || 'parentPostId'
}], 'parent_post');
};
if (append) {
appendReferenceItems([targetItem], 'parent_post');
} else {
setReferenceItems([targetItem], 'parent_post');
}
if (!silent) {
toast('已使用 parentPostId 填充参考图', 'success');
toast(append ? '已追加参考图' : '已使用 parentPostId 填充参考图', 'success');
}
return true;
}
@@ -3299,7 +3358,7 @@
if (applyParentBtn) {
applyParentBtn.addEventListener('click', () => {
applyParentPostReference(parentPostInput ? parentPostInput.value : '');
applyParentPostReference(parentPostInput ? parentPostInput.value : '', { append: true });
});
}
@@ -3307,7 +3366,7 @@
parentPostInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
applyParentPostReference(parentPostInput.value);
applyParentPostReference(parentPostInput.value, { append: true });
}
});
parentPostInput.addEventListener('input', () => {
@@ -3326,7 +3385,7 @@
event.preventDefault();
event.stopPropagation();
parentPostInput.value = text;
applyParentPostReference(text, { silent: true });
applyParentPostReference(text, { silent: true, append: true });
});
}
@@ -3368,13 +3427,13 @@
event.preventDefault();
event.stopPropagation();
imageUrlInput.value = text;
const applied = applyParentPostReference(text, { silent: true });
const applied = applyParentPostReference(text, { silent: true, append: true });
if (!applied) {
const resolved = resolveReferenceByText(text);
if (resolved.parentPostId && parentPostInput) {
parentPostInput.value = resolved.parentPostId;
}
setReferenceItems([{
appendReferenceItems([{
id: makeReferenceId('url'),
previewUrl: resolved.url || resolved.sourceUrl || text,
sourceUrl: resolved.sourceUrl || text,
@@ -3409,7 +3468,7 @@
if (!isTypingInParentInput && !isTypingInImageUrlInput && (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target?.isContentEditable)) {
return;
}
const applied = applyParentPostReference(text, { silent: true });
const applied = applyParentPostReference(text, { silent: true, append: true });
if (applied) {
event.preventDefault();
}
@@ -167,7 +167,7 @@
<div id="app-footer"></div>
<script src="/static/common/js/admin-auth.js?v=0.3.0"></script>
<script src="/static/common/js/parentpost-memory.js?v=0.1.0"></script>
<script src="/static/common/js/parentpost-memory.js?v=0.1.1"></script>
<script src="/static/common/js/public-header.js?v=0.3.3"></script>
<script src="/static/common/js/footer.js?v=0.3.0"></script>
<script src="/static/common/js/toast.js"></script>
+1 -1
View File
@@ -363,7 +363,7 @@
<div id="mobileSettingsOverlay" class="mobile-settings-overlay"></div>
<script src="/static/common/js/admin-auth.js?v=0.3.0"></script>
<script src="/static/common/js/parentpost-memory.js?v=0.1.0"></script>
<script src="/static/common/js/parentpost-memory.js?v=0.1.1"></script>
<script src="/static/common/js/public-header.js?v=0.3.3"></script>
<script src="/static/common/js/footer.js?v=0.3.0"></script>
<script src="/static/common/js/toast.js"></script>