fix(runtime-queue): stabilize manual send and composer growth
This commit is contained in:
parent
aa3b9bbfe3
commit
209dbf5532
@ -1444,6 +1444,17 @@ async def handle_task_with_sender(
|
|||||||
socketio.start_background_task(run_bg_poll)
|
socketio.start_background_task(run_bg_poll)
|
||||||
|
|
||||||
has_running_completion_jobs = has_running_sub_agents or has_running_background_commands
|
has_running_completion_jobs = has_running_sub_agents or has_running_background_commands
|
||||||
|
pending_runtime_guidance_messages: List[str] = []
|
||||||
|
try:
|
||||||
|
from .tasks import task_manager
|
||||||
|
|
||||||
|
pending_runtime_guidance_messages = task_manager.consume_runtime_guidance_messages(
|
||||||
|
username=username,
|
||||||
|
task_id=client_sid,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[RuntimeGuidance] 读取剩余引导消息失败: {exc}")
|
||||||
|
pending_runtime_guidance_messages = []
|
||||||
|
|
||||||
# 发送完成事件(如果有后台完成任务在运行,前端会保持等待状态)
|
# 发送完成事件(如果有后台完成任务在运行,前端会保持等待状态)
|
||||||
if not has_running_completion_jobs:
|
if not has_running_completion_jobs:
|
||||||
@ -1458,4 +1469,5 @@ async def handle_task_with_sender(
|
|||||||
# 沿用子智能体字段,确保前端直接走已验证通路
|
# 沿用子智能体字段,确保前端直接走已验证通路
|
||||||
'has_running_sub_agents': has_running_completion_jobs,
|
'has_running_sub_agents': has_running_completion_jobs,
|
||||||
'has_running_background_commands': has_running_background_commands,
|
'has_running_background_commands': has_running_background_commands,
|
||||||
|
'pending_runtime_guidance_messages': pending_runtime_guidance_messages,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -954,6 +954,52 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
if tool_failed:
|
if tool_failed:
|
||||||
mark_force_thinking(web_terminal, reason=f"{function_name}_failed")
|
mark_force_thinking(web_terminal, reason=f"{function_name}_failed")
|
||||||
|
|
||||||
|
# 运行期“引导对话”:需要等待同一轮全部工具执行结束后再注入,
|
||||||
|
# 避免一轮内并行/多工具调用时过早插入。
|
||||||
|
try:
|
||||||
|
from .tasks import task_manager
|
||||||
|
|
||||||
|
runtime_guidance_original = task_manager.pop_runtime_guidance_for_injection(
|
||||||
|
username=username, task_id=client_sid
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
runtime_guidance_original = None
|
||||||
|
debug_log(f"[RuntimeGuidance] 读取引导队列失败: {exc}")
|
||||||
|
|
||||||
|
if runtime_guidance_original:
|
||||||
|
runtime_guidance_text = runtime_guidance_original
|
||||||
|
runtime_guidance_meta = {
|
||||||
|
"runtime_guidance": True,
|
||||||
|
"runtime_guidance_original": runtime_guidance_original,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
web_terminal.context_manager.add_conversation(
|
||||||
|
"user",
|
||||||
|
runtime_guidance_text,
|
||||||
|
metadata=runtime_guidance_meta,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[RuntimeGuidance] 持久化引导消息失败: {exc}")
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": runtime_guidance_text,
|
||||||
|
"metadata": runtime_guidance_meta,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
sender(
|
||||||
|
"user_message",
|
||||||
|
{
|
||||||
|
"message": runtime_guidance_text,
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"runtime_guidance": True,
|
||||||
|
"runtime_guidance_original": runtime_guidance_original,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
debug_log(
|
||||||
|
"[RuntimeGuidance] 已在工具结果批次结束后注入引导消息 "
|
||||||
|
f"task_id={client_sid}"
|
||||||
|
)
|
||||||
|
|
||||||
web_terminal._tool_loop_active = previous_tool_loop_active
|
web_terminal._tool_loop_active = previous_tool_loop_active
|
||||||
return {"stopped": False, "last_tool_call_time": last_tool_call_time}
|
return {"stopped": False, "last_tool_call_time": last_tool_call_time}
|
||||||
|
|||||||
340
server/tasks.py
340
server/tasks.py
@ -38,6 +38,8 @@ class TaskRecord:
|
|||||||
"session_data",
|
"session_data",
|
||||||
"stop_requested",
|
"stop_requested",
|
||||||
"next_event_idx",
|
"next_event_idx",
|
||||||
|
"runtime_pending_queue",
|
||||||
|
"runtime_guidance_queue",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -72,6 +74,8 @@ class TaskRecord:
|
|||||||
self.session_data: Dict[str, Any] = {}
|
self.session_data: Dict[str, Any] = {}
|
||||||
self.stop_requested: bool = False
|
self.stop_requested: bool = False
|
||||||
self.next_event_idx: int = 0
|
self.next_event_idx: int = 0
|
||||||
|
self.runtime_pending_queue: List[Dict[str, Any]] = []
|
||||||
|
self.runtime_guidance_queue: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
class TaskManager:
|
class TaskManager:
|
||||||
@ -199,6 +203,233 @@ class TaskManager:
|
|||||||
debug_log(f"[Task] 任务 {task_id} 已取消,事件队列已清空")
|
debug_log(f"[Task] 任务 {task_id} 已取消,事件队列已清空")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_runtime_pending_queue(raw_queue: Any) -> List[Dict[str, Any]]:
|
||||||
|
now_ts = time.time()
|
||||||
|
normalized: List[Dict[str, Any]] = []
|
||||||
|
if not isinstance(raw_queue, list):
|
||||||
|
return normalized
|
||||||
|
for raw_item in raw_queue:
|
||||||
|
if isinstance(raw_item, dict):
|
||||||
|
item_id = str(raw_item.get("id") or "").strip()
|
||||||
|
text = str(raw_item.get("text") or "").strip()
|
||||||
|
created_at = raw_item.get("created_at")
|
||||||
|
else:
|
||||||
|
item_id = ""
|
||||||
|
text = str(raw_item or "").strip()
|
||||||
|
created_at = None
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
if not item_id:
|
||||||
|
item_id = str(uuid.uuid4())
|
||||||
|
try:
|
||||||
|
created_at_float = float(created_at)
|
||||||
|
except Exception:
|
||||||
|
created_at_float = now_ts
|
||||||
|
normalized.append(
|
||||||
|
{
|
||||||
|
"id": item_id,
|
||||||
|
"text": text,
|
||||||
|
"created_at": created_at_float,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _runtime_pending_public(queue: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
result: List[Dict[str, Any]] = []
|
||||||
|
for item in queue or []:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
text = str(item.get("text") or "").strip()
|
||||||
|
item_id = str(item.get("id") or "").strip()
|
||||||
|
if not text or not item_id:
|
||||||
|
continue
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"id": item_id,
|
||||||
|
"text": text,
|
||||||
|
"created_at": item.get("created_at"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def enqueue_runtime_pending_message(
|
||||||
|
self, username: str, task_id: str, message: str, max_queue_size: int = 5
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
text = str(message or "").strip()
|
||||||
|
if not text:
|
||||||
|
return {"success": False, "code": "empty_message", "error": "消息不能为空"}
|
||||||
|
limit = int(max(1, max_queue_size))
|
||||||
|
with self._lock:
|
||||||
|
rec = self._tasks.get(task_id)
|
||||||
|
if not rec or rec.username != username:
|
||||||
|
return {"success": False, "code": "task_not_found", "error": "任务不存在"}
|
||||||
|
if rec.status not in {"pending", "running", "cancel_requested"}:
|
||||||
|
return {"success": False, "code": "task_not_running", "error": "任务已结束,无法追加消息"}
|
||||||
|
queue = self._normalize_runtime_pending_queue(getattr(rec, "runtime_pending_queue", None))
|
||||||
|
if len(queue) >= limit:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"code": "queue_full",
|
||||||
|
"error": f"堆积消息已满(最多 {limit} 条)",
|
||||||
|
}
|
||||||
|
item = {
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"text": text,
|
||||||
|
"created_at": time.time(),
|
||||||
|
}
|
||||||
|
queue.append(item)
|
||||||
|
rec.runtime_pending_queue = queue
|
||||||
|
rec.updated_at = time.time()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"task_id": rec.task_id,
|
||||||
|
"item": item,
|
||||||
|
"messages": self._runtime_pending_public(queue),
|
||||||
|
}
|
||||||
|
|
||||||
|
def remove_runtime_pending_message(
|
||||||
|
self, username: str, task_id: str, message_id: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
target_id = str(message_id or "").strip()
|
||||||
|
if not target_id:
|
||||||
|
return {"success": False, "code": "invalid_message_id", "error": "消息ID无效"}
|
||||||
|
with self._lock:
|
||||||
|
rec = self._tasks.get(task_id)
|
||||||
|
if not rec or rec.username != username:
|
||||||
|
return {"success": False, "code": "task_not_found", "error": "任务不存在"}
|
||||||
|
queue = self._normalize_runtime_pending_queue(getattr(rec, "runtime_pending_queue", None))
|
||||||
|
remove_idx = -1
|
||||||
|
for idx, item in enumerate(queue):
|
||||||
|
if str(item.get("id") or "") == target_id:
|
||||||
|
remove_idx = idx
|
||||||
|
break
|
||||||
|
if remove_idx < 0:
|
||||||
|
return {"success": False, "code": "message_not_found", "error": "消息不存在"}
|
||||||
|
queue.pop(remove_idx)
|
||||||
|
rec.runtime_pending_queue = queue
|
||||||
|
rec.updated_at = time.time()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"task_id": rec.task_id,
|
||||||
|
"messages": self._runtime_pending_public(queue),
|
||||||
|
}
|
||||||
|
|
||||||
|
def promote_runtime_pending_to_guidance(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
task_id: str,
|
||||||
|
message_id: str,
|
||||||
|
max_guidance_queue_size: int = 5,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
target_id = str(message_id or "").strip()
|
||||||
|
if not target_id:
|
||||||
|
return {"success": False, "code": "invalid_message_id", "error": "消息ID无效"}
|
||||||
|
guidance_limit = int(max(1, max_guidance_queue_size))
|
||||||
|
with self._lock:
|
||||||
|
rec = self._tasks.get(task_id)
|
||||||
|
if not rec or rec.username != username:
|
||||||
|
return {"success": False, "code": "task_not_found", "error": "任务不存在"}
|
||||||
|
if rec.status not in {"pending", "running", "cancel_requested"}:
|
||||||
|
return {"success": False, "code": "task_not_running", "error": "任务已结束,无法引导"}
|
||||||
|
queue = self._normalize_runtime_pending_queue(getattr(rec, "runtime_pending_queue", None))
|
||||||
|
guidance_queue = getattr(rec, "runtime_guidance_queue", None)
|
||||||
|
if not isinstance(guidance_queue, list):
|
||||||
|
guidance_queue = []
|
||||||
|
if len(guidance_queue) >= guidance_limit:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"code": "guidance_queue_full",
|
||||||
|
"error": f"引导队列已满(最多 {guidance_limit} 条)",
|
||||||
|
}
|
||||||
|
selected = None
|
||||||
|
remain_queue: List[Dict[str, Any]] = []
|
||||||
|
for item in queue:
|
||||||
|
if selected is None and str(item.get("id") or "") == target_id:
|
||||||
|
selected = item
|
||||||
|
continue
|
||||||
|
remain_queue.append(item)
|
||||||
|
if not selected:
|
||||||
|
return {"success": False, "code": "message_not_found", "error": "消息不存在"}
|
||||||
|
selected_text = str(selected.get("text") or "").strip()
|
||||||
|
if not selected_text:
|
||||||
|
return {"success": False, "code": "empty_message", "error": "消息内容为空"}
|
||||||
|
guidance_queue.append(selected_text)
|
||||||
|
rec.runtime_guidance_queue = guidance_queue
|
||||||
|
rec.runtime_pending_queue = remain_queue
|
||||||
|
rec.updated_at = time.time()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"task_id": rec.task_id,
|
||||||
|
"queued_count": len(guidance_queue),
|
||||||
|
"messages": self._runtime_pending_public(remain_queue),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_runtime_pending_messages(self, username: str, task_id: str) -> List[Dict[str, Any]]:
|
||||||
|
with self._lock:
|
||||||
|
rec = self._tasks.get(task_id)
|
||||||
|
if not rec or rec.username != username:
|
||||||
|
return []
|
||||||
|
queue = self._normalize_runtime_pending_queue(getattr(rec, "runtime_pending_queue", None))
|
||||||
|
rec.runtime_pending_queue = queue
|
||||||
|
return self._runtime_pending_public(queue)
|
||||||
|
|
||||||
|
def enqueue_runtime_guidance(
|
||||||
|
self, username: str, task_id: str, message: str, max_queue_size: int = 5
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
text = str(message or "").strip()
|
||||||
|
if not text:
|
||||||
|
return {"success": False, "code": "empty_message", "error": "引导内容不能为空"}
|
||||||
|
with self._lock:
|
||||||
|
rec = self._tasks.get(task_id)
|
||||||
|
if not rec or rec.username != username:
|
||||||
|
return {"success": False, "code": "task_not_found", "error": "任务不存在"}
|
||||||
|
if rec.status not in {"pending", "running", "cancel_requested"}:
|
||||||
|
return {"success": False, "code": "task_not_running", "error": "任务已结束,无法追加引导"}
|
||||||
|
queue = getattr(rec, "runtime_guidance_queue", None)
|
||||||
|
if not isinstance(queue, list):
|
||||||
|
queue = []
|
||||||
|
rec.runtime_guidance_queue = queue
|
||||||
|
if len(queue) >= int(max(1, max_queue_size)):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"code": "queue_full",
|
||||||
|
"error": f"引导队列已满(最多 {int(max(1, max_queue_size))} 条)",
|
||||||
|
}
|
||||||
|
queue.append(text)
|
||||||
|
rec.updated_at = time.time()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"queued_count": len(queue),
|
||||||
|
"task_id": rec.task_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def pop_runtime_guidance_for_injection(self, username: str, task_id: str) -> Optional[str]:
|
||||||
|
with self._lock:
|
||||||
|
rec = self._tasks.get(task_id)
|
||||||
|
if not rec or rec.username != username:
|
||||||
|
return None
|
||||||
|
queue = getattr(rec, "runtime_guidance_queue", None)
|
||||||
|
if not isinstance(queue, list) or not queue:
|
||||||
|
return None
|
||||||
|
item = str(queue.pop(0) or "").strip()
|
||||||
|
rec.updated_at = time.time()
|
||||||
|
return item or None
|
||||||
|
|
||||||
|
def consume_runtime_guidance_messages(self, username: str, task_id: str) -> List[str]:
|
||||||
|
with self._lock:
|
||||||
|
rec = self._tasks.get(task_id)
|
||||||
|
if not rec or rec.username != username:
|
||||||
|
return []
|
||||||
|
queue = getattr(rec, "runtime_guidance_queue", None)
|
||||||
|
if not isinstance(queue, list) or not queue:
|
||||||
|
return []
|
||||||
|
items = [str(item or "").strip() for item in queue]
|
||||||
|
rec.runtime_guidance_queue = []
|
||||||
|
rec.updated_at = time.time()
|
||||||
|
return [item for item in items if item]
|
||||||
|
|
||||||
# ---- internal helpers ----
|
# ---- internal helpers ----
|
||||||
def _append_event(self, rec: TaskRecord, event_type: str, data: Dict[str, Any]):
|
def _append_event(self, rec: TaskRecord, event_type: str, data: Dict[str, Any]):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@ -574,6 +805,9 @@ def get_task_api(task_id: str):
|
|||||||
"error": rec.error,
|
"error": rec.error,
|
||||||
"events": events,
|
"events": events,
|
||||||
"next_offset": next_offset,
|
"next_offset": next_offset,
|
||||||
|
"runtime_queued_messages": task_manager.get_runtime_pending_messages(
|
||||||
|
username, task_id
|
||||||
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -586,3 +820,109 @@ def cancel_task_api(task_id: str):
|
|||||||
if not ok:
|
if not ok:
|
||||||
return jsonify({"success": False, "error": "任务不存在"}), 404
|
return jsonify({"success": False, "error": "任务不存在"}), 404
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_bp.route("/api/tasks/<task_id>/runtime_guidance", methods=["POST"])
|
||||||
|
@api_login_required
|
||||||
|
def enqueue_runtime_guidance_api(task_id: str):
|
||||||
|
username = get_current_username()
|
||||||
|
payload = request.get_json() or {}
|
||||||
|
message = (payload.get("message") or "").strip()
|
||||||
|
if not message:
|
||||||
|
return jsonify({"success": False, "error": "引导内容不能为空"}), 400
|
||||||
|
|
||||||
|
result = task_manager.enqueue_runtime_guidance(username, task_id, message)
|
||||||
|
if not result.get("success"):
|
||||||
|
code = result.get("code") or "runtime_guidance_failed"
|
||||||
|
if code == "task_not_found":
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "任务不存在"}), 404
|
||||||
|
if code in {"queue_full", "task_not_running"}:
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "任务状态不允许"}), 409
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "追加引导失败"}), 400
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"task_id": result.get("task_id"),
|
||||||
|
"queued_count": result.get("queued_count", 0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_bp.route("/api/tasks/<task_id>/runtime_queue", methods=["POST"])
|
||||||
|
@api_login_required
|
||||||
|
def enqueue_runtime_queue_message_api(task_id: str):
|
||||||
|
username = get_current_username()
|
||||||
|
payload = request.get_json() or {}
|
||||||
|
message = (payload.get("message") or "").strip()
|
||||||
|
if not message:
|
||||||
|
return jsonify({"success": False, "error": "消息不能为空"}), 400
|
||||||
|
result = task_manager.enqueue_runtime_pending_message(username, task_id, message)
|
||||||
|
if not result.get("success"):
|
||||||
|
code = result.get("code") or "runtime_queue_enqueue_failed"
|
||||||
|
if code == "task_not_found":
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "任务不存在"}), 404
|
||||||
|
if code in {"queue_full", "task_not_running"}:
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "任务状态不允许"}), 409
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "追加消息失败"}), 400
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"task_id": result.get("task_id"),
|
||||||
|
"item": result.get("item"),
|
||||||
|
"messages": result.get("messages") or [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_bp.route("/api/tasks/<task_id>/runtime_queue/<message_id>", methods=["DELETE"])
|
||||||
|
@api_login_required
|
||||||
|
def delete_runtime_queue_message_api(task_id: str, message_id: str):
|
||||||
|
username = get_current_username()
|
||||||
|
result = task_manager.remove_runtime_pending_message(username, task_id, message_id)
|
||||||
|
if not result.get("success"):
|
||||||
|
code = result.get("code") or "runtime_queue_delete_failed"
|
||||||
|
if code == "task_not_found":
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "任务不存在"}), 404
|
||||||
|
if code == "message_not_found":
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "消息不存在"}), 404
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "删除失败"}), 400
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"task_id": result.get("task_id"),
|
||||||
|
"messages": result.get("messages") or [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_bp.route("/api/tasks/<task_id>/runtime_queue/<message_id>/guide", methods=["POST"])
|
||||||
|
@api_login_required
|
||||||
|
def guide_runtime_queue_message_api(task_id: str, message_id: str):
|
||||||
|
username = get_current_username()
|
||||||
|
result = task_manager.promote_runtime_pending_to_guidance(username, task_id, message_id)
|
||||||
|
if not result.get("success"):
|
||||||
|
code = result.get("code") or "runtime_queue_guide_failed"
|
||||||
|
if code == "task_not_found":
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "任务不存在"}), 404
|
||||||
|
if code == "message_not_found":
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "消息不存在"}), 404
|
||||||
|
if code in {"guidance_queue_full", "task_not_running"}:
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "任务状态不允许"}), 409
|
||||||
|
return jsonify({"success": False, "error": result.get("error") or "引导失败"}), 400
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"task_id": result.get("task_id"),
|
||||||
|
"queued_count": result.get("queued_count", 0),
|
||||||
|
"messages": result.get("messages") or [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@ -121,6 +121,7 @@
|
|||||||
'chat-container--monitor': chatDisplayMode === 'monitor',
|
'chat-container--monitor': chatDisplayMode === 'monitor',
|
||||||
'has-title-ribbon': titleRibbonVisible
|
'has-title-ribbon': titleRibbonVisible
|
||||||
}"
|
}"
|
||||||
|
:style="chatContainerStyle"
|
||||||
>
|
>
|
||||||
<TokenDrawer
|
<TokenDrawer
|
||||||
:visible="Boolean(currentConversationId)"
|
:visible="Boolean(currentConversationId)"
|
||||||
@ -294,6 +295,7 @@
|
|||||||
:permission-options="permissionModeOptions"
|
:permission-options="permissionModeOptions"
|
||||||
:current-context-tokens="currentContextTokens"
|
:current-context-tokens="currentContextTokens"
|
||||||
:versioning-enabled="versioningEnabled"
|
:versioning-enabled="versioningEnabled"
|
||||||
|
:runtime-queued-messages="runtimeQueuedMessages"
|
||||||
@update:input-message="inputSetMessage"
|
@update:input-message="inputSetMessage"
|
||||||
@input-change="handleInputChange"
|
@input-change="handleInputChange"
|
||||||
@input-focus="handleInputFocus"
|
@input-focus="handleInputFocus"
|
||||||
@ -323,6 +325,9 @@
|
|||||||
@toggle-permission-menu="togglePermissionMenu"
|
@toggle-permission-menu="togglePermissionMenu"
|
||||||
@change-permission-mode="changePermissionMode"
|
@change-permission-mode="changePermissionMode"
|
||||||
@open-versioning-dialog="openVersioningDialog"
|
@open-versioning-dialog="openVersioningDialog"
|
||||||
|
@guide-runtime-message="handleGuideRuntimeMessage"
|
||||||
|
@delete-runtime-message="handleDeleteRuntimeMessage"
|
||||||
|
@composer-height-change="handleComposerHeightChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -129,6 +129,17 @@ export const computed = {
|
|||||||
titleRibbonVisible() {
|
titleRibbonVisible() {
|
||||||
return !this.isMobileViewport && this.chatDisplayMode === 'chat';
|
return !this.isMobileViewport && this.chatDisplayMode === 'chat';
|
||||||
},
|
},
|
||||||
|
chatContainerStyle() {
|
||||||
|
const minHeight = 80;
|
||||||
|
const raw = Number(this.composerReservedHeight || 0);
|
||||||
|
const height = Number.isFinite(raw) ? Math.max(minHeight, Math.round(raw)) : minHeight;
|
||||||
|
const growth = Math.max(0, height - minHeight);
|
||||||
|
return {
|
||||||
|
'--composer-base-height': `calc(${minHeight}px + var(--app-bottom-inset, 0px))`,
|
||||||
|
'--composer-reserved-height': `calc(${height}px + var(--app-bottom-inset, 0px))`,
|
||||||
|
'--composer-growth-height': `${growth}px`
|
||||||
|
};
|
||||||
|
},
|
||||||
...mapWritableState(useToolStore, [
|
...mapWritableState(useToolStore, [
|
||||||
'preparingTools',
|
'preparingTools',
|
||||||
'activeTools',
|
'activeTools',
|
||||||
|
|||||||
@ -94,6 +94,18 @@ export const conversationMethods = {
|
|||||||
this.inputSetLineCount(1);
|
this.inputSetLineCount(1);
|
||||||
this.inputSetMultiline(false);
|
this.inputSetMultiline(false);
|
||||||
this.inputClearMessage();
|
this.inputClearMessage();
|
||||||
|
this.runtimeQueuedMessages = [];
|
||||||
|
this.runtimeGuidanceFallbackQueue = [];
|
||||||
|
this.runtimeQueueAutoSendInProgress = false;
|
||||||
|
this.runtimeQueueSyncLockKey = '';
|
||||||
|
this.runtimeQueueSyncLockUntil = 0;
|
||||||
|
if (typeof this.clearRuntimeQueueSuppressionState === 'function') {
|
||||||
|
this.clearRuntimeQueueSuppressionState();
|
||||||
|
} else {
|
||||||
|
this.runtimeQueueSuppressedMessageIds = new Set();
|
||||||
|
this.runtimeGuidanceSuppressedTextCounts = {};
|
||||||
|
}
|
||||||
|
this.composerReservedHeight = 80;
|
||||||
this.inputClearSelectedImages();
|
this.inputClearSelectedImages();
|
||||||
this.inputSetImagePickerOpen(false);
|
this.inputSetImagePickerOpen(false);
|
||||||
this.imageEntries = [];
|
this.imageEntries = [];
|
||||||
|
|||||||
@ -3,6 +3,114 @@ import { debugLog } from './common';
|
|||||||
import { useModelStore } from '../../stores/model';
|
import { useModelStore } from '../../stores/model';
|
||||||
|
|
||||||
export const messageMethods = {
|
export const messageMethods = {
|
||||||
|
buildRuntimeQueueSnapshotKey(messages = []) {
|
||||||
|
const list = Array.isArray(messages) ? messages : [];
|
||||||
|
return JSON.stringify(
|
||||||
|
list.map((item) => [
|
||||||
|
String(item?.id || ''),
|
||||||
|
String(item?.text || ''),
|
||||||
|
Number(item?.createdAt || 0)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
setRuntimeQueueSyncLock(messages = [], ttlMs = 1800) {
|
||||||
|
this.runtimeQueueSyncLockKey = this.buildRuntimeQueueSnapshotKey(messages);
|
||||||
|
this.runtimeQueueSyncLockUntil = Date.now() + Math.max(300, Number(ttlMs || 0));
|
||||||
|
},
|
||||||
|
|
||||||
|
ensureRuntimeQueueSuppressionState() {
|
||||||
|
if (!(this.runtimeQueueSuppressedMessageIds instanceof Set)) {
|
||||||
|
this.runtimeQueueSuppressedMessageIds = new Set();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.runtimeGuidanceSuppressedTextCounts ||
|
||||||
|
typeof this.runtimeGuidanceSuppressedTextCounts !== 'object' ||
|
||||||
|
Array.isArray(this.runtimeGuidanceSuppressedTextCounts)
|
||||||
|
) {
|
||||||
|
this.runtimeGuidanceSuppressedTextCounts = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
markRuntimeQueueSuppressedByManualStop() {
|
||||||
|
this.ensureRuntimeQueueSuppressionState();
|
||||||
|
const queueList = Array.isArray(this.runtimeQueuedMessages) ? this.runtimeQueuedMessages : [];
|
||||||
|
queueList.forEach((item) => {
|
||||||
|
const id = String(item?.id || '').trim();
|
||||||
|
if (id) {
|
||||||
|
this.runtimeQueueSuppressedMessageIds.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fallbackList = Array.isArray(this.runtimeGuidanceFallbackQueue)
|
||||||
|
? this.runtimeGuidanceFallbackQueue
|
||||||
|
: [];
|
||||||
|
fallbackList.forEach((item) => {
|
||||||
|
const text = String(item || '').trim();
|
||||||
|
if (!text) return;
|
||||||
|
const counts = this.runtimeGuidanceSuppressedTextCounts;
|
||||||
|
counts[text] = Number(counts[text] || 0) + 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
consumeSuppressedRuntimeGuidanceText(rawText) {
|
||||||
|
this.ensureRuntimeQueueSuppressionState();
|
||||||
|
const text = String(rawText || '').trim();
|
||||||
|
if (!text) return false;
|
||||||
|
const counts = this.runtimeGuidanceSuppressedTextCounts;
|
||||||
|
const current = Number(counts[text] || 0);
|
||||||
|
return Number.isFinite(current) && current > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
consumeSuppressedRuntimeQueueMessageId(rawId) {
|
||||||
|
this.ensureRuntimeQueueSuppressionState();
|
||||||
|
const id = String(rawId || '').trim();
|
||||||
|
if (!id) return false;
|
||||||
|
return this.runtimeQueueSuppressedMessageIds.has(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearRuntimeQueueSuppressionState() {
|
||||||
|
this.runtimeQueueSuppressedMessageIds = new Set();
|
||||||
|
this.runtimeGuidanceSuppressedTextCounts = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
pruneSuppressedRuntimeQueues() {
|
||||||
|
this.ensureRuntimeQueueSuppressionState();
|
||||||
|
const blockedIds = this.runtimeQueueSuppressedMessageIds;
|
||||||
|
const currentQueue = Array.isArray(this.runtimeQueuedMessages)
|
||||||
|
? this.runtimeQueuedMessages
|
||||||
|
: [];
|
||||||
|
const nextQueue = currentQueue.filter((item) => {
|
||||||
|
const id = String(item?.id || '').trim();
|
||||||
|
if (!id) return false;
|
||||||
|
return !blockedIds.has(id);
|
||||||
|
});
|
||||||
|
if (nextQueue.length !== currentQueue.length) {
|
||||||
|
this.runtimeQueuedMessages = nextQueue;
|
||||||
|
this.setRuntimeQueueSyncLock(nextQueue, 2200);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFallback = Array.isArray(this.runtimeGuidanceFallbackQueue)
|
||||||
|
? this.runtimeGuidanceFallbackQueue
|
||||||
|
: [];
|
||||||
|
if (currentFallback.length > 0) {
|
||||||
|
const keptFallback = [];
|
||||||
|
currentFallback.forEach((item) => {
|
||||||
|
const text = String(item || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.consumeSuppressedRuntimeGuidanceText(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keptFallback.push(text);
|
||||||
|
});
|
||||||
|
if (keptFallback.length !== currentFallback.length) {
|
||||||
|
this.runtimeGuidanceFallbackQueue = keptFallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async executeSystemCommand(rawCommand, options = {}) {
|
async executeSystemCommand(rawCommand, options = {}) {
|
||||||
const command = (rawCommand || '').toString().trim();
|
const command = (rawCommand || '').toString().trim();
|
||||||
if (!command) {
|
if (!command) {
|
||||||
@ -102,7 +210,349 @@ export const messageMethods = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSendOrStop() {
|
applyRuntimeQueuedMessages(messages = []) {
|
||||||
|
this.ensureRuntimeQueueSuppressionState();
|
||||||
|
const previousList = Array.isArray(this.runtimeQueuedMessages)
|
||||||
|
? this.runtimeQueuedMessages
|
||||||
|
: [];
|
||||||
|
const previousById = new Map(previousList.map((item) => [item?.id, item]));
|
||||||
|
const previousIndexById = new Map(previousList.map((item, index) => [item?.id, index]));
|
||||||
|
const limit = Math.max(1, Number(this.runtimeQueueLimit || 5));
|
||||||
|
const normalizedRaw = (Array.isArray(messages) ? messages : [])
|
||||||
|
.map((item) => {
|
||||||
|
if (!item) return null;
|
||||||
|
const id = String(item.id || '').trim();
|
||||||
|
const text = String(item.text || '').trim();
|
||||||
|
if (!id || !text) return null;
|
||||||
|
const previous = previousById.get(id);
|
||||||
|
const rawCreatedAt = Number(item.created_at ?? item.createdAt ?? Date.now());
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
text,
|
||||||
|
createdAt: Number.isFinite(rawCreatedAt) ? rawCreatedAt : Date.now(),
|
||||||
|
source: previous?.source || 'user'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => !!item);
|
||||||
|
|
||||||
|
const dedupById = new Map();
|
||||||
|
normalizedRaw.forEach((item) => {
|
||||||
|
if (!item?.id || dedupById.has(item.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dedupById.set(item.id, item);
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalized = Array.from(dedupById.values())
|
||||||
|
.sort((a, b) => {
|
||||||
|
const at = Number(a?.createdAt || 0);
|
||||||
|
const bt = Number(b?.createdAt || 0);
|
||||||
|
if (at !== bt) {
|
||||||
|
return at - bt;
|
||||||
|
}
|
||||||
|
const ai = previousIndexById.has(a?.id)
|
||||||
|
? Number(previousIndexById.get(a?.id))
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
const bi = previousIndexById.has(b?.id)
|
||||||
|
? Number(previousIndexById.get(b?.id))
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
if (ai !== bi) {
|
||||||
|
return ai - bi;
|
||||||
|
}
|
||||||
|
return String(a?.id || '').localeCompare(String(b?.id || ''));
|
||||||
|
})
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
const unchanged =
|
||||||
|
previousList.length === normalized.length &&
|
||||||
|
previousList.every((item, index) => {
|
||||||
|
const next = normalized[index];
|
||||||
|
return (
|
||||||
|
item?.id === next?.id &&
|
||||||
|
item?.text === next?.text &&
|
||||||
|
Number(item?.createdAt || 0) === Number(next?.createdAt || 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unchanged) {
|
||||||
|
return previousList;
|
||||||
|
}
|
||||||
|
this.runtimeQueuedMessages = normalized;
|
||||||
|
return normalized;
|
||||||
|
},
|
||||||
|
|
||||||
|
async enqueueRuntimeQueuedMessage(rawMessage) {
|
||||||
|
const text = (rawMessage || '').toString().trim();
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
const taskId = taskStore.currentTaskId;
|
||||||
|
if (!taskId) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '暂不可堆积',
|
||||||
|
message: '未检测到活跃任务,请稍后再试',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const response = await fetch(`/api/tasks/${encodeURIComponent(taskId)}/runtime_queue`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: text
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok || !payload?.success) {
|
||||||
|
throw new Error(payload?.error || '堆积消息失败');
|
||||||
|
}
|
||||||
|
const nextQueue = this.applyRuntimeQueuedMessages(payload?.data?.messages || []) || [];
|
||||||
|
this.setRuntimeQueueSyncLock(nextQueue, 2200);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '堆积失败',
|
||||||
|
message: error?.message || '请稍后重试',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleDeleteRuntimeMessage(messageId) {
|
||||||
|
const targetId = String(messageId || '').trim();
|
||||||
|
if (!targetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
const taskId = taskStore.currentTaskId;
|
||||||
|
if (!taskId) {
|
||||||
|
const nextQueue = (this.runtimeQueuedMessages || []).filter(
|
||||||
|
(item) => item?.id !== targetId
|
||||||
|
);
|
||||||
|
this.runtimeQueuedMessages = nextQueue;
|
||||||
|
this.setRuntimeQueueSyncLock(nextQueue, 1200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/tasks/${encodeURIComponent(taskId)}/runtime_queue/${encodeURIComponent(targetId)}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok || !payload?.success) {
|
||||||
|
throw new Error(payload?.error || '删除失败');
|
||||||
|
}
|
||||||
|
const nextQueue = this.applyRuntimeQueuedMessages(payload?.data?.messages || []) || [];
|
||||||
|
this.setRuntimeQueueSyncLock(nextQueue, 2200);
|
||||||
|
} catch (error) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '删除失败',
|
||||||
|
message: error?.message || '请稍后重试',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleGuideRuntimeMessage(messageId) {
|
||||||
|
const item = (this.runtimeQueuedMessages || []).find((entry) => entry?.id === messageId);
|
||||||
|
if (!item?.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.composerBusy) {
|
||||||
|
const sent = !!(await this.sendMessage({
|
||||||
|
presetText: String(item.text || '').trim(),
|
||||||
|
source: 'runtime_queue_manual_guide'
|
||||||
|
}));
|
||||||
|
if (!sent) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '发送失败',
|
||||||
|
message: '引导消息未发出,请稍后重试',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentQueue = Array.isArray(this.runtimeQueuedMessages)
|
||||||
|
? [...this.runtimeQueuedMessages]
|
||||||
|
: [];
|
||||||
|
const nextQueue = currentQueue.filter((entry) => entry?.id !== item.id);
|
||||||
|
if (nextQueue.length !== currentQueue.length) {
|
||||||
|
this.runtimeQueuedMessages = nextQueue;
|
||||||
|
this.setRuntimeQueueSyncLock(nextQueue, 2200);
|
||||||
|
}
|
||||||
|
this.ensureRuntimeQueueSuppressionState();
|
||||||
|
if (item?.id) {
|
||||||
|
this.runtimeQueueSuppressedMessageIds.delete(item.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
const taskId = taskStore.currentTaskId;
|
||||||
|
if (!taskId) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '暂不可引导',
|
||||||
|
message: '未检测到活跃任务,请稍后再试',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/tasks/${encodeURIComponent(taskId)}/runtime_queue/${encodeURIComponent(messageId)}/guide`,
|
||||||
|
{
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok || !payload?.success) {
|
||||||
|
throw new Error(payload?.error || '提交引导失败');
|
||||||
|
}
|
||||||
|
const nextQueue = this.applyRuntimeQueuedMessages(payload?.data?.messages || []) || [];
|
||||||
|
this.setRuntimeQueueSyncLock(nextQueue, 2200);
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已设为引导对话',
|
||||||
|
message: '将在下一次工具结果后插入到当前对话',
|
||||||
|
type: 'success',
|
||||||
|
duration: 1800
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '引导失败',
|
||||||
|
message: error?.message || '请稍后重试',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async tryAutoSendRuntimeQueuedMessages(reason = 'unspecified') {
|
||||||
|
if (this.runtimeQueueAutoSendInProgress) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.composerBusy ||
|
||||||
|
this.streamingMessage ||
|
||||||
|
this.taskInProgress ||
|
||||||
|
this.stopRequested ||
|
||||||
|
this.waitingForSubAgent
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackQueue = Array.isArray(this.runtimeGuidanceFallbackQueue)
|
||||||
|
? this.runtimeGuidanceFallbackQueue
|
||||||
|
: [];
|
||||||
|
const runtimeQueue = Array.isArray(this.runtimeQueuedMessages)
|
||||||
|
? this.runtimeQueuedMessages
|
||||||
|
: [];
|
||||||
|
let nextText = '';
|
||||||
|
let source = 'runtime_queue';
|
||||||
|
let runtimeQueueMessageId = null;
|
||||||
|
let fallbackIndex = -1;
|
||||||
|
|
||||||
|
if (fallbackQueue.length > 0) {
|
||||||
|
for (let i = 0; i < fallbackQueue.length; i += 1) {
|
||||||
|
const candidate = (fallbackQueue[i] || '').toString().trim();
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.consumeSuppressedRuntimeGuidanceText(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextText = candidate;
|
||||||
|
source = 'runtime_guidance_fallback';
|
||||||
|
fallbackIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextText && runtimeQueue.length > 0) {
|
||||||
|
for (let i = 0; i < runtimeQueue.length; i += 1) {
|
||||||
|
const next = runtimeQueue[i];
|
||||||
|
const candidateId = String(next?.id || '').trim();
|
||||||
|
const candidateText = (next?.text || '').toString().trim();
|
||||||
|
if (!candidateId || !candidateText) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.consumeSuppressedRuntimeQueueMessageId(candidateId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextText = candidateText;
|
||||||
|
source = next?.source || 'runtime_queue';
|
||||||
|
runtimeQueueMessageId = candidateId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextText) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.runtimeQueueAutoSendInProgress = true;
|
||||||
|
let sent = false;
|
||||||
|
try {
|
||||||
|
sent = !!(await this.sendMessage({
|
||||||
|
presetText: nextText,
|
||||||
|
source
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
sent = false;
|
||||||
|
} finally {
|
||||||
|
this.runtimeQueueAutoSendInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source === 'runtime_guidance_fallback') {
|
||||||
|
const current = Array.isArray(this.runtimeGuidanceFallbackQueue)
|
||||||
|
? [...this.runtimeGuidanceFallbackQueue]
|
||||||
|
: [];
|
||||||
|
const removeIndex =
|
||||||
|
fallbackIndex >= 0
|
||||||
|
? fallbackIndex
|
||||||
|
: current.findIndex((item) => String(item || '').trim() === nextText);
|
||||||
|
if (removeIndex >= 0) {
|
||||||
|
current.splice(removeIndex, 1);
|
||||||
|
this.runtimeGuidanceFallbackQueue = current;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const current = Array.isArray(this.runtimeQueuedMessages)
|
||||||
|
? [...this.runtimeQueuedMessages]
|
||||||
|
: [];
|
||||||
|
if (runtimeQueueMessageId) {
|
||||||
|
const nextQueue = current.filter((item) => item?.id !== runtimeQueueMessageId);
|
||||||
|
this.runtimeQueuedMessages = nextQueue;
|
||||||
|
this.setRuntimeQueueSyncLock(nextQueue, 1500);
|
||||||
|
} else {
|
||||||
|
const removeIndex = current.findIndex(
|
||||||
|
(item) => String(item?.text || '').trim() === nextText
|
||||||
|
);
|
||||||
|
if (removeIndex >= 0) {
|
||||||
|
current.splice(removeIndex, 1);
|
||||||
|
this.runtimeQueuedMessages = current;
|
||||||
|
this.setRuntimeQueueSyncLock(current, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugLog('[RuntimeQueue] auto-send success', {
|
||||||
|
reason,
|
||||||
|
source,
|
||||||
|
messagePreview: nextText.slice(0, 60)
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleSendOrStop() {
|
||||||
if (this.compressionInProgress) {
|
if (this.compressionInProgress) {
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '对话自动压缩中',
|
title: '对话自动压缩中',
|
||||||
@ -111,15 +561,39 @@ export const messageMethods = {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const hasText = !!((this.inputMessage || '').trim().length > 0);
|
||||||
|
const hasMedia =
|
||||||
|
(Array.isArray(this.selectedImages) && this.selectedImages.length > 0) ||
|
||||||
|
(Array.isArray(this.selectedVideos) && this.selectedVideos.length > 0);
|
||||||
if (this.composerBusy) {
|
if (this.composerBusy) {
|
||||||
|
if (hasText) {
|
||||||
|
const queued = await this.enqueueRuntimeQueuedMessage(this.inputMessage);
|
||||||
|
if (queued) {
|
||||||
|
this.inputClearMessage();
|
||||||
|
this.inputSetLineCount(1);
|
||||||
|
this.inputSetMultiline(false);
|
||||||
|
this.autoResizeInput();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasMedia) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '运行中仅支持文本',
|
||||||
|
message: '图片/视频请等待当前任务结束后发送',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.stopTask();
|
this.stopTask();
|
||||||
} else {
|
} else {
|
||||||
this.sendMessage();
|
this.sendMessage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendMessage() {
|
async sendMessage(options = {}) {
|
||||||
console.log('[DEBUG_AWAITING] ===== sendMessage =====');
|
console.log('[DEBUG_AWAITING] ===== sendMessage =====');
|
||||||
|
const presetText = typeof options?.presetText === 'string' ? options.presetText : null;
|
||||||
|
const usePresetText = presetText !== null;
|
||||||
|
|
||||||
if (this.compressionInProgress) {
|
if (this.compressionInProgress) {
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
@ -127,10 +601,10 @@ export const messageMethods = {
|
|||||||
message: '压缩完成后才能继续发送消息',
|
message: '压缩完成后才能继续发送消息',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.streamingUi) {
|
if (this.streamingUi && !usePresetText) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
@ -138,32 +612,40 @@ export const messageMethods = {
|
|||||||
message: '当前无法发送消息,请等待连接恢复后重试',
|
message: '当前无法发送消息,请等待连接恢复后重试',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.mediaUploading) {
|
if (this.mediaUploading && !usePresetText) {
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '上传中',
|
title: '上传中',
|
||||||
message: '请等待图片/视频上传完成后再发送',
|
message: '请等待图片/视频上传完成后再发送',
|
||||||
type: 'info'
|
type: 'info'
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = (this.inputMessage || '').trim();
|
const text = ((usePresetText ? presetText : this.inputMessage) || '').trim();
|
||||||
const images = Array.isArray(this.selectedImages) ? this.selectedImages.slice(0, 9) : [];
|
const images = usePresetText
|
||||||
const videos = Array.isArray(this.selectedVideos) ? this.selectedVideos.slice(0, 1) : [];
|
? []
|
||||||
|
: Array.isArray(this.selectedImages)
|
||||||
|
? this.selectedImages.slice(0, 9)
|
||||||
|
: [];
|
||||||
|
const videos = usePresetText
|
||||||
|
? []
|
||||||
|
: Array.isArray(this.selectedVideos)
|
||||||
|
? this.selectedVideos.slice(0, 1)
|
||||||
|
: [];
|
||||||
const hasText = text.length > 0;
|
const hasText = text.length > 0;
|
||||||
const hasImages = images.length > 0;
|
const hasImages = images.length > 0;
|
||||||
const hasVideos = videos.length > 0;
|
const hasVideos = videos.length > 0;
|
||||||
|
|
||||||
if (!hasText && !hasImages && !hasVideos) {
|
if (!hasText && !hasImages && !hasVideos) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quotaType = this.thinkingMode ? 'thinking' : 'fast';
|
const quotaType = this.thinkingMode ? 'thinking' : 'fast';
|
||||||
if (this.isQuotaExceeded(quotaType)) {
|
if (this.isQuotaExceeded(quotaType)) {
|
||||||
this.showQuotaToast({ type: quotaType });
|
this.showQuotaToast({ type: quotaType });
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelStore = useModelStore();
|
const modelStore = useModelStore();
|
||||||
@ -174,7 +656,7 @@ export const messageMethods = {
|
|||||||
message: '请切换到支持图片输入的模型再发送图片',
|
message: '请切换到支持图片输入的模型再发送图片',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasVideos && !currentModel?.supportsVideo) {
|
if (hasVideos && !currentModel?.supportsVideo) {
|
||||||
@ -183,7 +665,7 @@ export const messageMethods = {
|
|||||||
message: '请切换到支持视频输入的模型后再发送视频',
|
message: '请切换到支持视频输入的模型后再发送视频',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasVideos && hasImages) {
|
if (hasVideos && hasImages) {
|
||||||
@ -192,7 +674,7 @@ export const messageMethods = {
|
|||||||
message: '视频与图片需分开发送,每条仅包含一种媒体',
|
message: '视频与图片需分开发送,每条仅包含一种媒体',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasVideos) {
|
if (hasVideos) {
|
||||||
@ -256,7 +738,7 @@ export const messageMethods = {
|
|||||||
message: error?.message || '创建新对话失败,请重试',
|
message: error?.message || '创建新对话失败,请重试',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,21 +787,23 @@ export const messageMethods = {
|
|||||||
if (typeof this.forceUnlockMonitor === 'function') {
|
if (typeof this.forceUnlockMonitor === 'function') {
|
||||||
this.forceUnlockMonitor('create_task_failed');
|
this.forceUnlockMonitor('create_task_failed');
|
||||||
}
|
}
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.inputClearMessage();
|
if (!usePresetText) {
|
||||||
this.inputClearSelectedImages();
|
this.inputClearMessage();
|
||||||
this.inputClearSelectedVideos();
|
this.inputClearSelectedImages();
|
||||||
this.inputSetImagePickerOpen(false);
|
this.inputClearSelectedVideos();
|
||||||
this.inputSetVideoPickerOpen(false);
|
this.inputSetImagePickerOpen(false);
|
||||||
this.inputSetLineCount(1);
|
this.inputSetVideoPickerOpen(false);
|
||||||
this.inputSetMultiline(false);
|
this.inputSetLineCount(1);
|
||||||
this.persistComposerDraftNow({
|
this.inputSetMultiline(false);
|
||||||
reason: 'send-message-cleared',
|
this.persistComposerDraftNow({
|
||||||
force: true,
|
reason: 'send-message-cleared',
|
||||||
keepalive: true
|
force: true,
|
||||||
}).catch(() => {});
|
keepalive: true
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
if (hasImages) {
|
if (hasImages) {
|
||||||
this.conversationHasImages = true;
|
this.conversationHasImages = true;
|
||||||
this.conversationHasVideos = false;
|
this.conversationHasVideos = false;
|
||||||
@ -339,6 +823,7 @@ export const messageMethods = {
|
|||||||
this.updateCurrentContextTokens();
|
this.updateCurrentContextTokens();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 新增:停止任务方法
|
// 新增:停止任务方法
|
||||||
@ -359,6 +844,9 @@ export const messageMethods = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldDropToolEvents = this.streamingUi;
|
const shouldDropToolEvents = this.streamingUi;
|
||||||
|
if (typeof this.markRuntimeQueueSuppressedByManualStop === 'function') {
|
||||||
|
this.markRuntimeQueueSuppressedByManualStop();
|
||||||
|
}
|
||||||
this.stopRequested = true;
|
this.stopRequested = true;
|
||||||
this.dropToolEvents = shouldDropToolEvents;
|
this.dropToolEvents = shouldDropToolEvents;
|
||||||
|
|
||||||
|
|||||||
@ -166,7 +166,8 @@ export const taskPollingMethods = {
|
|||||||
const tasks = Array.isArray(subResult?.data) ? subResult.data : [];
|
const tasks = Array.isArray(subResult?.data) ? subResult.data : [];
|
||||||
const relevantTasks = tasks.filter((task: any) => {
|
const relevantTasks = tasks.filter((task: any) => {
|
||||||
if (!task) return false;
|
if (!task) return false;
|
||||||
if (task.conversation_id && task.conversation_id !== this.currentConversationId) return false;
|
if (task.conversation_id && task.conversation_id !== this.currentConversationId)
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
hasSubAgentPending = relevantTasks.some((task: any) => {
|
hasSubAgentPending = relevantTasks.some((task: any) => {
|
||||||
@ -202,8 +203,7 @@ export const taskPollingMethods = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
resolved,
|
resolved,
|
||||||
hasPending:
|
hasPending: !resolved || hasSubAgentPending || hasBackgroundPending,
|
||||||
!resolved || hasSubAgentPending || hasBackgroundPending,
|
|
||||||
hasBackgroundPending
|
hasBackgroundPending
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -568,6 +568,9 @@ export const taskPollingMethods = {
|
|||||||
case 'system_message':
|
case 'system_message':
|
||||||
this.handleSystemMessage(eventData, eventIdx);
|
this.handleSystemMessage(eventData, eventIdx);
|
||||||
break;
|
break;
|
||||||
|
case 'runtime_queue_sync':
|
||||||
|
this.handleRuntimeQueueSync(eventData);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
debugLog(`[TaskPolling] 未知事件类型: ${eventType}`);
|
debugLog(`[TaskPolling] 未知事件类型: ${eventType}`);
|
||||||
@ -1238,6 +1241,29 @@ export const taskPollingMethods = {
|
|||||||
handleTaskComplete(data: any) {
|
handleTaskComplete(data: any) {
|
||||||
const pendingToolsBefore =
|
const pendingToolsBefore =
|
||||||
typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null;
|
typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null;
|
||||||
|
const pendingRuntimeGuidance = Array.isArray(data?.pending_runtime_guidance_messages)
|
||||||
|
? data.pending_runtime_guidance_messages
|
||||||
|
.map((item: any) => String(item || '').trim())
|
||||||
|
.filter((item: string) => item.length > 0)
|
||||||
|
: [];
|
||||||
|
if (pendingRuntimeGuidance.length > 0) {
|
||||||
|
const limit = Math.max(1, Number(this.runtimeQueueLimit || 5));
|
||||||
|
const mergedGuidance = [
|
||||||
|
...(this.runtimeGuidanceFallbackQueue || []),
|
||||||
|
...pendingRuntimeGuidance
|
||||||
|
]
|
||||||
|
.map((item: any) => String(item || '').trim())
|
||||||
|
.filter((item: string) => item.length > 0)
|
||||||
|
.slice(0, limit);
|
||||||
|
const queueAllowance = Math.max(0, limit - mergedGuidance.length);
|
||||||
|
if (
|
||||||
|
Array.isArray(this.runtimeQueuedMessages) &&
|
||||||
|
this.runtimeQueuedMessages.length > queueAllowance
|
||||||
|
) {
|
||||||
|
this.runtimeQueuedMessages = this.runtimeQueuedMessages.slice(0, queueAllowance);
|
||||||
|
}
|
||||||
|
this.runtimeGuidanceFallbackQueue = mergedGuidance;
|
||||||
|
}
|
||||||
jsonDebug('handleTaskComplete:before', {
|
jsonDebug('handleTaskComplete:before', {
|
||||||
data,
|
data,
|
||||||
taskInProgress: this.taskInProgress,
|
taskInProgress: this.taskInProgress,
|
||||||
@ -1283,6 +1309,11 @@ export const taskPollingMethods = {
|
|||||||
this.waitingForBackgroundCommand = false;
|
this.waitingForBackgroundCommand = false;
|
||||||
this.stopWaitingTaskProbe();
|
this.stopWaitingTaskProbe();
|
||||||
this.clearTaskState(); // 清理任务状态
|
this.clearTaskState(); // 清理任务状态
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (typeof this.tryAutoSendRuntimeQueuedMessages === 'function') {
|
||||||
|
this.tryAutoSendRuntimeQueuedMessages('task_complete');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
@ -1335,6 +1366,11 @@ export const taskPollingMethods = {
|
|||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|
||||||
this.clearTaskState();
|
this.clearTaskState();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (typeof this.tryAutoSendRuntimeQueuedMessages === 'function') {
|
||||||
|
this.tryAutoSendRuntimeQueuedMessages('task_stopped');
|
||||||
|
}
|
||||||
|
});
|
||||||
jsonDebug('handleTaskStopped:after', {
|
jsonDebug('handleTaskStopped:after', {
|
||||||
taskInProgress: this.taskInProgress,
|
taskInProgress: this.taskInProgress,
|
||||||
streamingMessage: this.streamingMessage,
|
streamingMessage: this.streamingMessage,
|
||||||
@ -1342,6 +1378,55 @@ export const taskPollingMethods = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleRuntimeQueueSync(data: any) {
|
||||||
|
const messages = Array.isArray(data?.messages) ? data.messages : [];
|
||||||
|
const buildSnapshotKey =
|
||||||
|
typeof this.buildRuntimeQueueSnapshotKey === 'function'
|
||||||
|
? this.buildRuntimeQueueSnapshotKey
|
||||||
|
: (list: any[]) =>
|
||||||
|
JSON.stringify(
|
||||||
|
(Array.isArray(list) ? list : []).map((item: any) => [
|
||||||
|
String(item?.id || ''),
|
||||||
|
String(item?.text || ''),
|
||||||
|
Number(item?.createdAt ?? item?.created_at ?? 0)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const incomingPreview = (Array.isArray(messages) ? messages : []).map((item: any) => ({
|
||||||
|
id: String(item?.id || '').trim(),
|
||||||
|
text: String(item?.text || '').trim(),
|
||||||
|
createdAt: Number(item?.created_at ?? item?.createdAt ?? 0)
|
||||||
|
}));
|
||||||
|
const incomingKey = buildSnapshotKey(incomingPreview);
|
||||||
|
const now = Date.now();
|
||||||
|
const lockKey = String(this.runtimeQueueSyncLockKey || '');
|
||||||
|
const lockUntil = Number(this.runtimeQueueSyncLockUntil || 0);
|
||||||
|
if (lockKey && now < lockUntil && incomingKey !== lockKey) {
|
||||||
|
debugLog('[RuntimeQueue] 忽略可能过期的轮询同步', {
|
||||||
|
now,
|
||||||
|
lockUntil,
|
||||||
|
incomingSize: incomingPreview.length
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lockKey && incomingKey === lockKey) {
|
||||||
|
this.runtimeQueueSyncLockKey = '';
|
||||||
|
this.runtimeQueueSyncLockUntil = 0;
|
||||||
|
}
|
||||||
|
if (typeof this.applyRuntimeQueuedMessages === 'function') {
|
||||||
|
this.applyRuntimeQueuedMessages(messages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const limit = Math.max(1, Number(this.runtimeQueueLimit || 5));
|
||||||
|
this.runtimeQueuedMessages = messages
|
||||||
|
.map((item: any) => ({
|
||||||
|
id: String(item?.id || '').trim(),
|
||||||
|
text: String(item?.text || '').trim(),
|
||||||
|
createdAt: Number(item?.created_at ?? item?.createdAt ?? Date.now())
|
||||||
|
}))
|
||||||
|
.filter((item: any) => item.id && item.text)
|
||||||
|
.slice(0, limit);
|
||||||
|
},
|
||||||
|
|
||||||
// 新增统一清理方法
|
// 新增统一清理方法
|
||||||
clearTaskState() {
|
clearTaskState() {
|
||||||
this.stopWaitingTaskProbe();
|
this.stopWaitingTaskProbe();
|
||||||
@ -1560,6 +1645,11 @@ export const taskPollingMethods = {
|
|||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
taskStore.stopPolling();
|
taskStore.stopPolling();
|
||||||
})();
|
})();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (typeof this.tryAutoSendRuntimeQueuedMessages === 'function') {
|
||||||
|
this.tryAutoSendRuntimeQueuedMessages('task_error');
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleTokenUpdate(data: any) {
|
handleTokenUpdate(data: any) {
|
||||||
|
|||||||
@ -110,8 +110,7 @@ function connectionDiag(
|
|||||||
...payload
|
...payload
|
||||||
};
|
};
|
||||||
pushConnectionDiagRecord(record);
|
pushConnectionDiagRecord(record);
|
||||||
const logger =
|
const logger = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
|
||||||
level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
|
|
||||||
logger('[CONN_DIAG]', event, record);
|
logger('[CONN_DIAG]', event, record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,7 +369,11 @@ export const uiMethods = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payloadText = JSON.stringify({ content });
|
const payloadText = JSON.stringify({ content });
|
||||||
if (useBeacon && typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
|
if (
|
||||||
|
useBeacon &&
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
typeof navigator.sendBeacon === 'function'
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const blob = new Blob([payloadText], { type: 'application/json' });
|
const blob = new Blob([payloadText], { type: 'application/json' });
|
||||||
navigator.sendBeacon('/api/input-draft', blob);
|
navigator.sendBeacon('/api/input-draft', blob);
|
||||||
@ -1857,6 +1860,21 @@ export const uiMethods = {
|
|||||||
return this.$refs.inputComposer || null;
|
return this.$refs.inputComposer || null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleComposerHeightChange(payload = {}) {
|
||||||
|
const raw =
|
||||||
|
Number(payload?.reservedHeight || 0) ||
|
||||||
|
Number(payload?.height || 0) ||
|
||||||
|
Number(payload?.composerHeight || 0);
|
||||||
|
if (!Number.isFinite(raw) || raw <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = Math.max(80, Math.min(560, Math.round(raw)));
|
||||||
|
if (Math.abs(next - Number(this.composerReservedHeight || 0)) < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.composerReservedHeight = next;
|
||||||
|
},
|
||||||
|
|
||||||
getComposerElement(field) {
|
getComposerElement(field) {
|
||||||
const composer = this.getInputComposerRef();
|
const composer = this.getInputComposerRef();
|
||||||
const unwrap = (value: any) => {
|
const unwrap = (value: any) => {
|
||||||
@ -2291,7 +2309,8 @@ export const uiMethods = {
|
|||||||
if (wasConnected && shouldDisconnect) {
|
if (wasConnected && shouldDisconnect) {
|
||||||
this.connectionHeartbeatLastChangeAt = Date.now();
|
this.connectionHeartbeatLastChangeAt = Date.now();
|
||||||
}
|
}
|
||||||
const shouldLogFail = wasConnected || nextFailCount <= failThreshold || nextFailCount % 10 === 0;
|
const shouldLogFail =
|
||||||
|
wasConnected || nextFailCount <= failThreshold || nextFailCount % 10 === 0;
|
||||||
connectionDiag(
|
connectionDiag(
|
||||||
shouldLogFail ? 'warn' : 'log',
|
shouldLogFail ? 'warn' : 'log',
|
||||||
'health-failed',
|
'health-failed',
|
||||||
@ -2315,7 +2334,8 @@ export const uiMethods = {
|
|||||||
conversationId: this.currentConversationId || null,
|
conversationId: this.currentConversationId || null,
|
||||||
taskInProgress: !!this.taskInProgress,
|
taskInProgress: !!this.taskInProgress,
|
||||||
streamingMessage: !!this.streamingMessage,
|
streamingMessage: !!this.streamingMessage,
|
||||||
hasPendingTools: typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null
|
hasPendingTools:
|
||||||
|
typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null
|
||||||
},
|
},
|
||||||
{ force: shouldLogFail }
|
{ force: shouldLogFail }
|
||||||
);
|
);
|
||||||
@ -2351,7 +2371,8 @@ export const uiMethods = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const connectedInterval =
|
const connectedInterval =
|
||||||
typeof this.connectionHeartbeatIntervalMs === 'number' && this.connectionHeartbeatIntervalMs > 0
|
typeof this.connectionHeartbeatIntervalMs === 'number' &&
|
||||||
|
this.connectionHeartbeatIntervalMs > 0
|
||||||
? this.connectionHeartbeatIntervalMs
|
? this.connectionHeartbeatIntervalMs
|
||||||
: 8000;
|
: 8000;
|
||||||
const disconnectedInterval =
|
const disconnectedInterval =
|
||||||
@ -2360,7 +2381,10 @@ export const uiMethods = {
|
|||||||
? this.connectionHeartbeatDisconnectedIntervalMs
|
? this.connectionHeartbeatDisconnectedIntervalMs
|
||||||
: 1000;
|
: 1000;
|
||||||
const nextInterval = this.isConnected ? connectedInterval : disconnectedInterval;
|
const nextInterval = this.isConnected ? connectedInterval : disconnectedInterval;
|
||||||
if (isConnectionDiagEnabled() && (!this.isConnected || (this.connectionHeartbeatSeq || 0) <= 3)) {
|
if (
|
||||||
|
isConnectionDiagEnabled() &&
|
||||||
|
(!this.isConnected || (this.connectionHeartbeatSeq || 0) <= 3)
|
||||||
|
) {
|
||||||
connectionDiag('log', 'heartbeat-next', {
|
connectionDiag('log', 'heartbeat-next', {
|
||||||
seq: this.connectionHeartbeatSeq,
|
seq: this.connectionHeartbeatSeq,
|
||||||
isConnected: !!this.isConnected,
|
isConnected: !!this.isConnected,
|
||||||
|
|||||||
@ -25,6 +25,17 @@ export function dataState() {
|
|||||||
taskInProgress: false,
|
taskInProgress: false,
|
||||||
// 等待后台通知期间,用于发现并接管新建任务的轮询定时器
|
// 等待后台通知期间,用于发现并接管新建任务的轮询定时器
|
||||||
waitingTaskProbeTimer: null,
|
waitingTaskProbeTimer: null,
|
||||||
|
// 运行期消息堆积(提前发送 / 引导对话)
|
||||||
|
runtimeQueuedMessages: [],
|
||||||
|
runtimeGuidanceFallbackQueue: [],
|
||||||
|
runtimeQueueSuppressedMessageIds: new Set(),
|
||||||
|
runtimeGuidanceSuppressedTextCounts: {},
|
||||||
|
runtimeQueueLimit: 5,
|
||||||
|
runtimeQueueAutoSendInProgress: false,
|
||||||
|
runtimeQueueSyncLockKey: '',
|
||||||
|
runtimeQueueSyncLockUntil: 0,
|
||||||
|
// 输入区动态保留高度(用于同步扩大消息区可滚动范围)
|
||||||
|
composerReservedHeight: 80,
|
||||||
// 记录上一次成功加载历史的对话ID,防止初始化阶段重复加载导致动画播放两次
|
// 记录上一次成功加载历史的对话ID,防止初始化阶段重复加载导致动画播放两次
|
||||||
lastHistoryLoadedConversationId: null,
|
lastHistoryLoadedConversationId: null,
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,37 @@ export const watchers = {
|
|||||||
this.refreshBlankHeroState();
|
this.refreshBlankHeroState();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
composerBusy(newValue, oldValue) {
|
||||||
|
if (oldValue && !newValue && typeof this.tryAutoSendRuntimeQueuedMessages === 'function') {
|
||||||
|
this.tryAutoSendRuntimeQueuedMessages('watch-composer-idle');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runtimeQueuedMessages: {
|
||||||
|
deep: true,
|
||||||
|
handler(list) {
|
||||||
|
if (
|
||||||
|
Array.isArray(list) &&
|
||||||
|
list.length > 0 &&
|
||||||
|
!this.composerBusy &&
|
||||||
|
typeof this.tryAutoSendRuntimeQueuedMessages === 'function'
|
||||||
|
) {
|
||||||
|
this.tryAutoSendRuntimeQueuedMessages('watch-runtime-queue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runtimeGuidanceFallbackQueue: {
|
||||||
|
deep: true,
|
||||||
|
handler(list) {
|
||||||
|
if (
|
||||||
|
Array.isArray(list) &&
|
||||||
|
list.length > 0 &&
|
||||||
|
!this.composerBusy &&
|
||||||
|
typeof this.tryAutoSendRuntimeQueuedMessages === 'function'
|
||||||
|
) {
|
||||||
|
this.tryAutoSendRuntimeQueuedMessages('watch-runtime-guidance-fallback');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
currentConversationTitle(newVal, oldVal) {
|
currentConversationTitle(newVal, oldVal) {
|
||||||
const target = (newVal && newVal.trim()) || '';
|
const target = (newVal && newVal.trim()) || '';
|
||||||
if (this.suppressTitleTyping) {
|
if (this.suppressTitleTyping) {
|
||||||
|
|||||||
@ -1,6 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="input-area compact-input-area">
|
<div class="input-area compact-input-area" ref="inputAreaRoot">
|
||||||
<div class="stadium-input-wrapper" ref="stadiumShellOuter">
|
<div class="stadium-input-wrapper" ref="stadiumShellOuter">
|
||||||
|
<div
|
||||||
|
class="runtime-queue-list"
|
||||||
|
:class="{ 'runtime-queue-list--empty': !runtimeQueuedMessagesForRender.length }"
|
||||||
|
ref="runtimeQueueList"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(entry, index) in runtimeQueuedMessagesForRender"
|
||||||
|
:key="entry.id"
|
||||||
|
class="runtime-queue-item"
|
||||||
|
:class="{
|
||||||
|
'runtime-queue-item--top': index === 0
|
||||||
|
}"
|
||||||
|
:data-runtime-queue-id="entry.id || ''"
|
||||||
|
:ref="bindRuntimeQueueItemRef(entry.id)"
|
||||||
|
>
|
||||||
|
<span class="runtime-queue-item__text">{{ entry.text || '' }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="runtime-queue-item__action"
|
||||||
|
@click.stop="$emit('guide-runtime-message', entry.id)"
|
||||||
|
>
|
||||||
|
引导
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="runtime-queue-item__action runtime-queue-item__action--danger"
|
||||||
|
@click.stop="$emit('delete-runtime-message', entry.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="stadium-shell"
|
class="stadium-shell"
|
||||||
ref="compactInputShell"
|
ref="compactInputShell"
|
||||||
@ -65,7 +97,7 @@
|
|||||||
class="stadium-input"
|
class="stadium-input"
|
||||||
rows="1"
|
rows="1"
|
||||||
:value="inputMessage"
|
:value="inputMessage"
|
||||||
:disabled="!isConnected || streamingMessage || inputLocked"
|
:disabled="!isConnected || inputLocked"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
@ -73,23 +105,15 @@
|
|||||||
@input="onInput"
|
@input="onInput"
|
||||||
@focus="$emit('input-focus')"
|
@focus="$emit('input-focus')"
|
||||||
@blur="$emit('input-blur')"
|
@blur="$emit('input-blur')"
|
||||||
@keydown.enter.ctrl.prevent="$emit('send-message')"
|
@keydown.enter.ctrl.prevent="$emit('send-or-stop')"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="stadium-btn send-btn"
|
class="stadium-btn send-btn"
|
||||||
@click="$emit('send-or-stop')"
|
@click="$emit('send-or-stop')"
|
||||||
:disabled="
|
:disabled="sendButtonDisabled"
|
||||||
!isConnected ||
|
|
||||||
(inputLocked && !streamingMessage) ||
|
|
||||||
(mediaUploading && !streamingMessage) ||
|
|
||||||
(!(inputMessage || '').trim() &&
|
|
||||||
!selectedImages?.length &&
|
|
||||||
!selectedVideos?.length &&
|
|
||||||
!streamingMessage)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<span v-if="streamingMessage" class="stop-icon"></span>
|
<span v-if="showStopIcon" class="stop-icon"></span>
|
||||||
<span v-else class="send-icon"></span>
|
<span v-else class="send-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -200,7 +224,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch, nextTick, computed } from 'vue';
|
import {
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onBeforeUpdate,
|
||||||
|
onUpdated,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
nextTick,
|
||||||
|
computed
|
||||||
|
} from 'vue';
|
||||||
import QuickMenu from '@/components/input/QuickMenu.vue';
|
import QuickMenu from '@/components/input/QuickMenu.vue';
|
||||||
import { useInputStore } from '@/stores/input';
|
import { useInputStore } from '@/stores/input';
|
||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
@ -236,7 +269,10 @@ const emit = defineEmits([
|
|||||||
'open-review',
|
'open-review',
|
||||||
'toggle-permission-menu',
|
'toggle-permission-menu',
|
||||||
'change-permission-mode',
|
'change-permission-mode',
|
||||||
'open-versioning-dialog'
|
'open-versioning-dialog',
|
||||||
|
'guide-runtime-message',
|
||||||
|
'delete-runtime-message',
|
||||||
|
'composer-height-change'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -285,14 +321,18 @@ const props = defineProps<{
|
|||||||
permissionOptions: Array<{ value: string; label: string; description: string }>;
|
permissionOptions: Array<{ value: string; label: string; description: string }>;
|
||||||
currentContextTokens: number;
|
currentContextTokens: number;
|
||||||
versioningEnabled?: boolean;
|
versioningEnabled?: boolean;
|
||||||
|
runtimeQueuedMessages?: Array<{ id: string; text: string }>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inputStore = useInputStore();
|
const inputStore = useInputStore();
|
||||||
const personalizationStore = usePersonalizationStore();
|
const personalizationStore = usePersonalizationStore();
|
||||||
|
const inputAreaRoot = ref<HTMLElement | null>(null);
|
||||||
const stadiumShellOuter = ref<HTMLElement | null>(null);
|
const stadiumShellOuter = ref<HTMLElement | null>(null);
|
||||||
const compactInputShell = ref<HTMLElement | null>(null);
|
const compactInputShell = ref<HTMLElement | null>(null);
|
||||||
const stadiumInput = ref<HTMLTextAreaElement | null>(null);
|
const stadiumInput = ref<HTMLTextAreaElement | null>(null);
|
||||||
const fileUploadInput = ref<HTMLInputElement | null>(null);
|
const fileUploadInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const baselineComposerVisualHeight = ref<number>(0);
|
||||||
|
let composerResizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
const formatImageName = (path: string): string => {
|
const formatImageName = (path: string): string => {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
@ -314,6 +354,322 @@ const composerInputKey = computed(
|
|||||||
() => `${props.currentConversationId || 'new'}:${props.isConnected ? 'online' : 'offline'}`
|
() => `${props.currentConversationId || 'new'}:${props.isConnected ? 'online' : 'offline'}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const runtimeQueuedMessagesForRender = computed(() => {
|
||||||
|
const list = Array.isArray(props.runtimeQueuedMessages) ? props.runtimeQueuedMessages : [];
|
||||||
|
return list.filter((item) => item && item.id && item.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
const RUNTIME_QUEUE_ANIM_DURATION = 300;
|
||||||
|
const RUNTIME_QUEUE_ANIM_EASING = 'cubic-bezier(0.25, 0.8, 0.25, 1)';
|
||||||
|
const runtimeQueueList = ref<HTMLElement | null>(null);
|
||||||
|
const runtimeQueueItemRefs = new Map<string, HTMLElement>();
|
||||||
|
const runtimeQueuePrevIds: string[] = [];
|
||||||
|
const runtimeQueueRenderedIds: string[] = [];
|
||||||
|
const runtimeQueuePrevRects = new Map<string, DOMRect>();
|
||||||
|
const runtimeQueuePrevElements = new Map<string, HTMLElement>();
|
||||||
|
const runtimeQueueGhosts = new Map<string, HTMLElement>();
|
||||||
|
const runtimeQueueActiveAnimations = new WeakMap<HTMLElement, Animation>();
|
||||||
|
const runtimeQueueFallbackTimers = new WeakMap<HTMLElement, number>();
|
||||||
|
let runtimeQueueAnimationReady = false;
|
||||||
|
|
||||||
|
const bindRuntimeQueueItemRef = (id: string) => (el: Element | null) => {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (el instanceof HTMLElement) {
|
||||||
|
runtimeQueueItemRefs.set(id, el);
|
||||||
|
} else {
|
||||||
|
runtimeQueueItemRefs.delete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRuntimeQueueAnimation = (el: HTMLElement) => {
|
||||||
|
const anim = runtimeQueueActiveAnimations.get(el);
|
||||||
|
if (anim) {
|
||||||
|
try {
|
||||||
|
anim.cancel();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
runtimeQueueActiveAnimations.delete(el);
|
||||||
|
}
|
||||||
|
const timer = runtimeQueueFallbackTimers.get(el);
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
runtimeQueueFallbackTimers.delete(el);
|
||||||
|
}
|
||||||
|
el.style.removeProperty('transition');
|
||||||
|
el.style.removeProperty('transform');
|
||||||
|
};
|
||||||
|
|
||||||
|
const playRuntimeQueueTransform = (
|
||||||
|
el: HTMLElement,
|
||||||
|
fromY: number,
|
||||||
|
toY: number,
|
||||||
|
done?: () => void
|
||||||
|
) => {
|
||||||
|
if (Math.abs(fromY - toY) < 0.5) {
|
||||||
|
stopRuntimeQueueAnimation(el);
|
||||||
|
el.style.removeProperty('transition');
|
||||||
|
el.style.removeProperty('transform');
|
||||||
|
done?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopRuntimeQueueAnimation(el);
|
||||||
|
const from = `translateY(${fromY}px)`;
|
||||||
|
const to = `translateY(${toY}px)`;
|
||||||
|
const finish = () => {
|
||||||
|
el.style.removeProperty('transition');
|
||||||
|
el.style.removeProperty('transform');
|
||||||
|
done?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
el.style.transition = 'none';
|
||||||
|
el.style.transform = from;
|
||||||
|
|
||||||
|
if (typeof el.animate === 'function') {
|
||||||
|
// 强制应用起始位移,避免首帧先闪现到终点
|
||||||
|
void el.offsetHeight;
|
||||||
|
const animation = el.animate([{ transform: from }, { transform: to }], {
|
||||||
|
duration: RUNTIME_QUEUE_ANIM_DURATION,
|
||||||
|
easing: RUNTIME_QUEUE_ANIM_EASING,
|
||||||
|
fill: 'both'
|
||||||
|
});
|
||||||
|
runtimeQueueActiveAnimations.set(el, animation);
|
||||||
|
animation.addEventListener(
|
||||||
|
'finish',
|
||||||
|
() => {
|
||||||
|
if (runtimeQueueActiveAnimations.get(el) === animation) {
|
||||||
|
runtimeQueueActiveAnimations.delete(el);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
animation.addEventListener(
|
||||||
|
'cancel',
|
||||||
|
() => {
|
||||||
|
if (runtimeQueueActiveAnimations.get(el) === animation) {
|
||||||
|
runtimeQueueActiveAnimations.delete(el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
el.style.transition = `transform ${RUNTIME_QUEUE_ANIM_DURATION}ms ${RUNTIME_QUEUE_ANIM_EASING}`;
|
||||||
|
el.style.transform = to;
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
runtimeQueueFallbackTimers.delete(el);
|
||||||
|
finish();
|
||||||
|
}, RUNTIME_QUEUE_ANIM_DURATION + 24);
|
||||||
|
runtimeQueueFallbackTimers.set(el, timer);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveRuntimeQueueOffset = (el: HTMLElement): number => {
|
||||||
|
const rectHeight = Math.round(el.getBoundingClientRect().height);
|
||||||
|
if (rectHeight > 0) {
|
||||||
|
return rectHeight;
|
||||||
|
}
|
||||||
|
if (el.offsetHeight > 0) {
|
||||||
|
return el.offsetHeight;
|
||||||
|
}
|
||||||
|
return 34;
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshotRuntimeQueueIds = (): string[] => {
|
||||||
|
return runtimeQueuedMessagesForRender.value
|
||||||
|
.map((entry) => String(entry?.id || '').trim())
|
||||||
|
.filter((id) => !!id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const spawnRuntimeQueueLeaveGhost = (id: string) => {
|
||||||
|
const sourceEl = runtimeQueuePrevElements.get(id);
|
||||||
|
const sourceRect = runtimeQueuePrevRects.get(id);
|
||||||
|
const listEl = runtimeQueueList.value;
|
||||||
|
if (!sourceEl || !sourceRect || !listEl || !listEl.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingGhost = runtimeQueueGhosts.get(id);
|
||||||
|
if (existingGhost) {
|
||||||
|
stopRuntimeQueueAnimation(existingGhost);
|
||||||
|
existingGhost.remove();
|
||||||
|
runtimeQueueGhosts.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghost = sourceEl.cloneNode(true) as HTMLElement;
|
||||||
|
const listRect = listEl.getBoundingClientRect();
|
||||||
|
const top = sourceRect.top - listRect.top;
|
||||||
|
const width = sourceRect.width > 0 ? sourceRect.width : listRect.width;
|
||||||
|
|
||||||
|
ghost.style.position = 'absolute';
|
||||||
|
ghost.style.left = '0';
|
||||||
|
ghost.style.top = `${Math.round(top)}px`;
|
||||||
|
ghost.style.width = `${Math.round(width)}px`;
|
||||||
|
ghost.style.pointerEvents = 'none';
|
||||||
|
ghost.style.zIndex = '0';
|
||||||
|
ghost.style.margin = '0';
|
||||||
|
ghost.querySelectorAll('button').forEach((button) => {
|
||||||
|
if (button instanceof HTMLButtonElement) {
|
||||||
|
button.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
listEl.appendChild(ghost);
|
||||||
|
runtimeQueueGhosts.set(id, ghost);
|
||||||
|
|
||||||
|
const offset = resolveRuntimeQueueOffset(ghost);
|
||||||
|
playRuntimeQueueTransform(ghost, 0, offset, () => {
|
||||||
|
runtimeQueueGhosts.delete(id);
|
||||||
|
ghost.remove();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateRuntimeQueueEnter = (el: HTMLElement) => {
|
||||||
|
const offset = resolveRuntimeQueueOffset(el);
|
||||||
|
el.style.pointerEvents = 'none';
|
||||||
|
playRuntimeQueueTransform(el, offset, 0, () => {
|
||||||
|
el.style.removeProperty('pointer-events');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onBeforeUpdate(() => {
|
||||||
|
runtimeQueuePrevIds.splice(0, runtimeQueuePrevIds.length, ...runtimeQueueRenderedIds);
|
||||||
|
runtimeQueuePrevRects.clear();
|
||||||
|
runtimeQueuePrevElements.clear();
|
||||||
|
|
||||||
|
runtimeQueueRenderedIds.forEach((id) => {
|
||||||
|
const el = runtimeQueueItemRefs.get(id);
|
||||||
|
if (!el?.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runtimeQueuePrevRects.set(id, el.getBoundingClientRect());
|
||||||
|
runtimeQueuePrevElements.set(id, el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
const currentIds = snapshotRuntimeQueueIds();
|
||||||
|
if (!runtimeQueueAnimationReady) {
|
||||||
|
runtimeQueueRenderedIds.splice(0, runtimeQueueRenderedIds.length, ...currentIds);
|
||||||
|
runtimeQueueAnimationReady = true;
|
||||||
|
runtimeQueuePrevIds.splice(0, runtimeQueuePrevIds.length);
|
||||||
|
runtimeQueuePrevRects.clear();
|
||||||
|
runtimeQueuePrevElements.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousIdSet = new Set(runtimeQueuePrevIds);
|
||||||
|
const currentIdSet = new Set(currentIds);
|
||||||
|
const enteringIdSet = new Set(currentIds.filter((id) => !previousIdSet.has(id)));
|
||||||
|
const leavingIds = runtimeQueuePrevIds.filter((id) => !currentIdSet.has(id));
|
||||||
|
|
||||||
|
leavingIds.forEach((id) => {
|
||||||
|
spawnRuntimeQueueLeaveGhost(id);
|
||||||
|
runtimeQueueItemRefs.delete(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
currentIds.forEach((id) => {
|
||||||
|
const el = runtimeQueueItemRefs.get(id);
|
||||||
|
if (!el?.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prevRect = runtimeQueuePrevRects.get(id);
|
||||||
|
if (!prevRect || enteringIdSet.has(id)) {
|
||||||
|
animateRuntimeQueueEnter(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextRect = el.getBoundingClientRect();
|
||||||
|
const deltaY = prevRect.top - nextRect.top;
|
||||||
|
if (Math.abs(deltaY) < 0.5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playRuntimeQueueTransform(el, deltaY, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
runtimeQueueRenderedIds.splice(0, runtimeQueueRenderedIds.length, ...currentIds);
|
||||||
|
runtimeQueuePrevIds.splice(0, runtimeQueuePrevIds.length);
|
||||||
|
runtimeQueuePrevRects.clear();
|
||||||
|
runtimeQueuePrevElements.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasComposerContent = computed(() => {
|
||||||
|
const hasText = !!(props.inputMessage || '').trim();
|
||||||
|
if (props.streamingMessage) {
|
||||||
|
return hasText;
|
||||||
|
}
|
||||||
|
const hasImages = Array.isArray(props.selectedImages) && props.selectedImages.length > 0;
|
||||||
|
const hasVideos = Array.isArray(props.selectedVideos) && props.selectedVideos.length > 0;
|
||||||
|
return hasText || hasImages || hasVideos;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showStopIcon = computed(() => props.streamingMessage && !hasComposerContent.value);
|
||||||
|
|
||||||
|
const sendButtonDisabled = computed(() => {
|
||||||
|
if (!props.isConnected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.streamingMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (props.inputLocked) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.mediaUploading) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !hasComposerContent.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasRuntimeLayoutExpansion = computed(() => {
|
||||||
|
const hasQueue = runtimeQueuedMessagesForRender.value.length > 0;
|
||||||
|
const hasImages = Array.isArray(props.selectedImages) && props.selectedImages.length > 0;
|
||||||
|
const hasVideos = Array.isArray(props.selectedVideos) && props.selectedVideos.length > 0;
|
||||||
|
return hasQueue || hasImages || hasVideos || !!props.inputIsMultiline;
|
||||||
|
});
|
||||||
|
|
||||||
|
const collectComposerVisualHeight = () => {
|
||||||
|
const root = inputAreaRoot.value;
|
||||||
|
const shell = compactInputShell.value;
|
||||||
|
if (!root || !shell) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const shellRect = shell.getBoundingClientRect();
|
||||||
|
let top = shellRect.top;
|
||||||
|
let bottom = shellRect.bottom;
|
||||||
|
const nodes = root.querySelectorAll('.runtime-queue-list:not(.runtime-queue-list--empty)');
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (!(node instanceof HTMLElement)) return;
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
top = Math.min(top, rect.top);
|
||||||
|
bottom = Math.max(bottom, rect.bottom);
|
||||||
|
});
|
||||||
|
return Math.max(0, bottom - top);
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitComposerHeight = () => {
|
||||||
|
const visualHeight = collectComposerVisualHeight();
|
||||||
|
if (!visualHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (baselineComposerVisualHeight.value <= 0 || !hasRuntimeLayoutExpansion.value) {
|
||||||
|
baselineComposerVisualHeight.value = visualHeight;
|
||||||
|
}
|
||||||
|
const baseline = baselineComposerVisualHeight.value || visualHeight;
|
||||||
|
const growth = Math.max(0, visualHeight - baseline);
|
||||||
|
const baseReservedHeight = 80;
|
||||||
|
const reservedHeight = Math.ceil(baseReservedHeight + growth);
|
||||||
|
emit('composer-height-change', {
|
||||||
|
reservedHeight,
|
||||||
|
visualHeight,
|
||||||
|
growth
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const adjustTextareaSize = () => {
|
const adjustTextareaSize = () => {
|
||||||
const textarea = stadiumInput.value;
|
const textarea = stadiumInput.value;
|
||||||
if (!textarea) {
|
if (!textarea) {
|
||||||
@ -328,11 +684,14 @@ const adjustTextareaSize = () => {
|
|||||||
const multiline = targetHeight > lineHeight * 1.4;
|
const multiline = targetHeight > lineHeight * 1.4;
|
||||||
applyLineMetrics(lines, multiline);
|
applyLineMetrics(lines, multiline);
|
||||||
textarea.style.height = `${targetHeight}px`;
|
textarea.style.height = `${targetHeight}px`;
|
||||||
|
nextTick(() => {
|
||||||
|
emitComposerHeight();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInput = (event: Event) => {
|
const onInput = (event: Event) => {
|
||||||
const target = event.target as HTMLTextAreaElement;
|
const target = event.target as HTMLTextAreaElement;
|
||||||
if (!props.isConnected || props.streamingMessage || props.inputLocked) {
|
if (!props.isConnected || props.inputLocked) {
|
||||||
target.value = props.inputMessage || '';
|
target.value = props.inputMessage || '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -423,6 +782,7 @@ const contextUsageCompactText = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
inputAreaRoot,
|
||||||
stadiumShellOuter,
|
stadiumShellOuter,
|
||||||
compactInputShell,
|
compactInputShell,
|
||||||
stadiumInput,
|
stadiumInput,
|
||||||
@ -448,8 +808,74 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.currentConversationId,
|
||||||
|
async () => {
|
||||||
|
baselineComposerVisualHeight.value = 0;
|
||||||
|
await nextTick();
|
||||||
|
emitComposerHeight();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.runtimeQueuedMessages,
|
||||||
|
async () => {
|
||||||
|
await nextTick();
|
||||||
|
emitComposerHeight();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.selectedImages?.length || 0, props.selectedVideos?.length || 0],
|
||||||
|
async () => {
|
||||||
|
await nextTick();
|
||||||
|
emitComposerHeight();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
adjustTextareaSize();
|
adjustTextareaSize();
|
||||||
|
nextTick(() => {
|
||||||
|
emitComposerHeight();
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
composerResizeObserver = new ResizeObserver(() => {
|
||||||
|
emitComposerHeight();
|
||||||
|
});
|
||||||
|
if (inputAreaRoot.value) {
|
||||||
|
composerResizeObserver.observe(inputAreaRoot.value);
|
||||||
|
}
|
||||||
|
if (stadiumShellOuter.value) {
|
||||||
|
composerResizeObserver.observe(stadiumShellOuter.value);
|
||||||
|
}
|
||||||
|
if (compactInputShell.value) {
|
||||||
|
composerResizeObserver.observe(compactInputShell.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
runtimeQueueItemRefs.forEach((el) => {
|
||||||
|
if (el) {
|
||||||
|
stopRuntimeQueueAnimation(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
runtimeQueueGhosts.forEach((ghost) => {
|
||||||
|
stopRuntimeQueueAnimation(ghost);
|
||||||
|
ghost.remove();
|
||||||
|
});
|
||||||
|
runtimeQueueGhosts.clear();
|
||||||
|
runtimeQueueItemRefs.clear();
|
||||||
|
runtimeQueuePrevIds.splice(0, runtimeQueuePrevIds.length);
|
||||||
|
runtimeQueueRenderedIds.splice(0, runtimeQueueRenderedIds.length);
|
||||||
|
runtimeQueuePrevRects.clear();
|
||||||
|
runtimeQueuePrevElements.clear();
|
||||||
|
runtimeQueueAnimationReady = false;
|
||||||
|
if (composerResizeObserver) {
|
||||||
|
composerResizeObserver.disconnect();
|
||||||
|
composerResizeObserver = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -503,5 +929,4 @@ onMounted(() => {
|
|||||||
.image-remove-btn-hover:hover {
|
.image-remove-btn-hover:hover {
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -49,7 +49,8 @@ export const useTaskStore = defineStore('task', {
|
|||||||
pollingFailThreshold: 8,
|
pollingFailThreshold: 8,
|
||||||
pollingIntervalMs: 250,
|
pollingIntervalMs: 250,
|
||||||
taskCreatedAt: null as number | null,
|
taskCreatedAt: null as number | null,
|
||||||
taskUpdatedAt: null as number | null
|
taskUpdatedAt: null as number | null,
|
||||||
|
runtimeQueueSnapshotKey: ''
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@ -104,6 +105,7 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.taskStatus = result.data.status;
|
this.taskStatus = result.data.status;
|
||||||
this.lastEventIndex = 0;
|
this.lastEventIndex = 0;
|
||||||
this.taskCreatedAt = result.data.created_at;
|
this.taskCreatedAt = result.data.created_at;
|
||||||
|
this.runtimeQueueSnapshotKey = '';
|
||||||
this.pollingError = null;
|
this.pollingError = null;
|
||||||
this.pollingErrorCount = 0;
|
this.pollingErrorCount = 0;
|
||||||
this.pollingWarned = false;
|
this.pollingWarned = false;
|
||||||
@ -189,6 +191,30 @@ export const useTaskStore = defineStore('task', {
|
|||||||
// 更新任务状态
|
// 更新任务状态
|
||||||
this.taskStatus = data.status;
|
this.taskStatus = data.status;
|
||||||
this.taskUpdatedAt = data.updated_at;
|
this.taskUpdatedAt = data.updated_at;
|
||||||
|
const runtimeQueueMessages = Array.isArray(data?.runtime_queued_messages)
|
||||||
|
? data.runtime_queued_messages
|
||||||
|
: [];
|
||||||
|
const runtimeQueueSnapshotKey = JSON.stringify(
|
||||||
|
runtimeQueueMessages.map((item: any) => [
|
||||||
|
String(item?.id || ''),
|
||||||
|
String(item?.text || '')
|
||||||
|
])
|
||||||
|
);
|
||||||
|
if (runtimeQueueSnapshotKey !== this.runtimeQueueSnapshotKey) {
|
||||||
|
this.runtimeQueueSnapshotKey = runtimeQueueSnapshotKey;
|
||||||
|
try {
|
||||||
|
eventHandler({
|
||||||
|
type: 'runtime_queue_sync',
|
||||||
|
data: {
|
||||||
|
task_id: data?.task_id || this.currentTaskId,
|
||||||
|
conversation_id: data?.conversation_id || null,
|
||||||
|
messages: runtimeQueueMessages
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Task] 处理 runtime_queue_sync 失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
let sawTaskCompleteEvent = false;
|
let sawTaskCompleteEvent = false;
|
||||||
let sawTaskStoppedEvent = false;
|
let sawTaskStoppedEvent = false;
|
||||||
|
|
||||||
@ -434,6 +460,7 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.lastEventIndex = 0;
|
this.lastEventIndex = 0;
|
||||||
}
|
}
|
||||||
this.pollingError = null;
|
this.pollingError = null;
|
||||||
|
this.runtimeQueueSnapshotKey = '';
|
||||||
this.startPolling(options.eventHandler);
|
this.startPolling(options.eventHandler);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -453,6 +480,7 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.isPolling = false;
|
this.isPolling = false;
|
||||||
this.pollingInFlight = false;
|
this.pollingInFlight = false;
|
||||||
this.pollingWarned = false;
|
this.pollingWarned = false;
|
||||||
|
this.runtimeQueueSnapshotKey = '';
|
||||||
this.currentTaskId = null; // 清除任务 ID
|
this.currentTaskId = null; // 清除任务 ID
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -515,6 +543,7 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.pollingError = null;
|
this.pollingError = null;
|
||||||
this.pollingErrorCount = 0;
|
this.pollingErrorCount = 0;
|
||||||
this.pollingWarned = false;
|
this.pollingWarned = false;
|
||||||
|
this.runtimeQueueSnapshotKey = '';
|
||||||
|
|
||||||
// 获取任务详情,计算已处理的事件数量
|
// 获取任务详情,计算已处理的事件数量
|
||||||
try {
|
try {
|
||||||
@ -559,6 +588,7 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.pollingWarned = false;
|
this.pollingWarned = false;
|
||||||
this.taskCreatedAt = null;
|
this.taskCreatedAt = null;
|
||||||
this.taskUpdatedAt = null;
|
this.taskUpdatedAt = null;
|
||||||
|
this.runtimeQueueSnapshotKey = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
resetForNewConversation() {
|
resetForNewConversation() {
|
||||||
@ -570,6 +600,7 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.pollingError = null;
|
this.pollingError = null;
|
||||||
this.pollingErrorCount = 0;
|
this.pollingErrorCount = 0;
|
||||||
this.pollingWarned = false;
|
this.pollingWarned = false;
|
||||||
|
this.runtimeQueueSnapshotKey = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
/* 聊天容器整体布局,保证聊天区可见并支持上下滚动 */
|
/* 聊天容器整体布局,保证聊天区可见并支持上下滚动 */
|
||||||
.chat-container {
|
.chat-container {
|
||||||
--chat-surface-color: var(--theme-surface-strong, #ffffff);
|
--chat-surface-color: var(--theme-surface-strong, #ffffff);
|
||||||
--composer-reserved-height: calc(75px + var(--app-bottom-inset, 0px));
|
--composer-base-height: calc(80px + var(--app-bottom-inset, 0px));
|
||||||
|
--composer-reserved-height: var(--composer-base-height);
|
||||||
|
--composer-growth-height: 0px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -75,7 +77,7 @@
|
|||||||
overflow-anchor: none;
|
overflow-anchor: none;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
padding-bottom: calc(20px + var(--app-bottom-inset, 0px));
|
padding-bottom: calc(20px + var(--composer-growth-height, 0px));
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -152,7 +154,8 @@
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.chat-container {
|
.chat-container {
|
||||||
--composer-reserved-height: calc(96px + var(--app-bottom-inset, 0px));
|
--composer-base-height: calc(80px + var(--app-bottom-inset, 0px));
|
||||||
|
--composer-reserved-height: var(--composer-base-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-container--mobile {
|
.chat-container--mobile {
|
||||||
|
|||||||
@ -24,6 +24,115 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.runtime-queue-list {
|
||||||
|
--runtime-queue-bg: var(--theme-surface-soft);
|
||||||
|
--runtime-queue-divider: rgba(15, 23, 42, 0.12);
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: calc(100% - 2px);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: visible;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-list--empty {
|
||||||
|
height: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-item {
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--runtime-queue-z, 1);
|
||||||
|
min-height: 34px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 10px 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--runtime-queue-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-item--top {
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-item__text {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--claude-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-item__action {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 3px 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-item__action:hover {
|
||||||
|
background: var(--theme-chip-bg);
|
||||||
|
color: var(--claude-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-item__action--danger:hover {
|
||||||
|
color: #b91c1c;
|
||||||
|
background: rgba(185, 28, 28, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-item + .runtime-queue-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--runtime-queue-divider);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme='dark'] {
|
||||||
|
.runtime-queue-list {
|
||||||
|
--runtime-queue-bg: #2a2a2a;
|
||||||
|
--runtime-queue-divider: rgba(255, 255, 255, 0.12);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-item {
|
||||||
|
background: var(--runtime-queue-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-queue-list.runtime-queue-list--empty {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.permission-switcher {
|
.permission-switcher {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
@ -222,6 +331,7 @@
|
|||||||
.stadium-shell {
|
.stadium-shell {
|
||||||
--stadium-radius: 24px;
|
--stadium-radius: 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: calc(var(--stadium-radius) * 2.1);
|
min-height: calc(var(--stadium-radius) * 2.1);
|
||||||
padding: 12px 18px;
|
padding: 12px 18px;
|
||||||
@ -545,8 +655,8 @@
|
|||||||
|
|
||||||
.composer-container {
|
.composer-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: var(--composer-reserved-height, calc(108px + var(--app-bottom-inset, 0px)));
|
height: var(--composer-base-height, calc(80px + var(--app-bottom-inset, 0px)));
|
||||||
flex: 0 0 var(--composer-reserved-height, calc(108px + var(--app-bottom-inset, 0px)));
|
flex: 0 0 var(--composer-base-height, calc(80px + var(--app-bottom-inset, 0px)));
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user