feat(chv): 架构与代码质量 9.5+ 提升——长函数清零、WarmPool facade、#215 对齐、审计计数、只读 VM 字段、ADR-8~11
This commit is contained in:
1 parent
c908a0147a
commit
42de20da68
15 files changed
+840
-345
No files matched your search
+93
-72
@@ -9,12 +9,100 @@ import os
|
||||
import socket
|
||||
|
||||
|
||||
def _read_header(sock):
|
||||
"""读满响应头:返回 (header_text, 已收体部前缀)。
|
||||
|
||||
超时或提前关流即 RuntimeError,绝不把半截头当结束
|
||||
(feat-chv-fix P1-6 的语义,纯移动)。"""
|
||||
buf = b""
|
||||
header_end = None
|
||||
timed_out = False
|
||||
while header_end is None:
|
||||
try:
|
||||
data = sock.recv(65536)
|
||||
except socket.timeout:
|
||||
timed_out = True
|
||||
break
|
||||
if not data:
|
||||
break
|
||||
buf += data
|
||||
header_end = buf.find(b"\r\n\r\n")
|
||||
if timed_out or header_end is None:
|
||||
raise RuntimeError("CHV API 响应头不完整(超时或提前关流)")
|
||||
header = buf[:header_end].decode("utf-8", errors="replace")
|
||||
return header, buf[header_end + 4:]
|
||||
|
||||
|
||||
def _parse_content_length(header):
|
||||
"""从响应头取 Content-Length:chunked 或无长度即拒绝。
|
||||
|
||||
chunked 明确拒绝(CHV REST 不应发 chunked);无长度则
|
||||
无法证完整性,同样拒绝。"""
|
||||
content_length = None
|
||||
is_chunked = False
|
||||
for line in header.split("\r\n")[1:]:
|
||||
if ":" not in line:
|
||||
continue
|
||||
name, value = line.split(":", 1)
|
||||
name = name.strip().lower()
|
||||
value = value.strip()
|
||||
if name == "content-length":
|
||||
try:
|
||||
content_length = int(value)
|
||||
except ValueError:
|
||||
raise RuntimeError("CHV API Content-Length 非法")
|
||||
elif name == "transfer-encoding" \
|
||||
and "chunked" in value.lower():
|
||||
is_chunked = True
|
||||
if is_chunked:
|
||||
raise RuntimeError("CHV API 响应为 chunked,拒绝解析")
|
||||
if content_length is None:
|
||||
raise RuntimeError("CHV API 响应无 Content-Length,拒绝解析")
|
||||
return content_length
|
||||
|
||||
|
||||
def _read_body(sock, body, content_length):
|
||||
"""按 Content-Length 读满体部:超时或提前关流即截断错误。"""
|
||||
while len(body) < content_length:
|
||||
try:
|
||||
data = sock.recv(65536)
|
||||
except socket.timeout:
|
||||
raise RuntimeError(
|
||||
"CHV API 响应体截断:已收 %d / 需 %d 字节"
|
||||
% (len(body), content_length))
|
||||
if not data:
|
||||
raise RuntimeError(
|
||||
"CHV API 响应体截断:已收 %d / 需 %d 字节"
|
||||
% (len(body), content_length))
|
||||
body += data
|
||||
return body[:content_length]
|
||||
|
||||
|
||||
def _parse_response(header, body):
|
||||
"""从已读满的头与体部解析 (status, body):能解析 JSON
|
||||
时为 dict,否则为原文本;状态行解析失败时 status 为 0。"""
|
||||
text = header + "\r\n\r\n" + body.decode("utf-8", errors="replace")
|
||||
status = 0
|
||||
if text.startswith("HTTP/"):
|
||||
try:
|
||||
status = int(text.split()[1])
|
||||
except (IndexError, ValueError):
|
||||
status = 0
|
||||
body_text = body.decode("utf-8", errors="replace")
|
||||
try:
|
||||
parsed = json.loads(body_text) if body_text.strip() else {}
|
||||
except ValueError:
|
||||
parsed = body_text
|
||||
return status, parsed
|
||||
|
||||
|
||||
def _unix_socket_api(socket_path, method, path, payload=None):
|
||||
"""Cloud Hypervisor REST API 的默认传输:标准库 raw HTTP
|
||||
over Unix socket(无第三方依赖)。返回 (status, body),
|
||||
body 能解析 JSON 时为 dict,否则为原文本。socket 不存在
|
||||
或连接失败时 raise RuntimeError(调用方据此判 api 超时
|
||||
/失败,不编造 200)。"""
|
||||
/失败,不编造 200)。读满语义见 _read_header /
|
||||
_parse_content_length / _read_body(P1-6,纯移动)。"""
|
||||
if not os.path.exists(socket_path):
|
||||
raise RuntimeError("api socket 不存在:%s" % socket_path)
|
||||
body = b""
|
||||
@@ -32,79 +120,12 @@ def _unix_socket_api(socket_path, method, path, payload=None):
|
||||
try:
|
||||
sock.connect(socket_path)
|
||||
sock.sendall(raw)
|
||||
chunks = []
|
||||
# feat-chv-fix(P1-6):读超时不再当“响应结束”。
|
||||
# 先读满响应头,再按 Content-Length 读满体部;超时、
|
||||
# 提前关流、体部短于 Content-Length 一律 RuntimeError
|
||||
# (调用方判 api_timeout / 传输失败),绝不解析半截
|
||||
# 响应。chunked 与无 Content-Length 响应明确拒绝
|
||||
# (Cloud Hypervisor REST 不应发 chunked;无长度则
|
||||
# 无法证完整性)。
|
||||
buf = b""
|
||||
header_end = None
|
||||
timed_out = False
|
||||
while header_end is None:
|
||||
try:
|
||||
data = sock.recv(65536)
|
||||
except socket.timeout:
|
||||
timed_out = True
|
||||
break
|
||||
if not data:
|
||||
break
|
||||
buf += data
|
||||
header_end = buf.find(b"\r\n\r\n")
|
||||
if timed_out or header_end is None:
|
||||
raise RuntimeError("CHV API 响应头不完整(超时或提前关流)")
|
||||
header = buf[:header_end].decode("utf-8", errors="replace")
|
||||
body = buf[header_end + 4:]
|
||||
content_length = None
|
||||
is_chunked = False
|
||||
for line in header.split("\r\n")[1:]:
|
||||
if ":" not in line:
|
||||
continue
|
||||
name, value = line.split(":", 1)
|
||||
name = name.strip().lower()
|
||||
value = value.strip()
|
||||
if name == "content-length":
|
||||
try:
|
||||
content_length = int(value)
|
||||
except ValueError:
|
||||
raise RuntimeError("CHV API Content-Length 非法")
|
||||
elif name == "transfer-encoding" \
|
||||
and "chunked" in value.lower():
|
||||
is_chunked = True
|
||||
if is_chunked:
|
||||
raise RuntimeError("CHV API 响应为 chunked,拒绝解析")
|
||||
if content_length is None:
|
||||
raise RuntimeError("CHV API 响应无 Content-Length,拒绝解析")
|
||||
while len(body) < content_length:
|
||||
try:
|
||||
data = sock.recv(65536)
|
||||
except socket.timeout:
|
||||
raise RuntimeError(
|
||||
"CHV API 响应体截断:已收 %d / 需 %d 字节"
|
||||
% (len(body), content_length))
|
||||
if not data:
|
||||
raise RuntimeError(
|
||||
"CHV API 响应体截断:已收 %d / 需 %d 字节"
|
||||
% (len(body), content_length))
|
||||
body += data
|
||||
body = body[:content_length]
|
||||
header, body = _read_header(sock)
|
||||
content_length = _parse_content_length(header)
|
||||
body = _read_body(sock, body, content_length)
|
||||
finally:
|
||||
sock.close()
|
||||
text = header + "\r\n\r\n" + body.decode("utf-8", errors="replace")
|
||||
status = 0
|
||||
if text.startswith("HTTP/"):
|
||||
try:
|
||||
status = int(text.split()[1])
|
||||
except (IndexError, ValueError):
|
||||
status = 0
|
||||
body_text = body.decode("utf-8", errors="replace")
|
||||
try:
|
||||
parsed = json.loads(body_text) if body_text.strip() else {}
|
||||
except ValueError:
|
||||
parsed = body_text
|
||||
return status, parsed
|
||||
return _parse_response(header, body)
|
||||
|
||||
|
||||
def normalize_api_result(result) -> tuple:
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""#215(feat-arch-converge)接口对齐层,CHV 本地实现。
|
||||
|
||||
背景:#215 未合并,本基线里没有 cloud/timeouts.py、
|
||||
cloud/gates.py、cloud/quota.py、cloud/sandbox_policy.py。
|
||||
本模块绝不复制 #215 的文件,只把 CHV 真正能映射的
|
||||
接口写成与 #215 相同的签名,#215 合并后替换为 import
|
||||
即是机械的(见 docs/chv-architecture.md ADR-8)。
|
||||
|
||||
映射表(逐个核对过 #215 远程分支的真实函数,不是凭空编的):
|
||||
- gates.resolve_inside(base, rel):目录门,CHV 快照/磁盘
|
||||
路径白名单正好需要它——本模块实现同语义并已在
|
||||
hardening.validate_snapshot_url 里使用,#215 合并后
|
||||
改为 from cloud.gates import resolve_inside 即可。
|
||||
- sandbox_policy.tmpfs_isolated(executor_kind, path):CHV
|
||||
客机 /tmp 落在客机磁盘上,不是宿主 tmpfs,故对
|
||||
cloud-hypervisor 一律返回 (False, ...)——fail-closed,
|
||||
绝不宣称隔离。未知执行器同样 fail-closed。
|
||||
- timeouts.resolve_module_timeout / quota.decide:无映射。
|
||||
前者是审批/队列超时(None=不过期、0=不过期),CHV
|
||||
的 api/ready 超时是秒数且 0 无效,语义不同,硬套会
|
||||
改行为;后者是按月美元配额,CHV 的配额是 VM 数/磁盘
|
||||
GB/快照数,单位不同。两者在本模块里 deliberately
|
||||
不提供,测试钉住“CHV 不 import 它们”。
|
||||
|
||||
本模块只依赖标准库,是叶子模块,不 import cloud.sandbox。
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def resolve_inside(base, rel):
|
||||
"""目录门:base 内的相对路径安全解析,越界回 None。
|
||||
|
||||
空串回 base 本身;绝对路径、.. 逃逸、符号链接逃逸、
|
||||
commonpath 跨盘符 ValueError 一律 None。签名与 #215
|
||||
的 gates.resolve_inside 一致,合并后可直接替换为 import。"""
|
||||
if not rel:
|
||||
return base
|
||||
if os.path.isabs(rel):
|
||||
return None
|
||||
real_base = os.path.realpath(base)
|
||||
target = os.path.realpath(os.path.join(real_base, rel))
|
||||
try:
|
||||
if os.path.commonpath([real_base, target]) != real_base:
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
return target
|
||||
|
||||
|
||||
def tmpfs_isolated(executor_kind, path):
|
||||
"""某执行器下某路径是否走 tmpfs 隔离:(isolated, reason)。
|
||||
|
||||
cloud-hypervisor:客机 /tmp 落在客机磁盘上,不走宿主
|
||||
tmpfs,一律 (False, ...);未知执行器 fail-closed 同样
|
||||
False。签名与 #215 的 sandbox_policy.tmpfs_isolated 一致。"""
|
||||
if executor_kind == "cloud-hypervisor":
|
||||
return False, "CHV 客机 /tmp 落在客机磁盘上,不走宿主 tmpfs"
|
||||
return False, "未知执行器,按不隔离处理(fail-closed 口径)"
|
||||
+40
-35
@@ -170,45 +170,21 @@ def _timeout_fragment(params: dict, env: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def build_chv_config(params: dict, env: dict):
|
||||
"""把显式构造参数(params,None=未给)与环境变量(env,
|
||||
默认为 os.environ 的调用方传入 dict)解析成 ChvConfig。
|
||||
def _assemble_chv_config(base: dict, ssh: dict, quota: dict,
|
||||
timeouts: dict):
|
||||
"""把基础字段 + SSH/配额/超时三个片段组装成 ChvConfig。
|
||||
|
||||
显式参数只有 __init__ 本来就有的那些;max_vms、磁盘模式、
|
||||
磁盘配额、egress、快照、预热池目标数本来就没有显式参数,
|
||||
仍只走环境变量(与 #216 一致,不悄悄加新参数面)。
|
||||
片段化(feat-chv-arch95):SSH 面、配额面、超时面分别
|
||||
由 _ssh_fragment/_quota_fragment/_timeout_fragment 解析,
|
||||
错误文案与校验顺序逐字一致;build_chv_config 只组装。"""
|
||||
env = env if env is not None else {}
|
||||
binary = params.get("binary") or env.get("CHV_BINARY") \
|
||||
or "cloud-hypervisor"
|
||||
kernel = params.get("kernel") or env.get("CHV_KERNEL")
|
||||
rootfs = params.get("rootfs") or params.get("image") \
|
||||
or env.get("CHV_ROOTFS") or env.get("CHV_IMAGE")
|
||||
cpus = str(params.get("cpus") or env.get("CHV_CPU") or "2")
|
||||
memory = params.get("memory") or env.get("CHV_MEM") or "2G"
|
||||
state_dir = params.get("state_dir") or env.get("CHV_STATE_DIR") \
|
||||
or os.path.join(os.path.expanduser("~"),
|
||||
".local", "share", "codex-app-server", "chv-vms")
|
||||
kvm_path = params.get("kvm_path") \
|
||||
if params.get("kvm_path") is not None else "/dev/kvm"
|
||||
cpuinfo_path = params.get("cpuinfo_path") \
|
||||
if params.get("cpuinfo_path") is not None else "/proc/cpuinfo"
|
||||
ch_remote = params.get("ch_remote") or env.get("CHV_REMOTE") \
|
||||
or "ch-remote"
|
||||
model = params.get("model")
|
||||
ssh = _ssh_fragment(params, env)
|
||||
quota = _quota_fragment(env)
|
||||
timeouts = _timeout_fragment(params, env)
|
||||
纯移动:字段名与取值逐字不变,build_chv_config 只解析。"""
|
||||
return ChvConfig(
|
||||
binary=binary, kernel=kernel, rootfs=rootfs, cpus=cpus,
|
||||
memory=memory, state_dir=state_dir, ssh_key=ssh["ssh_key"],
|
||||
ssh_user=ssh["ssh_user"], ssh_port=ssh["ssh_port"],
|
||||
binary=base["binary"], kernel=base["kernel"],
|
||||
rootfs=base["rootfs"], cpus=base["cpus"],
|
||||
memory=base["memory"], state_dir=base["state_dir"],
|
||||
ssh_key=ssh["ssh_key"], ssh_user=ssh["ssh_user"],
|
||||
ssh_port=ssh["ssh_port"],
|
||||
guest_host_py=ssh["guest_host_py"], cmdline=ssh["cmdline"],
|
||||
net=ssh["net"], net_json=ssh["net_json"],
|
||||
kvm_path=kvm_path, cpuinfo_path=cpuinfo_path,
|
||||
ch_remote=ch_remote, model=model,
|
||||
kvm_path=base["kvm_path"], cpuinfo_path=base["cpuinfo_path"],
|
||||
ch_remote=base["ch_remote"], model=base["model"],
|
||||
max_vms=quota["max_vms"], disk_mode=quota["disk_mode"],
|
||||
disk_gb=quota["disk_gb"], ssh_host=ssh["ssh_host"],
|
||||
ssh_forward=ssh["ssh_forward"],
|
||||
@@ -228,3 +204,32 @@ def build_chv_config(params: dict, env: dict):
|
||||
snapshot_retention_days=quota["snapshot_retention_days"],
|
||||
snapshot_dir=quota["snapshot_dir"],
|
||||
allow_remote_snapshot=quota["allow_remote_snapshot"])
|
||||
|
||||
|
||||
def build_chv_config(params: dict, env: dict):
|
||||
"""把显式构造参数与环境变量解析成 ChvConfig(见
|
||||
_assemble_chv_config 的组装段;片段化与校验顺序不变)。"""
|
||||
env = env if env is not None else {}
|
||||
base = {
|
||||
"binary": params.get("binary") or env.get("CHV_BINARY")
|
||||
or "cloud-hypervisor",
|
||||
"kernel": params.get("kernel") or env.get("CHV_KERNEL"),
|
||||
"rootfs": params.get("rootfs") or params.get("image")
|
||||
or env.get("CHV_ROOTFS") or env.get("CHV_IMAGE"),
|
||||
"cpus": str(params.get("cpus") or env.get("CHV_CPU") or "2"),
|
||||
"memory": params.get("memory") or env.get("CHV_MEM") or "2G",
|
||||
"state_dir": params.get("state_dir")
|
||||
or env.get("CHV_STATE_DIR")
|
||||
or os.path.join(os.path.expanduser("~"),
|
||||
".local", "share", "codex-app-server", "chv-vms"),
|
||||
"kvm_path": params.get("kvm_path")
|
||||
if params.get("kvm_path") is not None else "/dev/kvm",
|
||||
"cpuinfo_path": params.get("cpuinfo_path")
|
||||
if params.get("cpuinfo_path") is not None else "/proc/cpuinfo",
|
||||
"ch_remote": params.get("ch_remote") or env.get("CHV_REMOTE")
|
||||
or "ch-remote",
|
||||
"model": params.get("model"),
|
||||
}
|
||||
return _assemble_chv_config(
|
||||
base, _ssh_fragment(params, env), _quota_fragment(env),
|
||||
_timeout_fragment(params, env))
|
||||
+215
-195
@@ -289,6 +289,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
else _unix_socket_api
|
||||
self.sleeper = sleeper if sleeper is not None else time.sleep
|
||||
self.audit_callback = None
|
||||
self.audit_failure_count = 0
|
||||
self._vms_lock = threading.Lock()
|
||||
if runner is not None:
|
||||
self.runner = runner
|
||||
@@ -602,9 +603,15 @@ class CloudHypervisorExecutor(Executor):
|
||||
feat-chv-secfix(M5):审计回调失败不再双重静默
|
||||
pass——记宿主 error 日志(审计完整性本身没有保证,
|
||||
但失败至少可见;任务仍不因此炸,审计失败不改变
|
||||
任务状态是 worker 层的既定语义)。审计 detail 里
|
||||
绝不含密钥值(调用方负责:本类所有 _emit 的 detail
|
||||
只含 VM 名、路径、URL、kind,不含任何 key 值)。"""
|
||||
任务状态是 worker 层的既定语义)。feat-chv-arch95plus
|
||||
取舍(见 ADR-9):不改成“审计失败即中断任务”——
|
||||
审计是可观测性不是门禁,DB 短暂不可用时硬中断会把
|
||||
一次审计失败放大成任务失败,且与 worker 全局
|
||||
best-effort 语义冲突;改为“失败记日志 + 计数 + 不
|
||||
中断”,计数见 self.audit_failure_count(宿主可观测,
|
||||
非任务状态)。审计 detail 里绝不含密钥值(调用方
|
||||
负责:本类所有 _emit 的 detail 只含 VM 名、路径、
|
||||
URL、kind,不含任何 key 值)。"""
|
||||
try:
|
||||
if self.audit_callback is not None:
|
||||
self.audit_callback(event, task, detail, kind)
|
||||
@@ -612,6 +619,8 @@ class CloudHypervisorExecutor(Executor):
|
||||
_LOGGER.info("chv_audit event=%s kind=%s %s",
|
||||
event, kind, detail)
|
||||
except Exception as exc:
|
||||
self.audit_failure_count = getattr(
|
||||
self, "audit_failure_count", 0) + 1
|
||||
_LOGGER.error("chv_audit_failed event=%s kind=%s error=%s",
|
||||
event, kind, exc)
|
||||
|
||||
@@ -981,21 +990,37 @@ class CloudHypervisorExecutor(Executor):
|
||||
"vmm.ping 超时:%d 次内 API 未通,socket=%s"
|
||||
% (attempts, socket_path))
|
||||
|
||||
def wait_guest_ready(self, task):
|
||||
"""等 guest SSH 就绪(只选 SSH,见类 docstring):
|
||||
轮询 ssh … "true",returncode 0 即就绪;次数 =
|
||||
ready_timeout/ready_interval 取整,至少 1 次。
|
||||
每轮先尽力捕获 host key(known_hosts 文件不存在时;
|
||||
sshd 未起时抓不到是常态,下轮再试),再发 ssh
|
||||
"true"——钉死式 SSH 在文件缺失时会直接拒绝未知主机,
|
||||
不先捕获就永远等不到就绪。超时时先关 VM 再 raise
|
||||
ChvFailure(kind=guest_not_ready),绝不在未就绪时
|
||||
派任务。"""
|
||||
# feat-chv-fix(P0-2):provisioned 模式下缺 known_hosts
|
||||
# 时直接 fail-fast,绝不调用 capture_host_key 回落 TOFU。
|
||||
# (keyscan 模式才走下面的捕获分支。)
|
||||
def _provisioned_key_guard(self, task):
|
||||
"""provisioned 下缺 known_hosts 即 fail-fast,绝不 keyscan。
|
||||
|
||||
返回 True 表示已拦截(调用方须先关 VM 再抛
|
||||
host_key_not_pinned);False 表示可继续轮询。"""
|
||||
if getattr(self, "host_key_mode", "provisioned") == "provisioned" \
|
||||
and not os.path.exists(self.known_hosts_path(task)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_host_key_mismatch(self, task, result):
|
||||
"""SSH stderr 含钉死拒绝标记时审计并返回 True(须 fail-fast)。
|
||||
|
||||
MITM 尝试是独立安全事件,不轮询成普通超时;审计
|
||||
detail 只含 VM 名与标记,不含 key 值。"""
|
||||
stderr = getattr(result, "stderr", "") or ""
|
||||
if "Host key verification failed" in stderr \
|
||||
or "REMOTE HOST IDENTIFICATION" in stderr:
|
||||
self._emit("host_key_mismatch", task,
|
||||
"vm=%s ssh host key 被拒绝(钉死不匹配)"
|
||||
% self.vm_name(task), "host_key_mismatch")
|
||||
return True
|
||||
return False
|
||||
|
||||
def wait_guest_ready(self, task):
|
||||
"""等 guest SSH 就绪:轮询 ssh "true",returncode 0 即就绪;
|
||||
每轮先尽力捕获 host key(keyscan 模式),超时先关 VM
|
||||
再 raise guest_not_ready,绝不在未就绪时派任务。"""
|
||||
# P0-2:provisioned 下缺 known_hosts 即 fail-fast,绝不
|
||||
# 调用 capture_host_key 回落 TOFU(keyscan 才走捕获分支)。
|
||||
if self._provisioned_key_guard(task):
|
||||
try:
|
||||
self.stop_vm(task)
|
||||
except Exception:
|
||||
@@ -1013,17 +1038,9 @@ class CloudHypervisorExecutor(Executor):
|
||||
self.build_ssh_command(task, ["true"]))
|
||||
if getattr(result, "returncode", 1) == 0:
|
||||
return True
|
||||
# feat-chv-secfix(M5):host key 不匹配是独立
|
||||
# 安全事件,不是“sshd 没起”。SSH stderr 含
|
||||
# 钉死拒绝标记时立即审计 host_key_mismatch 并
|
||||
# fail-fast(不把 MITM 尝试轮询成普通超时),
|
||||
# 审计 detail 只含 VM 名与标记,不含 key 值。
|
||||
stderr = getattr(result, "stderr", "") or ""
|
||||
if "Host key verification failed" in stderr \
|
||||
or "REMOTE HOST IDENTIFICATION" in stderr:
|
||||
self._emit("host_key_mismatch", task,
|
||||
"vm=%s ssh host key 被拒绝(钉死不匹配)"
|
||||
% self.vm_name(task), "host_key_mismatch")
|
||||
# M5:host key 不匹配是独立安全事件,见
|
||||
# _check_host_key_mismatch(审计 + fail-fast)。
|
||||
if self._check_host_key_mismatch(task, result):
|
||||
try:
|
||||
self.stop_vm(task)
|
||||
except Exception:
|
||||
@@ -1394,58 +1411,60 @@ class CloudHypervisorExecutor(Executor):
|
||||
% (self.vm_name(task_or_name), snapshot_url))
|
||||
return snapshot_url
|
||||
|
||||
def restore_vm(self, task, snapshot_url):
|
||||
"""快照恢复唤醒:preflight → 只起 VMM(--api-socket)
|
||||
→ 等 vmm.ping → ch-remote restore → 显式 resume →
|
||||
等 guest SSH 就绪。恢复后的 VM 默认是 paused,不
|
||||
resume 就不算醒——本方法绝不省略 resume 那一步。
|
||||
任一步失败都关 VM 再抛。成功返回 {vm, api_socket,
|
||||
state: "running", ready: True, mode: "restored"}。"""
|
||||
def _validate_restore(self, task, snapshot_url):
|
||||
"""restore 前置校验:返回归一化后的 snapshot_url。
|
||||
|
||||
restore 不走 vm.create、没有 VmConfig 可挂 seed:
|
||||
快照内已固化原 VM host key,重预置反而破坏钉死,
|
||||
故 provisioned 下复用已钉死的 known_hosts、缺失
|
||||
fail-fast、绝不 keyscan、绝不重生成。restore 时不
|
||||
重复验快照文件存在(远端/对象存储源合法,ch-remote
|
||||
本身会在源缺失时失败)。"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
if not snapshot_url:
|
||||
raise ValueError("snapshot_url 不能为空")
|
||||
if "://" not in snapshot_url:
|
||||
snapshot_url = "file://%s" % snapshot_url
|
||||
# feat-chv-secfix(L2):恢复源同样过 URL 白名单
|
||||
# (file:// 须在白名单目录内,远端须显式放行),
|
||||
# 在 preflight/起 VMM 之前就拒绝。
|
||||
validate_snapshot_url(
|
||||
snapshot_url, self.state_dir,
|
||||
snapshot_dir=getattr(self, "snapshot_dir", None),
|
||||
allow_remote=getattr(self, "allow_remote_snapshot", False))
|
||||
self.preflight()
|
||||
self.resolve_ssh_host()
|
||||
# feat-chv-fix(P0-2 修正版):restore 不走 vm.create,
|
||||
# 没有 VmConfig 可挂 seed——审计报告“restore 须把 seed
|
||||
# 接入 VmConfig”的说法在此不适用,纠正为:快照内已
|
||||
# 固化原 VM 的 host key,重新预置新 key 反而会破坏
|
||||
# 钉死。正确不变量是:provisioned 下复用已钉死的
|
||||
# known_hosts(stop 时公钥保留、私钥已删但快照内仍有
|
||||
# 原私钥),缺失时 fail-fast、绝不 keyscan、绝不重
|
||||
# 生成。file:// 快照须先验存在且非空,否则不启动。
|
||||
# 注意:restore 时不重复验快照文件存在——快照落盘
|
||||
# 校验是 snapshot_vm/hibernate_vm 创建时的门(P1-7),
|
||||
# restore 的 ch-remote 本身会在源缺失时失败;在这里
|
||||
# 再验一次会把“源在远端/对象存储”的合法路径也挡掉。
|
||||
if getattr(self, "host_key_mode", "provisioned") == "provisioned" \
|
||||
and not os.path.exists(self.known_hosts_path(task)):
|
||||
if self._provisioned_key_guard(task):
|
||||
raise ChvFailure(
|
||||
"host_key_not_pinned",
|
||||
"provisioned 模式下 restore 缺 known_hosts,拒绝 "
|
||||
"keyscan 回落:VM %s" % self.vm_name(task))
|
||||
return snapshot_url
|
||||
|
||||
def _restore_boot(self, task, snapshot_url):
|
||||
"""restore 的启动段:起 VMM → 等 ping → restore →
|
||||
显式 resume → 等 SSH 就绪(恢复后默认 paused,不
|
||||
resume 就不算醒)。"""
|
||||
name = self.vm_name(task)
|
||||
self._launch_api_only(task)
|
||||
self.wait_api_ready(task)
|
||||
self._check_api_socket_permissions(self.api_socket(task))
|
||||
self._ch_remote(task, "restore",
|
||||
"source_url=%s" % snapshot_url)
|
||||
self._emit("vm_restore", task,
|
||||
"vm=%s snapshot=%s" % (name, snapshot_url))
|
||||
self.resume_vm(task)
|
||||
self.wait_guest_ready(task)
|
||||
|
||||
def restore_vm(self, task, snapshot_url):
|
||||
"""快照恢复唤醒:校验 → 只起 VMM → restore → 显式
|
||||
resume → 等 SSH 就绪。恢复后默认 paused,不 resume
|
||||
就不算醒。任一步失败都关 VM 再抛;成功返回
|
||||
{vm, api_socket, state: "running", ready: True,
|
||||
mode: "restored"}。"""
|
||||
snapshot_url = self._validate_restore(task, snapshot_url)
|
||||
name = self.vm_name(task)
|
||||
self._check_capacity(exclude=name)
|
||||
try:
|
||||
self._launch_api_only(task)
|
||||
self.wait_api_ready(task)
|
||||
self._check_api_socket_permissions(self.api_socket(task))
|
||||
self._ch_remote(task, "restore",
|
||||
"source_url=%s" % snapshot_url)
|
||||
self._emit("vm_restore", task,
|
||||
"vm=%s snapshot=%s" % (name, snapshot_url))
|
||||
self.resume_vm(task)
|
||||
self.wait_guest_ready(task)
|
||||
self._restore_boot(task, snapshot_url)
|
||||
except Exception:
|
||||
try:
|
||||
self.stop_vm(task)
|
||||
@@ -1476,58 +1495,26 @@ class CloudHypervisorExecutor(Executor):
|
||||
说明来源是休眠快照。"""
|
||||
return self.restore_vm(task, snapshot_url)
|
||||
|
||||
def stop_vm(self, task_or_name):
|
||||
"""停止 VM(尽力而为,不抛异常;三种情况):
|
||||
- 已死进程:登记进程 poll() 非 None,跳过 terminate
|
||||
等待,直接删 socket、清登记,返回 True。
|
||||
- 已关 VM:无登记进程、无 socket,ch-remote 仍尽力
|
||||
调过一次,返回 False(本来就没有什么可清)。
|
||||
- socket 残留:有 socket、无登记进程,删 socket 并
|
||||
返回 True。
|
||||
ch-remote shutdown 失败不炸(VM 可能已死),但
|
||||
ch-remote 命令本身仍会发出——测试据此断言关闭路径
|
||||
确实用了 ch-remote shutdown,而不是只杀进程。"""
|
||||
name = self.vm_name(task_or_name)
|
||||
socket_path = os.path.join(self.state_dir, name + ".sock")
|
||||
def _stop_registered_proc(self, name, proc):
|
||||
"""清登记进程:已死直接清,活着先杀再清(纯移动)。"""
|
||||
if proc is None:
|
||||
return
|
||||
with self._vms_lock:
|
||||
had_proc = name in self._vms
|
||||
proc = self._vms.get(name)
|
||||
had = had_proc or os.path.exists(socket_path)
|
||||
self._vms.pop(name, None)
|
||||
try:
|
||||
self.runner([self.ch_remote,
|
||||
"--api-socket=%s" % socket_path, "shutdown"])
|
||||
alive = proc.poll() is None
|
||||
except Exception:
|
||||
pass
|
||||
if proc is not None:
|
||||
with self._vms_lock:
|
||||
self._vms.pop(name, None)
|
||||
try:
|
||||
alive = proc.poll() is None
|
||||
except Exception:
|
||||
alive = False
|
||||
if alive:
|
||||
self._kill_proc(proc)
|
||||
try:
|
||||
if os.path.exists(socket_path):
|
||||
os.remove(socket_path)
|
||||
except Exception:
|
||||
pass
|
||||
# 补缺口轨:per-VM seed 目录与 host 私钥随 VM
|
||||
# 关闭而删,绝不留给下一个 VM 复用;known_hosts
|
||||
# 公钥文件保留供事后审计,删失败不炸关闭。下次
|
||||
# 同名 VM 预置时若私钥已删即重新生成并覆盖
|
||||
# known_hosts,不会用旧钉死去校验新 VM。
|
||||
# feat-chv-secfix(H3/M2):清理分三块独立记账——
|
||||
# seed/私钥块、任务磁盘块、net 登记块,任一块失败
|
||||
# 都记对应审计 + 写脏标记(清理失败即拒起),绝不
|
||||
# 再以“一块失败、整段吞掉”收场;任务磁盘块是 H3
|
||||
# 的主修复:stop 即删 disk.raw(保留期 0;需取证
|
||||
# 须在 shutdown 前经 SSH 拷出,见 recycle_vm 文档)。
|
||||
# net.json 是 live 预约(非审计 artifact),随 VM
|
||||
# 关闭而删,tap/mac/ip 预约随即释放。
|
||||
_task = task_or_name if isinstance(task_or_name, dict) \
|
||||
else {"id": task_or_name}
|
||||
_cleanup_failed = False
|
||||
alive = False
|
||||
if alive:
|
||||
self._kill_proc(proc)
|
||||
|
||||
def _cleanup_vm_files(self, name, task):
|
||||
"""stop 的三块文件清理:seed/私钥、任务磁盘、net 登记。
|
||||
|
||||
任一块失败记对应审计并返回 True(脏标记由调用方写);
|
||||
seed/私钥与磁盘删除的审计名与文案逐字不变。"""
|
||||
from cloud.chv.hostkeys import dirty_marker_path # noqa: F401
|
||||
failed = False
|
||||
try:
|
||||
seed_dir = os.path.join(self.state_dir, name, "seed")
|
||||
if os.path.isdir(seed_dir):
|
||||
@@ -1539,45 +1526,69 @@ class CloudHypervisorExecutor(Executor):
|
||||
if os.path.exists(_p):
|
||||
os.remove(_p)
|
||||
except Exception as _cleanup_exc:
|
||||
_cleanup_failed = True
|
||||
self._emit("vm_key_cleanup_failed", _task,
|
||||
failed = True
|
||||
self._emit("vm_key_cleanup_failed", task,
|
||||
"vm=%s error=%s" % (name, _cleanup_exc))
|
||||
try:
|
||||
if delete_task_disk(self.state_dir, name):
|
||||
self._emit("vm_disk_deleted", _task,
|
||||
self._emit("vm_disk_deleted", task,
|
||||
"vm=%s disk deleted on stop" % name)
|
||||
except Exception as _disk_exc:
|
||||
_cleanup_failed = True
|
||||
self._emit("vm_disk_delete_failed", _task,
|
||||
failed = True
|
||||
self._emit("vm_disk_delete_failed", task,
|
||||
"vm=%s error=%s" % (name, _disk_exc))
|
||||
try:
|
||||
_net_path = os.path.join(self.state_dir, name, "net.json")
|
||||
if os.path.exists(_net_path):
|
||||
os.remove(_net_path)
|
||||
except Exception:
|
||||
_cleanup_failed = True
|
||||
failed = True
|
||||
self._guest_env_delivered.pop(name, None)
|
||||
if _cleanup_failed:
|
||||
# M2 根治:脏标记。标记本身写失败也不炸关闭,
|
||||
# 但下次 provision_host_key 仍会因残留文件复用
|
||||
# 旧 key——该残留情形由 cleanup_stale_secrets 的
|
||||
# 扫描兜底(见 cleanup_stale_vms),文档明写。
|
||||
try:
|
||||
from cloud.chv.hostkeys import dirty_marker_path
|
||||
_marker = dirty_marker_path(self.state_dir, name)
|
||||
return failed
|
||||
|
||||
def _write_dirty_marker(self, name, failed):
|
||||
"""M2 脏标记:清理失败写标记、成功清标记;写失败不炸关闭。"""
|
||||
from cloud.chv.hostkeys import dirty_marker_path
|
||||
try:
|
||||
_marker = dirty_marker_path(self.state_dir, name)
|
||||
if failed:
|
||||
os.makedirs(os.path.dirname(_marker), exist_ok=True)
|
||||
with open(_marker, "w") as fh:
|
||||
fh.write("cleanup failed\n")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
from cloud.chv.hostkeys import dirty_marker_path
|
||||
_marker = dirty_marker_path(self.state_dir, name)
|
||||
if os.path.exists(_marker):
|
||||
os.remove(_marker)
|
||||
except Exception:
|
||||
pass
|
||||
elif os.path.exists(_marker):
|
||||
os.remove(_marker)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def stop_vm(self, task_or_name):
|
||||
"""停止 VM(尽力而为,不抛异常):已死进程直接清登记,
|
||||
已关 VM 仍尽力调一次 ch-remote(返回 False),socket
|
||||
残留删 socket(返回 True)。ch-remote shutdown 失败
|
||||
不炸,但命令本身仍会发出(测试据此断言关闭路径)。"""
|
||||
name = self.vm_name(task_or_name)
|
||||
socket_path = os.path.join(self.state_dir, name + ".sock")
|
||||
with self._vms_lock:
|
||||
had_proc = name in self._vms
|
||||
proc = self._vms.get(name)
|
||||
had = had_proc or os.path.exists(socket_path)
|
||||
try:
|
||||
self.runner([self.ch_remote,
|
||||
"--api-socket=%s" % socket_path, "shutdown"])
|
||||
except Exception:
|
||||
pass
|
||||
self._stop_registered_proc(name, proc)
|
||||
try:
|
||||
if os.path.exists(socket_path):
|
||||
os.remove(socket_path)
|
||||
except Exception:
|
||||
pass
|
||||
# per-VM seed 目录与 host 私钥随 VM 关闭而删,绝不留给
|
||||
# 下一个 VM 复用;known_hosts 公钥保留供事后审计。
|
||||
# 三块清理独立记账 + 脏标记(H3/M2,见 _cleanup_vm_files)。
|
||||
_task = task_or_name if isinstance(task_or_name, dict) \
|
||||
else {"id": task_or_name}
|
||||
_cleanup_failed = self._cleanup_vm_files(name, _task)
|
||||
self._write_dirty_marker(name, _cleanup_failed)
|
||||
task = task_or_name if isinstance(task_or_name, dict) \
|
||||
else {"id": task_or_name}
|
||||
if had:
|
||||
@@ -1624,12 +1635,49 @@ class CloudHypervisorExecutor(Executor):
|
||||
"vm=%s gc error=%s" % (entry, exc))
|
||||
return {"deleted": deleted, "failed": failed}
|
||||
|
||||
def _is_live_vm(self, entry):
|
||||
"""VM 是否活着:socket 存在或登记进程未死即活。"""
|
||||
if os.path.exists(os.path.join(self.state_dir, entry + ".sock")):
|
||||
return True
|
||||
with self._vms_lock:
|
||||
proc = self._vms.get(entry)
|
||||
if proc is not None:
|
||||
try:
|
||||
return proc.poll() is None
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _clean_one_vm_secrets(self, entry, vm_dir):
|
||||
"""单 VM 残留清理:私钥一律删(live VM 私钥在 seed 盘
|
||||
建好后即删,残留只可能是崩溃遗留);非 live VM 的
|
||||
seed.iso/seed 目录同理删。返回 (删私钥, 删 seed)。"""
|
||||
removed_priv = False
|
||||
priv = os.path.join(vm_dir, "ssh_host_ed25519_key")
|
||||
if os.path.exists(priv):
|
||||
try:
|
||||
os.remove(priv)
|
||||
removed_priv = True
|
||||
except Exception:
|
||||
pass
|
||||
removed_seed = False
|
||||
if not self._is_live_vm(entry):
|
||||
for _suffix in ("seed.iso", "seed"):
|
||||
_p = os.path.join(vm_dir, _suffix)
|
||||
try:
|
||||
if os.path.isdir(_p):
|
||||
shutil.rmtree(_p)
|
||||
removed_seed = True
|
||||
elif os.path.exists(_p):
|
||||
os.remove(_p)
|
||||
removed_seed = True
|
||||
except Exception:
|
||||
pass
|
||||
return removed_priv, removed_seed
|
||||
|
||||
def cleanup_stale_secrets(self):
|
||||
"""崩溃残留扫描(H5):state_dir 下的 host 私钥文件
|
||||
在本轨不变量下只可能是残留——live VM 的私钥在 seed
|
||||
盘建好后即删(见 wake_vm),所以任何残留私钥文件
|
||||
一律删除并审计;非 live VM 的 seed.iso/seed 目录同
|
||||
理。返回已清理的 VM 名列表。worker 启动与
|
||||
"""崩溃残留扫描(H5):私钥一律删、非 live VM 的 seed
|
||||
同理删并审计;返回已清理 VM 名。worker 启动与
|
||||
cleanup_stale_vms 时调它,不依赖 socket 是否存在。"""
|
||||
cleaned = []
|
||||
try:
|
||||
@@ -1642,36 +1690,8 @@ class CloudHypervisorExecutor(Executor):
|
||||
vm_dir = os.path.join(self.state_dir, entry)
|
||||
if not os.path.isdir(vm_dir):
|
||||
continue
|
||||
priv = os.path.join(vm_dir, "ssh_host_ed25519_key")
|
||||
removed_priv = False
|
||||
if os.path.exists(priv):
|
||||
try:
|
||||
os.remove(priv)
|
||||
removed_priv = True
|
||||
except Exception:
|
||||
pass
|
||||
live = os.path.exists(
|
||||
os.path.join(self.state_dir, entry + ".sock"))
|
||||
with self._vms_lock:
|
||||
proc = self._vms.get(entry)
|
||||
if proc is not None:
|
||||
try:
|
||||
live = live or proc.poll() is None
|
||||
except Exception:
|
||||
pass
|
||||
removed_seed = False
|
||||
if not live:
|
||||
for _suffix in ("seed.iso", "seed"):
|
||||
_p = os.path.join(vm_dir, _suffix)
|
||||
try:
|
||||
if os.path.isdir(_p):
|
||||
shutil.rmtree(_p)
|
||||
removed_seed = True
|
||||
elif os.path.exists(_p):
|
||||
os.remove(_p)
|
||||
removed_seed = True
|
||||
except Exception:
|
||||
pass
|
||||
removed_priv, removed_seed = \
|
||||
self._clean_one_vm_secrets(entry, vm_dir)
|
||||
if removed_priv or removed_seed:
|
||||
cleaned.append(entry)
|
||||
self._emit("host_secret_residue_cleaned",
|
||||
@@ -1712,32 +1732,11 @@ class CloudHypervisorExecutor(Executor):
|
||||
"lost": ["guest /tmp", "guest worktree temp files"],
|
||||
}
|
||||
|
||||
def cleanup_stale_vms(self):
|
||||
"""清理残留 VM:只清 socket 残留与已死进程对应的 VM。
|
||||
def _stop_dead_socket_vms(self, entries):
|
||||
"""socket 扫描:登记进程仍活的跳过,其余走 stop_vm。
|
||||
|
||||
feat-chv-secfix(H3/H5):先跑残留盘 GC 与残留
|
||||
密钥扫描(不依赖 socket 是否存在,覆盖 worker 崩溃
|
||||
后 socket 已删、文件仍在的情形),再走原 socket 扫描。
|
||||
扫描 state_dir 下 *.sock:登记进程仍活(poll() 为 None)
|
||||
的跳过;其余一律走 stop_vm。返回已清理的 VM 名列表;
|
||||
state_dir 不存在时返回 [],绝不抛异常。基础设施或
|
||||
其它执行器的 socket 不在 state_dir 里,不会被误删;
|
||||
同目录下非 chv- 前缀的 socket 也跳过(只认本执行器
|
||||
的 chv- 命名)。
|
||||
"""
|
||||
返回已清理 VM 名;非 chv- 前缀 socket 一律跳过。"""
|
||||
cleaned = []
|
||||
try:
|
||||
self.cleanup_stale_secrets()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.gc_stale_disks()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
entries = os.listdir(self.state_dir)
|
||||
except Exception:
|
||||
return []
|
||||
for entry in entries:
|
||||
if not entry.endswith(".sock"):
|
||||
continue
|
||||
@@ -1754,9 +1753,11 @@ class CloudHypervisorExecutor(Executor):
|
||||
continue
|
||||
if self.stop_vm(name):
|
||||
cleaned.append(name)
|
||||
# 登记进程已死但 socket 文件已被删的情况:登记项本身
|
||||
# 也是残留,顺手清掉登记,不计入 cleaned(没有 socket
|
||||
# 可删,不算一次清理)。
|
||||
return cleaned
|
||||
|
||||
def _purge_dead_registry(self):
|
||||
"""登记进程已死但 socket 已被删的登记项顺手清掉
|
||||
(没有 socket 可删,不计入清理数)。"""
|
||||
for name in list(self._vms.keys()):
|
||||
proc = self._vms.get(name)
|
||||
try:
|
||||
@@ -1765,6 +1766,25 @@ class CloudHypervisorExecutor(Executor):
|
||||
dead = True
|
||||
if dead:
|
||||
self._vms.pop(name, None)
|
||||
|
||||
def cleanup_stale_vms(self):
|
||||
"""清理残留 VM:先跑残留盘 GC 与残留密钥扫描,再走
|
||||
socket 扫描与登记清理。只清 socket 残留与已死进程
|
||||
对应的 VM;state_dir 不存在返回 [],绝不抛异常。"""
|
||||
try:
|
||||
self.cleanup_stale_secrets()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.gc_stale_disks()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
entries = os.listdir(self.state_dir)
|
||||
except Exception:
|
||||
return []
|
||||
cleaned = self._stop_dead_socket_vms(entries)
|
||||
self._purge_dead_registry()
|
||||
return cleaned
|
||||
|
||||
# -- 客内执行 ------------------------------------------------------
|
||||
|
||||
+42
-26
@@ -99,33 +99,47 @@ def validate_net_entries(entries: list, existing_entries: list) -> list:
|
||||
vm})的 tap/mac/ip 任一重复即 net_tap_conflict,
|
||||
拒绝启动并由调用方审计。
|
||||
返回归一化后的条目 list。"""
|
||||
seen_taps, seen_macs, seen_ips = set(), set(), set()
|
||||
normalized = []
|
||||
for entry in entries:
|
||||
unknown = set(entry) - _NET_FIELDS
|
||||
if unknown:
|
||||
raise ChvFailure(
|
||||
"net_config_invalid",
|
||||
"CHV_NET_JSON 条目含未知字段:%s(白名单:%s)"
|
||||
% (",".join(sorted(unknown)),
|
||||
",".join(sorted(_NET_FIELDS))))
|
||||
tap = str(entry.get("tap") or "").strip()
|
||||
mac = str(entry.get("mac") or "").strip().lower()
|
||||
ip = str(entry.get("ip") or "").strip()
|
||||
for kind, value, seen in (("tap", tap, seen_taps),
|
||||
("mac", mac, seen_macs),
|
||||
("ip", ip, seen_ips)):
|
||||
normalized = [_normalize_net_entry(e) for e in entries]
|
||||
_check_net_duplicates(normalized)
|
||||
_check_net_conflicts(normalized, existing_entries)
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_net_entry(entry: dict) -> dict:
|
||||
"""单条 net 条目归一化:字段白名单 + tap/mac/ip 归一化。
|
||||
|
||||
空 tap 视为未指定,不参与 tap 冲突比较(见
|
||||
validate_net_entries 的说明,纯移动)。"""
|
||||
unknown = set(entry) - _NET_FIELDS
|
||||
if unknown:
|
||||
raise ChvFailure(
|
||||
"net_config_invalid",
|
||||
"CHV_NET_JSON 条目含未知字段:%s(白名单:%s)"
|
||||
% (",".join(sorted(unknown)),
|
||||
",".join(sorted(_NET_FIELDS))))
|
||||
return {"tap": str(entry.get("tap") or "").strip(),
|
||||
"mac": str(entry.get("mac") or "").strip().lower(),
|
||||
"ip": str(entry.get("ip") or "").strip(),
|
||||
"mask": str(entry.get("mask") or "")}
|
||||
|
||||
|
||||
def _check_net_duplicates(normalized: list) -> None:
|
||||
"""本次条目内 tap/mac/ip 重复即 net_config_invalid。"""
|
||||
seen = {"tap": set(), "mac": set(), "ip": set()}
|
||||
for entry in normalized:
|
||||
for kind in ("tap", "mac", "ip"):
|
||||
value = entry.get(kind)
|
||||
if not value:
|
||||
continue
|
||||
if kind == "tap" and not tap:
|
||||
continue
|
||||
if value in seen:
|
||||
if value in seen[kind]:
|
||||
raise ChvFailure(
|
||||
"net_config_invalid",
|
||||
"CHV_NET_JSON 内 %s 重复:%s" % (kind, value))
|
||||
seen.add(value)
|
||||
normalized.append({"tap": tap, "mac": mac, "ip": ip,
|
||||
"mask": str(entry.get("mask") or "")})
|
||||
seen[kind].add(value)
|
||||
|
||||
|
||||
def _check_net_conflicts(normalized: list, existing_entries: list) -> None:
|
||||
"""与已登记 VM 的 tap/mac/ip 任一重复即 net_tap_conflict。"""
|
||||
for entry in normalized:
|
||||
for other in existing_entries:
|
||||
for kind in ("tap", "mac", "ip"):
|
||||
@@ -136,7 +150,6 @@ def validate_net_entries(entries: list, existing_entries: list) -> list:
|
||||
"net %s %s 已被 VM %s 使用,拒绝共享"
|
||||
" tap/mac/ip:每 VM 必须独立"
|
||||
% (kind, value, other.get("vm")))
|
||||
return normalized
|
||||
|
||||
|
||||
def validate_egress_proxy(egress_proxy: str) -> None:
|
||||
@@ -162,9 +175,12 @@ def validate_egress_proxy(egress_proxy: str) -> None:
|
||||
|
||||
|
||||
def _is_within(directory, path):
|
||||
directory = os.path.realpath(directory)
|
||||
path = os.path.realpath(path)
|
||||
return path == directory or path.startswith(directory + os.sep)
|
||||
"""目录内判定:经 compat215.resolve_inside(#215 目录门
|
||||
同签名),越界回 None 即不在内。绝对路径先转相对路径
|
||||
再过门,.. 逃逸与符号链接逃逸一律挡。"""
|
||||
from cloud.chv.compat215 import resolve_inside
|
||||
rel = os.path.relpath(path, directory)
|
||||
return resolve_inside(directory, rel) is not None
|
||||
|
||||
|
||||
def validate_snapshot_url(snapshot_url: str, state_dir: str, snapshot_dir=None,
|
||||
|
||||
@@ -304,3 +304,13 @@ class WarmPool:
|
||||
"warm_pool_release", task,
|
||||
"vm=%s destroyed=%s" % (pool_vm, destroyed))
|
||||
return destroyed
|
||||
|
||||
|
||||
def reconcile_orphans_for_executor(executor) -> list:
|
||||
"""生产路径唯一的 WarmPool 入口:只做孤儿清理。
|
||||
|
||||
worker 启动/关闭调它,不构造 WarmPool 类、不调
|
||||
prewarm/acquire/release——实验接口与生产路径彻底分离:
|
||||
生产代码 import 本函数即可,acquire/prewarm 只存在于
|
||||
实验用的 WarmPool 类与测试里。"""
|
||||
return WarmPool(executor).reconcile_orphans()
|
||||
@@ -21,6 +21,10 @@ export interface Task {
|
||||
coordinator_part?: string; goal_mode?: number | boolean; verify_command?: string;
|
||||
no_time_limit?: number | boolean;
|
||||
environment_id?: string; attachments?: string[]; model?: string; effort?: string;
|
||||
// feat-chv-arch95plus 只读字段:仅 CHV 任务且 worker 已把
|
||||
// VM 死亡写进 interrupted_reason 时 server 才返回 true,
|
||||
// 其它情形字段不存在(绝不把“未报告死亡”当成“VM 活着”)。
|
||||
chv_vm_dead?: boolean;
|
||||
child_model?: string; child_effort?: string; approval?: string;
|
||||
result_text?: string; error_text?: string;
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ export default function TaskDetailPanel() {
|
||||
<div><dt>turn</dt><dd>{shortId(t.turn_id)}</dd></div>
|
||||
<div><dt>root turn</dt><dd>{shortId(t.root_turn_id)}</dd></div>
|
||||
<div><dt>executor</dt><dd>{t.executor || ""}{t.executor === "cloud-hypervisor" ? "(实验执行器,默认关闭;仅展示,无 VM 控制按钮)" : ""}</dd></div>
|
||||
{t.chv_vm_dead ? <div><dt>VM 状态</dt><dd>VM 中途死亡,任务未完成、断点可续跑(后端只读字段 chv_vm_dead;无该字段时不代表 VM 活着)</dd></div> : null}
|
||||
<div><dt>主机</dt><dd>{t.worktree_host || ""}</dd></div>
|
||||
<div><dt>模型</dt><dd>{t.model || "默认"}</dd></div>
|
||||
<div><dt>强度</dt><dd>{t.effort || "默认"}</dd></div>
|
||||
|
||||
+11
-11
@@ -20,7 +20,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Hh;function Gy(){if(Hh)return pt;Hh=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),h=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),p=Symbol.for("react.profiler"),N=Symbol.for("react.consumer"),C=Symbol.for("react.context"),j=Symbol.for("react.forward_ref"),z=Symbol.for("react.suspense"),R=Symbol.for("react.memo"),q=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),U=Symbol.for("react.view_transition"),F=Symbol.iterator;function X(y){return y===null||typeof y!="object"?null:(y=F&&y[F]||y["@@iterator"],typeof y=="function"?y:null)}var J={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},V=Object.assign,vt={};function St(y,D,ut){this.props=y,this.context=D,this.refs=vt,this.updater=ut||J}St.prototype.isReactComponent={},St.prototype.setState=function(y,D){if(typeof y!="object"&&typeof y!="function"&&y!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,y,D,"setState")},St.prototype.forceUpdate=function(y){this.updater.enqueueForceUpdate(this,y,"forceUpdate")};function jt(){}jt.prototype=St.prototype;function K(y,D,ut){this.props=y,this.context=D,this.refs=vt,this.updater=ut||J}var Nt=K.prototype=new jt;Nt.constructor=K,V(Nt,St.prototype),Nt.isPureReactComponent=!0;var w=Array.isArray;function Q(){}var nt={H:null,A:null,T:null,S:null},Dt=Object.prototype.hasOwnProperty;function qt(y,D,ut){var et=ut.ref;return{$$typeof:i,type:y,key:D,ref:et!==void 0?et:null,props:ut}}function dt(y,D){return qt(y.type,D,y.props)}function kt(y){return typeof y=="object"&&y!==null&&y.$$typeof===i}function H(y){var D={"=":"=0",":":"=2"};return"$"+y.replace(/[=:]/g,function(ut){return D[ut]})}var ot=/\/+/g;function yt(y,D){return typeof y=="object"&&y!==null&&y.key!=null?H(""+y.key):D.toString(36)}function G(y){switch(y.status){case"fulfilled":return y.value;case"rejected":throw y.reason;default:switch(typeof y.status=="string"?y.then(Q,Q):(y.status="pending",y.then(function(D){y.status==="pending"&&(y.status="fulfilled",y.value=D)},function(D){y.status==="pending"&&(y.status="rejected",y.reason=D)})),y.status){case"fulfilled":return y.value;case"rejected":throw y.reason}}throw y}function Y(y,D,ut,et,Ct){var bt=typeof y;(bt==="undefined"||bt==="boolean")&&(y=null);var Tt=!1;if(y===null)Tt=!0;else switch(bt){case"bigint":case"string":case"number":Tt=!0;break;case"object":switch(y.$$typeof){case i:case r:Tt=!0;break;case q:return Tt=y._init,Y(Tt(y._payload),D,ut,et,Ct)}}if(Tt)return Ct=Ct(y),Tt=et===""?"."+yt(y,0):et,w(Ct)?(ut="",Tt!=null&&(ut=Tt.replace(ot,"$&/")+"/"),Y(Ct,D,ut,"",function(re){return re})):Ct!=null&&(kt(Ct)&&(Ct=dt(Ct,ut+(Ct.key==null||y&&y.key===Ct.key?"":(""+Ct.key).replace(ot,"$&/")+"/")+Tt)),D.push(Ct)),1;Tt=0;var lt=et===""?".":et+":";if(w(y))for(var mt=0;mt<y.length;mt++)et=y[mt],bt=lt+yt(et,mt),Tt+=Y(et,D,ut,bt,Ct);else if(mt=X(y),typeof mt=="function")for(y=mt.call(y),mt=0;!(et=y.next()).done;)et=et.value,bt=lt+yt(et,mt++),Tt+=Y(et,D,ut,bt,Ct);else if(bt==="object"){if(typeof y.then=="function")return Y(G(y),D,ut,et,Ct);throw D=String(y),Error("Objects are not valid as a React child (found: "+(D==="[object Object]"?"object with keys {"+Object.keys(y).join(", ")+"}":D)+"). If you meant to render a collection of children, use an array instead.")}return Tt}function tt(y,D,ut){if(y==null)return y;var et=[],Ct=0;return Y(y,et,"","",function(bt){return D.call(ut,bt,Ct++)}),et}function gt(y){if(y._status===-1){var D=y._result,ut=D();ut.then(function(et){(y._status===0||y._status===-1)&&(y._status=1,y._result=et,ut.status===void 0&&(ut.status="fulfilled",ut.value=et))},function(et){(y._status===0||y._status===-1)&&(y._status=2,y._result=et,ut.status===void 0&&(ut.status="rejected",ut.reason=et))}),y._status===-1&&(y._status=0,y._result=ut)}if(y._status===1)return y._result.default;throw y._result}var xt=typeof reportError=="function"?reportError:function(y){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var D=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof y=="object"&&y!==null&&typeof y.message=="string"?String(y.message):String(y),error:y});if(!window.dispatchEvent(D))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",y);return}console.error(y)};function Se(y){var D=nt.T,ut={};ut.types=D!==null?D.types:null,nt.T=ut;try{var et=y(),Ct=nt.S;Ct!==null&&Ct(ut,et),typeof et=="object"&&et!==null&&typeof et.then=="function"&&et.then(Q,xt)}catch(bt){xt(bt)}finally{D!==null&&ut.types!==null&&(D.types=ut.types),nt.T=D}}function L(y){var D=nt.T;if(D!==null){var ut=D.types;ut===null?D.types=[y]:ut.indexOf(y)===-1&&ut.push(y)}else Se(L.bind(null,y))}var se={map:tt,forEach:function(y,D,ut){tt(y,function(){D.apply(this,arguments)},ut)},count:function(y){var D=0;return tt(y,function(){D++}),D},toALine truncated
|
||||
*/var Hh;function Gy(){if(Hh)return pt;Hh=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),h=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),p=Symbol.for("react.profiler"),N=Symbol.for("react.consumer"),C=Symbol.for("react.context"),j=Symbol.for("react.forward_ref"),z=Symbol.for("react.suspense"),R=Symbol.for("react.memo"),q=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),U=Symbol.for("react.view_transition"),F=Symbol.iterator;function X(y){return y===null||typeof y!="object"?null:(y=F&&y[F]||y["@@iterator"],typeof y=="function"?y:null)}var J={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Q=Object.assign,vt={};function St(y,D,ut){this.props=y,this.context=D,this.refs=vt,this.updater=ut||J}St.prototype.isReactComponent={},St.prototype.setState=function(y,D){if(typeof y!="object"&&typeof y!="function"&&y!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,y,D,"setState")},St.prototype.forceUpdate=function(y){this.updater.enqueueForceUpdate(this,y,"forceUpdate")};function jt(){}jt.prototype=St.prototype;function K(y,D,ut){this.props=y,this.context=D,this.refs=vt,this.updater=ut||J}var Nt=K.prototype=new jt;Nt.constructor=K,Q(Nt,St.prototype),Nt.isPureReactComponent=!0;var w=Array.isArray;function V(){}var nt={H:null,A:null,T:null,S:null},Dt=Object.prototype.hasOwnProperty;function qt(y,D,ut){var et=ut.ref;return{$$typeof:i,type:y,key:D,ref:et!==void 0?et:null,props:ut}}function dt(y,D){return qt(y.type,D,y.props)}function kt(y){return typeof y=="object"&&y!==null&&y.$$typeof===i}function H(y){var D={"=":"=0",":":"=2"};return"$"+y.replace(/[=:]/g,function(ut){return D[ut]})}var ot=/\/+/g;function yt(y,D){return typeof y=="object"&&y!==null&&y.key!=null?H(""+y.key):D.toString(36)}function G(y){switch(y.status){case"fulfilled":return y.value;case"rejected":throw y.reason;default:switch(typeof y.status=="string"?y.then(V,V):(y.status="pending",y.then(function(D){y.status==="pending"&&(y.status="fulfilled",y.value=D)},function(D){y.status==="pending"&&(y.status="rejected",y.reason=D)})),y.status){case"fulfilled":return y.value;case"rejected":throw y.reason}}throw y}function Y(y,D,ut,et,Ct){var bt=typeof y;(bt==="undefined"||bt==="boolean")&&(y=null);var Tt=!1;if(y===null)Tt=!0;else switch(bt){case"bigint":case"string":case"number":Tt=!0;break;case"object":switch(y.$$typeof){case i:case r:Tt=!0;break;case q:return Tt=y._init,Y(Tt(y._payload),D,ut,et,Ct)}}if(Tt)return Ct=Ct(y),Tt=et===""?"."+yt(y,0):et,w(Ct)?(ut="",Tt!=null&&(ut=Tt.replace(ot,"$&/")+"/"),Y(Ct,D,ut,"",function(re){return re})):Ct!=null&&(kt(Ct)&&(Ct=dt(Ct,ut+(Ct.key==null||y&&y.key===Ct.key?"":(""+Ct.key).replace(ot,"$&/")+"/")+Tt)),D.push(Ct)),1;Tt=0;var lt=et===""?".":et+":";if(w(y))for(var mt=0;mt<y.length;mt++)et=y[mt],bt=lt+yt(et,mt),Tt+=Y(et,D,ut,bt,Ct);else if(mt=X(y),typeof mt=="function")for(y=mt.call(y),mt=0;!(et=y.next()).done;)et=et.value,bt=lt+yt(et,mt++),Tt+=Y(et,D,ut,bt,Ct);else if(bt==="object"){if(typeof y.then=="function")return Y(G(y),D,ut,et,Ct);throw D=String(y),Error("Objects are not valid as a React child (found: "+(D==="[object Object]"?"object with keys {"+Object.keys(y).join(", ")+"}":D)+"). If you meant to render a collection of children, use an array instead.")}return Tt}function tt(y,D,ut){if(y==null)return y;var et=[],Ct=0;return Y(y,et,"","",function(bt){return D.call(ut,bt,Ct++)}),et}function gt(y){if(y._status===-1){var D=y._result,ut=D();ut.then(function(et){(y._status===0||y._status===-1)&&(y._status=1,y._result=et,ut.status===void 0&&(ut.status="fulfilled",ut.value=et))},function(et){(y._status===0||y._status===-1)&&(y._status=2,y._result=et,ut.status===void 0&&(ut.status="rejected",ut.reason=et))}),y._status===-1&&(y._status=0,y._result=ut)}if(y._status===1)return y._result.default;throw y._result}var xt=typeof reportError=="function"?reportError:function(y){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var D=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof y=="object"&&y!==null&&typeof y.message=="string"?String(y.message):String(y),error:y});if(!window.dispatchEvent(D))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",y);return}console.error(y)};function Se(y){var D=nt.T,ut={};ut.types=D!==null?D.types:null,nt.T=ut;try{var et=y(),Ct=nt.S;Ct!==null&&Ct(ut,et),typeof et=="object"&&et!==null&&typeof et.then=="function"&&et.then(V,xt)}catch(bt){xt(bt)}finally{D!==null&&ut.types!==null&&(D.types=ut.types),nt.T=D}}function L(y){var D=nt.T;if(D!==null){var ut=D.types;ut===null?D.types=[y]:ut.indexOf(y)===-1&&ut.push(y)}else Se(L.bind(null,y))}var se={map:tt,forEach:function(y,D,ut){tt(y,function(){D.apply(this,arguments)},ut)},count:function(y){var D=0;return tt(y,function(){D++}),D},toALine truncated
|
||||
* @license React
|
||||
* scheduler.production.js
|
||||
*
|
||||
@@ -28,7 +28,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var qh;function Vy(){return qh||(qh=1,(function(i){function r(G,Y){var tt=G.length;G.push(Y);t:for(;0<tt;){var gt=tt-1>>>1,xt=G[gt];if(0<p(xt,Y))G[gt]=Y,G[tt]=xt,tt=gt;else break t}}function h(G){return G.length===0?null:G[0]}function f(G){if(G.length===0)return null;var Y=G[0],tt=G.pop();if(tt!==Y){G[0]=tt;t:for(var gt=0,xt=G.length,Se=xt>>>1;gt<Se;){var L=2*(gt+1)-1,se=G[L],y=L+1,D=G[y];if(0>p(se,tt))y<xt&&0>p(D,se)?(G[gt]=D,G[y]=tt,gt=y):(G[gt]=se,G[L]=tt,gt=L);else if(y<xt&&0>p(D,tt))G[gt]=D,G[y]=tt,gt=y;else break t}}return Y}function p(G,Y){var tt=G.sortIndex-Y.sortIndex;return tt!==0?tt:G.id-Y.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var N=performance;i.unstable_now=function(){return N.now()}}else{var C=Date,j=C.now();i.unstable_now=function(){return C.now()-j}}var z=[],R=[],q=1,g=null,U=3,F=!1,X=!1,J=!1,V=!1,vt=typeof setTimeout=="function"?setTimeout:null,St=typeof clearTimeout=="function"?clearTimeout:null,jt=typeof setImmediate<"u"?setImmediate:null;function K(G){for(var Y=h(R);Y!==null;){if(Y.callback===null)f(R);else if(Y.startTime<=G)f(R),Y.sortIndex=Y.expirationTime,r(z,Y);else break;Y=h(R)}}function Nt(G){if(J=!1,K(G),!X)if(h(z)!==null)X=!0,w||(w=!0,kt());else{var Y=h(R);Y!==null&&yt(Nt,Y.startTime-G)}}var w=!1,Q=-1,nt=5,Dt=-1;function qt(){return V?!0:!(i.unstable_now()-Dt<nt)}function dt(){if(V=!1,w){var G=i.unstable_now();Dt=G;var Y=!0;try{t:{X=!1,J&&(J=!1,St(Q),Q=-1),F=!0;var tt=U;try{e:{for(K(G),g=h(z);g!==null&&!(g.expirationTime>G&&qt());){var gt=g.callback;if(typeof gt=="function"){g.callback=null,U=g.priorityLevel;var xt=gt(g.expirationTime<=G);if(G=i.unstable_now(),typeof xt=="function"){g.callback=xt,K(G),Y=!0;break e}g===h(z)&&f(z),K(G)}else f(z);g=h(z)}if(g!==null)Y=!0;else{var Se=h(R);Se!==null&&yt(Nt,Se.startTime-G),Y=!1}}break t}finally{g=null,U=tt,F=!1}Y=void 0}}finally{Y?kt():w=!1}}}var kt;if(typeof jt=="function")kt=function(){jt(dt)};else if(typeof MessageChannel<"u"){var H=new MessageChannel,ot=H.port2;H.port1.onmessage=dt,kt=function(){ot.postMessage(null)}}else kt=function(){vt(dt,0)};function yt(G,Y){Q=vt(function(){G(i.unstable_now())},Y)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(G){G.callback=null},i.unstable_forceFrameRate=function(G){0>G||125<G?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):nt=0<G?Math.floor(1e3/G):5},i.unstable_getCurrentPriorityLevel=function(){return U},i.unstable_next=function(G){switch(U){case 1:case 2:case 3:var Y=3;break;default:Y=U}var tt=U;U=Y;try{return G()}finally{U=tt}},i.unstable_requestPaint=function(){V=!0},i.unstable_runWithPriority=function(G,Y){switch(G){case 1:case 2:case 3:case 4:case 5:break;default:G=3}var tt=U;U=G;try{return Y()}finally{U=tt}},i.unstable_scheduleCallback=function(G,Y,tt){var gt=i.unstable_now();switch(typeof tt=="object"&&tt!==null?(tt=tt.delay,tt=typeof tt=="number"&&0<tt?gt+tt:gt):tt=gt,G){case 1:var xt=-1;break;case 2:xt=250;break;case 5:xt=1073741823;break;case 4:xt=1e4;break;default:xt=5e3}return xt=tt+xt,G={id:q++,callback:Y,priorityLevel:G,startTime:tt,expirationTime:xt,sortIndex:-1},tt>gt?(G.sortIndex=tt,r(R,G),h(z)===null&&G===h(R)&&(J?(St(Q),Q=-1):J=!0,yt(Nt,tt-gt))):(G.sortIndex=xt,r(z,G),X||F||(X=!0,w||(w=!0,kt()))),G},i.unstable_shouldYield=qt,i.unstable_wrapCallback=function(G){var Y=U;return function(){var tt=U;U=Y;try{return G.apply(this,arguments)}finally{U=tt}}}})(oo)),oo}var Yh;function Xy(){return Yh||(Yh=1,ro.exports=Vy()),ro.exports}var fo={exports:{}},Ue={};/**
|
||||
*/var qh;function Qy(){return qh||(qh=1,(function(i){function r(G,Y){var tt=G.length;G.push(Y);t:for(;0<tt;){var gt=tt-1>>>1,xt=G[gt];if(0<p(xt,Y))G[gt]=Y,G[tt]=xt,tt=gt;else break t}}function h(G){return G.length===0?null:G[0]}function f(G){if(G.length===0)return null;var Y=G[0],tt=G.pop();if(tt!==Y){G[0]=tt;t:for(var gt=0,xt=G.length,Se=xt>>>1;gt<Se;){var L=2*(gt+1)-1,se=G[L],y=L+1,D=G[y];if(0>p(se,tt))y<xt&&0>p(D,se)?(G[gt]=D,G[y]=tt,gt=y):(G[gt]=se,G[L]=tt,gt=L);else if(y<xt&&0>p(D,tt))G[gt]=D,G[y]=tt,gt=y;else break t}}return Y}function p(G,Y){var tt=G.sortIndex-Y.sortIndex;return tt!==0?tt:G.id-Y.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var N=performance;i.unstable_now=function(){return N.now()}}else{var C=Date,j=C.now();i.unstable_now=function(){return C.now()-j}}var z=[],R=[],q=1,g=null,U=3,F=!1,X=!1,J=!1,Q=!1,vt=typeof setTimeout=="function"?setTimeout:null,St=typeof clearTimeout=="function"?clearTimeout:null,jt=typeof setImmediate<"u"?setImmediate:null;function K(G){for(var Y=h(R);Y!==null;){if(Y.callback===null)f(R);else if(Y.startTime<=G)f(R),Y.sortIndex=Y.expirationTime,r(z,Y);else break;Y=h(R)}}function Nt(G){if(J=!1,K(G),!X)if(h(z)!==null)X=!0,w||(w=!0,kt());else{var Y=h(R);Y!==null&&yt(Nt,Y.startTime-G)}}var w=!1,V=-1,nt=5,Dt=-1;function qt(){return Q?!0:!(i.unstable_now()-Dt<nt)}function dt(){if(Q=!1,w){var G=i.unstable_now();Dt=G;var Y=!0;try{t:{X=!1,J&&(J=!1,St(V),V=-1),F=!0;var tt=U;try{e:{for(K(G),g=h(z);g!==null&&!(g.expirationTime>G&&qt());){var gt=g.callback;if(typeof gt=="function"){g.callback=null,U=g.priorityLevel;var xt=gt(g.expirationTime<=G);if(G=i.unstable_now(),typeof xt=="function"){g.callback=xt,K(G),Y=!0;break e}g===h(z)&&f(z),K(G)}else f(z);g=h(z)}if(g!==null)Y=!0;else{var Se=h(R);Se!==null&&yt(Nt,Se.startTime-G),Y=!1}}break t}finally{g=null,U=tt,F=!1}Y=void 0}}finally{Y?kt():w=!1}}}var kt;if(typeof jt=="function")kt=function(){jt(dt)};else if(typeof MessageChannel<"u"){var H=new MessageChannel,ot=H.port2;H.port1.onmessage=dt,kt=function(){ot.postMessage(null)}}else kt=function(){vt(dt,0)};function yt(G,Y){V=vt(function(){G(i.unstable_now())},Y)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(G){G.callback=null},i.unstable_forceFrameRate=function(G){0>G||125<G?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):nt=0<G?Math.floor(1e3/G):5},i.unstable_getCurrentPriorityLevel=function(){return U},i.unstable_next=function(G){switch(U){case 1:case 2:case 3:var Y=3;break;default:Y=U}var tt=U;U=Y;try{return G()}finally{U=tt}},i.unstable_requestPaint=function(){Q=!0},i.unstable_runWithPriority=function(G,Y){switch(G){case 1:case 2:case 3:case 4:case 5:break;default:G=3}var tt=U;U=G;try{return Y()}finally{U=tt}},i.unstable_scheduleCallback=function(G,Y,tt){var gt=i.unstable_now();switch(typeof tt=="object"&&tt!==null?(tt=tt.delay,tt=typeof tt=="number"&&0<tt?gt+tt:gt):tt=gt,G){case 1:var xt=-1;break;case 2:xt=250;break;case 5:xt=1073741823;break;case 4:xt=1e4;break;default:xt=5e3}return xt=tt+xt,G={id:q++,callback:Y,priorityLevel:G,startTime:tt,expirationTime:xt,sortIndex:-1},tt>gt?(G.sortIndex=tt,r(R,G),h(z)===null&&G===h(R)&&(J?(St(V),V=-1):J=!0,yt(Nt,tt-gt))):(G.sortIndex=xt,r(z,G),X||F||(X=!0,w||(w=!0,kt()))),G},i.unstable_shouldYield=qt,i.unstable_wrapCallback=function(G){var Y=U;return function(){var tt=U;U=Y;try{return G.apply(this,arguments)}finally{U=tt}}}})(oo)),oo}var Yh;function Xy(){return Yh||(Yh=1,ro.exports=Qy()),ro.exports}var fo={exports:{}},Ue={};/**
|
||||
* @license React
|
||||
* react-dom.production.js
|
||||
*
|
||||
@@ -36,7 +36,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Gh;function Ly(){if(Gh)return Ue;Gh=1;var i=po();function r(q){var g="https://react.dev/errors/"+q;if(1<arguments.length){g+="?args[]="+encodeURIComponent(arguments[1]);for(var U=2;U<arguments.length;U++)g+="&args[]="+encodeURIComponent(arguments[U])}return"Minified React error #"+q+"; visit "+g+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function h(){}var f={d:{f:h,r:function(){throw Error(r(522))},D:h,C:h,L:h,m:h,X:h,S:h,M:h},p:0,findDOMNode:null},p=Symbol.for("react.portal"),N=Symbol.for("react.recoverable"),C=Symbol.for("react.optimistic_key");function j(q,g,U){var F=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:p,key:F==null?null:F===C?C:""+F,children:q,containerInfo:g,implementation:U}}var z=i.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function R(q,g){if(q==="font")return"";if(typeof g=="string")return g==="use-credentials"?g:""}return Ue.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=f,Ue.browser=function(q){return{$$typeof:N,_reason:q}},Ue.createPortal=function(q,g){var U=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!g||g.nodeType!==1&&g.nodeType!==9&&g.nodeType!==11)throw Error(r(299));return j(q,g,null,U)},Ue.flushSync=function(q){var g=z.T,U=f.p;try{if(z.T=null,f.p=2,q)return q()}finally{z.T=g,f.p=U,f.d.f()}},Ue.preconnect=function(q,g){typeof q=="string"&&(g?(g=g.crossOrigin,g=typeof g=="string"?g==="use-credentials"?g:"":void 0):g=null,f.d.C(q,g))},Ue.prefetchDNS=function(q){typeof q=="string"&&f.d.D(q)},Ue.preinit=function(q,g){if(typeof q=="string"&&g&&typeof g.as=="string"){var U=g.as,F=R(U,g.crossOrigin),X=typeof g.integrity=="string"?g.integrity:void 0,J=typeof g.fetchPriority=="string"?g.fetchPriority:void 0;U==="style"?f.d.S(q,typeof g.precedence=="string"?g.precedence:void 0,{crossOrigin:F,integrity:X,fetchPriority:J}):U==="script"&&f.d.X(q,{crossOrigin:F,integrity:X,fetchPriority:J,nonce:typeof g.nonce=="string"?g.nonce:void 0})}},Ue.preinitModule=function(q,g){if(typeof q=="string")if(typeof g=="object"&&g!==null){if(g.as==null||g.as==="script"){var U=R(g.as,g.crossOrigin);f.d.M(q,{crossOrigin:U,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0})}}else g==null&&f.d.M(q)},Ue.preload=function(q,g){if(typeof q=="string"&&typeof g=="object"&&g!==null&&typeof g.as=="string"){var U=g.as,F=R(U,g.crossOrigin);f.d.L(q,U,{crossOrigin:F,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,type:typeof g.type=="string"?g.type:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0,referrerPolicy:typeof g.referrerPolicy=="string"?g.referrerPolicy:void 0,imageSrcSet:typeof g.imageSrcSet=="string"?g.imageSrcSet:void 0,imageSizes:typeof g.imageSizes=="string"?g.imageSizes:void 0,media:typeof g.media=="string"?g.media:void 0})}},Ue.preloadModule=function(q,g){if(typeof q=="string")if(g){var U=R(g.as,g.crossOrigin);f.d.m(q,{as:typeof g.as=="string"&&g.as!=="script"?g.as:void 0,crossOrigin:U,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0})}else f.d.m(q)},Ue.requestFormReset=function(q){f.d.r(q)},Ue.unstable_batchedUpdates=function(q,g){return q(g)},Ue.useFormState=function(q,g,U){return z.H.useFormState(q,g,U)},Ue.useFormStatus=function(){return z.H.useHostTransitionStatus()},Ue.version="19.3.1",Ue}var Qh;function Zy(){if(Qh)return fo.exports;Qh=1;function i(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(r){console.error(r)}}return i(),fo.exports=Ly(),fo.exports}/**
|
||||
*/var Gh;function Ly(){if(Gh)return Ue;Gh=1;var i=po();function r(q){var g="https://react.dev/errors/"+q;if(1<arguments.length){g+="?args[]="+encodeURIComponent(arguments[1]);for(var U=2;U<arguments.length;U++)g+="&args[]="+encodeURIComponent(arguments[U])}return"Minified React error #"+q+"; visit "+g+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function h(){}var f={d:{f:h,r:function(){throw Error(r(522))},D:h,C:h,L:h,m:h,X:h,S:h,M:h},p:0,findDOMNode:null},p=Symbol.for("react.portal"),N=Symbol.for("react.recoverable"),C=Symbol.for("react.optimistic_key");function j(q,g,U){var F=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:p,key:F==null?null:F===C?C:""+F,children:q,containerInfo:g,implementation:U}}var z=i.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function R(q,g){if(q==="font")return"";if(typeof g=="string")return g==="use-credentials"?g:""}return Ue.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=f,Ue.browser=function(q){return{$$typeof:N,_reason:q}},Ue.createPortal=function(q,g){var U=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!g||g.nodeType!==1&&g.nodeType!==9&&g.nodeType!==11)throw Error(r(299));return j(q,g,null,U)},Ue.flushSync=function(q){var g=z.T,U=f.p;try{if(z.T=null,f.p=2,q)return q()}finally{z.T=g,f.p=U,f.d.f()}},Ue.preconnect=function(q,g){typeof q=="string"&&(g?(g=g.crossOrigin,g=typeof g=="string"?g==="use-credentials"?g:"":void 0):g=null,f.d.C(q,g))},Ue.prefetchDNS=function(q){typeof q=="string"&&f.d.D(q)},Ue.preinit=function(q,g){if(typeof q=="string"&&g&&typeof g.as=="string"){var U=g.as,F=R(U,g.crossOrigin),X=typeof g.integrity=="string"?g.integrity:void 0,J=typeof g.fetchPriority=="string"?g.fetchPriority:void 0;U==="style"?f.d.S(q,typeof g.precedence=="string"?g.precedence:void 0,{crossOrigin:F,integrity:X,fetchPriority:J}):U==="script"&&f.d.X(q,{crossOrigin:F,integrity:X,fetchPriority:J,nonce:typeof g.nonce=="string"?g.nonce:void 0})}},Ue.preinitModule=function(q,g){if(typeof q=="string")if(typeof g=="object"&&g!==null){if(g.as==null||g.as==="script"){var U=R(g.as,g.crossOrigin);f.d.M(q,{crossOrigin:U,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0})}}else g==null&&f.d.M(q)},Ue.preload=function(q,g){if(typeof q=="string"&&typeof g=="object"&&g!==null&&typeof g.as=="string"){var U=g.as,F=R(U,g.crossOrigin);f.d.L(q,U,{crossOrigin:F,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,type:typeof g.type=="string"?g.type:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0,referrerPolicy:typeof g.referrerPolicy=="string"?g.referrerPolicy:void 0,imageSrcSet:typeof g.imageSrcSet=="string"?g.imageSrcSet:void 0,imageSizes:typeof g.imageSizes=="string"?g.imageSizes:void 0,media:typeof g.media=="string"?g.media:void 0})}},Ue.preloadModule=function(q,g){if(typeof q=="string")if(g){var U=R(g.as,g.crossOrigin);f.d.m(q,{as:typeof g.as=="string"&&g.as!=="script"?g.as:void 0,crossOrigin:U,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0})}else f.d.m(q)},Ue.requestFormReset=function(q){f.d.r(q)},Ue.unstable_batchedUpdates=function(q,g){return q(g)},Ue.useFormState=function(q,g,U){return z.H.useFormState(q,g,U)},Ue.useFormStatus=function(){return z.H.useHostTransitionStatus()},Ue.version="19.3.1",Ue}var Vh;function Zy(){if(Vh)return fo.exports;Vh=1;function i(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(r){console.error(r)}}return i(),fo.exports=Ly(),fo.exports}/**
|
||||
* @license React
|
||||
* react-dom-client.production.js
|
||||
*
|
||||
@@ -44,19 +44,19 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Vh;function Ky(){if(Vh)return ii;Vh=1;var i=Xy(),r=po(),h=Zy();function f(t){var e="https://react.dev/errors/"+t;if(1<arguments.length){e+="?args[]="+encodeURIComponent(arguments[1]);for(var l=2;l<arguments.length;l++)e+="&args[]="+encodeURIComponent(arguments[l])}return"Minified React error #"+t+"; visit "+e+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function p(t){return!(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)}function N(t){for(var e=t,l=e;l&&!l.alternate;)e=l,(e.flags&4098)!==0&&(t=e.return),l=e.return;for(;e.return;)e=e.return;return e.tag===3?t:null}function C(t){if(t.tag===13){var e=t.memoizedState;if(e===null&&(t=t.alternate,t!==null&&(e=t.memoizedState)),e!==null)return e.dehydrated}return null}function j(t){if(t.tag===31){var e=t.memoizedState;if(e===null&&(t=t.alternate,t!==null&&(e=t.memoizedState)),e!==null)return e.dehydrated}return null}function z(t){if(N(t)!==t)throw Error(f(188))}function R(t){var e=t.alternate;if(!e){if(e=N(t),e===null)throw Error(f(188));return e!==t?null:t}for(var l=t,a=e;;){var n=l.return;if(n===null)break;var u=n.alternate;if(u===null){if(a=n.return,a!==null){l=a;continue}break}if(n.child===u.child){for(u=n.child;u;){if(u===l)return z(n),t;if(u===a)return z(n),e;u=u.sibling}throw Error(f(188))}if(l.return!==a.return)l=n,a=u;else{for(var s=!1,o=n.child;o;){if(o===l){s=!0,l=n,a=u;break}if(o===a){s=!0,a=n,l=u;break}o=o.sibling}if(!s){for(o=u.child;o;){if(o===l){s=!0,l=u,a=n;break}if(o===a){s=!0,a=u,l=n;break}o=o.sibling}if(!s)throw Error(f(189))}}if(l.alternate!==a)throw Error(f(190))}if(l.tag!==3)throw Error(f(188));return l.stateNode.current===l?t:e}function q(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t;for(t=t.child;t!==null;){if(e=q(t),e!==null)return e;t=t.sibling}return null}function g(t,e,l,a,n,u){for(;t!==null;){if((t.tag===5||t.tag===27||t.tag===6)&&l(t,a,n,u)||(t.tag!==22||t.memoizedState===null)&&(e||t.tag!==5&&t.tag!==27)&&g(t.child,e,l,a,n,u))return!0;t=t.sibling}return!1}function U(t){for(t=t.return;t!==null;){if(t.tag===3||t.tag===5||t.tag===27)return t;t=t.return}return null}function F(t){var e=!1;for(t=t.return;t!==null&&(t.tag===4&&(e=!0),!(t.tag===3||t.tag===5||t.tag===27));)t=t.return;return e}function X(t){var e=[null,null],l=U(t);return l===null||J(e,t,l.child,{foundSelf:!1}),e}function J(t,e,l,a){for(;l!==null;){if(l===e)a.foundSelf=!0;else if(l.tag===5||l.tag===27||l.tag===6){if(a.foundSelf)return t[1]=l,!0;t[0]=l}else if((l.tag!==22||l.memoizedState===null)&&J(t,e,l.child,a))return!0;l=l.sibling}return!1}function V(t){switch(t.tag){case 5:case 27:case 6:return t.stateNode;case 3:return t.stateNode.containerInfo;default:throw Error(f(559))}}var vt=null,St=null;function jt(t,e,l){return t===l?!0:t===e?(vt=t,!0):!1}function K(t,e,l){return t===l?(St=t,!1):t===e?(St!==null&&(vt=t),!0):!1}function Nt(t){if(t===null)return null;do t=t===null?null:t.return;while(t&&t.tag!==5&&t.tag!==27&&t.tag!==3);return t||null}function w(t,e,l){for(var a=0,n=t;n;n=l(n))a++;n=0;for(var u=e;u;u=l(u))n++;for(;0<a-n;)t=l(t),a--;for(;0<n-a;)e=l(e),n--;for(;a--;){if(t===e||e!==null&&t===e.alternate)return t;t=l(t),e=l(e)}return null}var Q=Object.assign,nt=Symbol.for("react.element"),Dt=Symbol.for("react.transitional.element"),qt=Symbol.for("react.portal"),dt=Symbol.for("react.fragment"),kt=Symbol.for("react.strict_mode"),H=Symbol.for("react.profiler"),ot=Symbol.for("react.consumer"),yt=Symbol.for("react.context"),G=Symbol.for("react.forward_ref"),Y=Symbol.for("react.suspense"),tt=Symbol.for("react.suspense_list"),gt=Symbol.for("react.memo"),xt=Symbol.for("react.lazy"),Se=Symbol.for("react.activity"),L=Symbol.for("react.legacy_hidden"),se=Symbol.for("react.memo_cache_sentinel"),y=Symbol.for("react.view_transition"),D=Symbol.for("react.recoverable"),ut=Symbol.iterator;function et(t){return t===null||typeof t!="object"?null:(t=ut&&t[ut]||t["@@iterator"],typeof t=="function"?t:null)}var Ct=Symbol.for("react.client.reference");function bt(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===Ct?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case dt:return"Fragment";case H:return"Profiler";case kt:return"StrictMode";case Y:return"Suspense";case tt:return"SuspenseList";case Se:return"Activity";case y:return"ViewTransition"}if(typeof t=="object")switch(t.$$typeof){case qt:return"Portal";case yt:return t.displayName||"Context";case ot:return(t._context.displayName||"Context")+".Consumer";case G:var e=t.render;return t=t.displayName,t||(t=e.displayName||e.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case gt:return e=t.displayName||null,e!==null?e:bt(t.type)||"Memo";case xt:e=t._payload,t=t._init;try{return bt(t(e))}catch{}}return null}var Tt=Array.isArray,lt=r.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,mt=h.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,re={pending:!1,data:null,method:null,action:nLine truncated
|
||||
*/var Qh;function Ky(){if(Qh)return ii;Qh=1;var i=Xy(),r=po(),h=Zy();function f(t){var e="https://react.dev/errors/"+t;if(1<arguments.length){e+="?args[]="+encodeURIComponent(arguments[1]);for(var l=2;l<arguments.length;l++)e+="&args[]="+encodeURIComponent(arguments[l])}return"Minified React error #"+t+"; visit "+e+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function p(t){return!(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)}function N(t){for(var e=t,l=e;l&&!l.alternate;)e=l,(e.flags&4098)!==0&&(t=e.return),l=e.return;for(;e.return;)e=e.return;return e.tag===3?t:null}function C(t){if(t.tag===13){var e=t.memoizedState;if(e===null&&(t=t.alternate,t!==null&&(e=t.memoizedState)),e!==null)return e.dehydrated}return null}function j(t){if(t.tag===31){var e=t.memoizedState;if(e===null&&(t=t.alternate,t!==null&&(e=t.memoizedState)),e!==null)return e.dehydrated}return null}function z(t){if(N(t)!==t)throw Error(f(188))}function R(t){var e=t.alternate;if(!e){if(e=N(t),e===null)throw Error(f(188));return e!==t?null:t}for(var l=t,a=e;;){var n=l.return;if(n===null)break;var u=n.alternate;if(u===null){if(a=n.return,a!==null){l=a;continue}break}if(n.child===u.child){for(u=n.child;u;){if(u===l)return z(n),t;if(u===a)return z(n),e;u=u.sibling}throw Error(f(188))}if(l.return!==a.return)l=n,a=u;else{for(var s=!1,o=n.child;o;){if(o===l){s=!0,l=n,a=u;break}if(o===a){s=!0,a=n,l=u;break}o=o.sibling}if(!s){for(o=u.child;o;){if(o===l){s=!0,l=u,a=n;break}if(o===a){s=!0,a=u,l=n;break}o=o.sibling}if(!s)throw Error(f(189))}}if(l.alternate!==a)throw Error(f(190))}if(l.tag!==3)throw Error(f(188));return l.stateNode.current===l?t:e}function q(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t;for(t=t.child;t!==null;){if(e=q(t),e!==null)return e;t=t.sibling}return null}function g(t,e,l,a,n,u){for(;t!==null;){if((t.tag===5||t.tag===27||t.tag===6)&&l(t,a,n,u)||(t.tag!==22||t.memoizedState===null)&&(e||t.tag!==5&&t.tag!==27)&&g(t.child,e,l,a,n,u))return!0;t=t.sibling}return!1}function U(t){for(t=t.return;t!==null;){if(t.tag===3||t.tag===5||t.tag===27)return t;t=t.return}return null}function F(t){var e=!1;for(t=t.return;t!==null&&(t.tag===4&&(e=!0),!(t.tag===3||t.tag===5||t.tag===27));)t=t.return;return e}function X(t){var e=[null,null],l=U(t);return l===null||J(e,t,l.child,{foundSelf:!1}),e}function J(t,e,l,a){for(;l!==null;){if(l===e)a.foundSelf=!0;else if(l.tag===5||l.tag===27||l.tag===6){if(a.foundSelf)return t[1]=l,!0;t[0]=l}else if((l.tag!==22||l.memoizedState===null)&&J(t,e,l.child,a))return!0;l=l.sibling}return!1}function Q(t){switch(t.tag){case 5:case 27:case 6:return t.stateNode;case 3:return t.stateNode.containerInfo;default:throw Error(f(559))}}var vt=null,St=null;function jt(t,e,l){return t===l?!0:t===e?(vt=t,!0):!1}function K(t,e,l){return t===l?(St=t,!1):t===e?(St!==null&&(vt=t),!0):!1}function Nt(t){if(t===null)return null;do t=t===null?null:t.return;while(t&&t.tag!==5&&t.tag!==27&&t.tag!==3);return t||null}function w(t,e,l){for(var a=0,n=t;n;n=l(n))a++;n=0;for(var u=e;u;u=l(u))n++;for(;0<a-n;)t=l(t),a--;for(;0<n-a;)e=l(e),n--;for(;a--;){if(t===e||e!==null&&t===e.alternate)return t;t=l(t),e=l(e)}return null}var V=Object.assign,nt=Symbol.for("react.element"),Dt=Symbol.for("react.transitional.element"),qt=Symbol.for("react.portal"),dt=Symbol.for("react.fragment"),kt=Symbol.for("react.strict_mode"),H=Symbol.for("react.profiler"),ot=Symbol.for("react.consumer"),yt=Symbol.for("react.context"),G=Symbol.for("react.forward_ref"),Y=Symbol.for("react.suspense"),tt=Symbol.for("react.suspense_list"),gt=Symbol.for("react.memo"),xt=Symbol.for("react.lazy"),Se=Symbol.for("react.activity"),L=Symbol.for("react.legacy_hidden"),se=Symbol.for("react.memo_cache_sentinel"),y=Symbol.for("react.view_transition"),D=Symbol.for("react.recoverable"),ut=Symbol.iterator;function et(t){return t===null||typeof t!="object"?null:(t=ut&&t[ut]||t["@@iterator"],typeof t=="function"?t:null)}var Ct=Symbol.for("react.client.reference");function bt(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===Ct?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case dt:return"Fragment";case H:return"Profiler";case kt:return"StrictMode";case Y:return"Suspense";case tt:return"SuspenseList";case Se:return"Activity";case y:return"ViewTransition"}if(typeof t=="object")switch(t.$$typeof){case qt:return"Portal";case yt:return t.displayName||"Context";case ot:return(t._context.displayName||"Context")+".Consumer";case G:var e=t.render;return t=t.displayName,t||(t=e.displayName||e.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case gt:return e=t.displayName||null,e!==null?e:bt(t.type)||"Memo";case xt:e=t._payload,t=t._init;try{return bt(t(e))}catch{}}return null}var Tt=Array.isArray,lt=r.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,mt=h.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,re={pending:!1,data:null,method:null,action:nLine truncated
|
||||
at`)?" (<anonymous>)":-1<l.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+Be+t+oe}var rl=!1;function Fl(t,e){if(!t||rl)return"";rl=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var a={DetermineComponentFrameRoot:function(){try{if(e){var O=function(){throw Error()};if(Object.defineProperty(O.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(O,[])}catch(Z){var S=Z}Reflect.construct(t,[],O)}else{try{O.call()}catch(Z){S=Z}O=!1;try{var _=Object.getOwnPropertyDescriptor(t.prototype,"props");Object.defineProperty(t.prototype,"props",{configurable:!0,set:function(){throw Error()}}),O=!0,new t}finally{O&&(_!==void 0?Object.defineProperty(t.prototype,"props",_):delete t.prototype.props)}}}else{try{throw Error()}catch(Z){S=Z}(O=t())&&typeof O.catch=="function"&&O.catch(function(){})}}catch(Z){if(Z&&S&&typeof Z.stack=="string")return[Z.stack,S.stack]}return[null,null]}};a.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var n=Object.getOwnPropertyDescriptor(a.DetermineComponentFrameRoot,"name");n&&n.configurable&&Object.defineProperty(a.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var u=a.DetermineComponentFrameRoot(),s=u[0],o=u[1];if(s&&o){var m=s.split(`
|
||||
`),b=o.split(`
|
||||
`);for(n=a=0;a<m.length&&!m[a].includes("DetermineComponentFrameRoot");)a++;for(;n<b.length&&!b[n].includes("DetermineComponentFrameRoot");)n++;if(a===m.length||n===b.length)for(a=m.length-1,n=b.length-1;1<=a&&0<=n&&m[a]!==b[n];)n--;for(;1<=a&&0<=n;a--,n--)if(m[a]!==b[n]){if(a!==1||n!==1)do if(a--,n--,0>n||m[a]!==b[n]){var E=`
|
||||
`+m[a].replace(" at new "," at ");return t.displayName&&E.includes("<anonymous>")&&(E=E.replace("<anonymous>",t.displayName)),E}while(1<=a&&0<=n);break}}}finally{rl=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?fe(l):""}function ma(t,e){switch(t.tag){case 26:case 27:case 5:return fe(t.type);case 16:return fe("Lazy");case 13:return t.child!==e&&e!==null?fe("Suspense Fallback"):fe("Suspense");case 19:return fe("SuspenseList");case 0:case 15:return Fl(t.type,!1);case 11:return Fl(t.type.render,!1);case 1:return Fl(t.type,!0);case 31:return fe("Activity");case 30:return fe("ViewTransition");default:return""}}function Dl(t){try{var e="",l=null;do e+=ma(t,l),l=t,t=t.return;while(t);return e}catch(a){return`
|
||||
Error generating stack: `+a.message+`
|
||||
`+a.stack}}var ee=Object.prototype.hasOwnProperty,Pe=i.unstable_scheduleCallback,qe=i.unstable_cancelCallback,_l=i.unstable_shouldYield,ol=i.unstable_requestPaint,$t=i.unstable_now,kl=i.unstable_getCurrentPriorityLevel,mn=i.unstable_ImmediatePriority,Qa=i.unstable_UserBlockingPriority,be=i.unstable_NormalPriority,Va=i.unstable_LowPriority,ha=i.unstable_IdlePriority,su=i.log,ru=i.unstable_setDisableYieldValue,Wl=null,Me=null;function fl(t){if(typeof su=="function"&&ru(t),Me&&typeof Me.setStrictMode=="function")try{Me.setStrictMode(Wl,t)}catch{}}var Oe=Math.clz32?Math.clz32:du,ou=Math.log,fu=Math.LN2;function du(t){return t>>>=0,t===0?32:31-(ou(t)/fu|0)|0}var B=256,k=262144,P=4194304;function ct(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&-t;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function A(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,u=t.suspendedLanes,s=t.pingedLanes;t=t.warmLanes;var o=a&134217727;return o!==0?(a=o&~u,a!==0?n=ct(a):(s&=o,s!==0?n=ct(s):l||(l=o&~t,l!==0&&(n=ct(l))))):(o=a&~u,o!==0?n=ct(o):s!==0?n=ct(s):l||(l=a&~t,l!==0&&(n=ct(l)))),n===0?0:e!==0&&e!==n&&(e&u)===0&&(u=n&-n,l=e&-e,u>=l||u===32&&(l&4194048)!==0)?e:n}function $(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function st(t,e){(e&8)!==0&&(e|=e&32);var l=t.entangledLanes;if(l!==0)for(t=t.entanglements,l&=e;0<l;){var a=31-Oe(l),n=1<<a;e|=t[a],l&=~n}return e}function Yt(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function wt(){var t=P;return P<<=1,(P&62914560)===0&&(P=4194304),t}function je(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function Te(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function wc(t,e,l,a,n,u){var s=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var o=t.entanglements,m=t.expirationTimes,b=t.hiddenUpdates;for(l=s&~l;0<l;){var E=31-Oe(l),O=1<<E;o[E]=0,m[E]=-1;var S=b[E];if(S!==null)for(b[E]=null,E=0;E<S.length;E++){var _=S[E];_!==null&&(_.lane&=-536870913)}l&=~O}a!==0&&si(t,a,0),u!==0&&n===0&&t.tag!==0&&(t.suspendedLanes|=u&~(s&~e))}function si(t,e,l){t.pendingLanes|=e,t.suspendedLanes&=~e;var a=31-Oe(e);t.entangledLanes|=e,t.entanglements[a]=t.entanglements[a]|1073741824|l&261930}function hn(t,e){var l=t.entangledLanes|=e;for(t=t.entanglements;l;){var a=31-Oe(l),n=1<<a;n&e|t[a]&e&&(t[a]|=e),l&=~n}}function $l(t,e){var l=e&-e;return l=(l&42)!==0?1:wl(l),(l&(t.suspendedLanes|e))!==0?0:l}function wl(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function mu(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function hu(){var t=mt.p;return t!==0?t:(t=window.event,t===void 0?32:Ah(t.type))}function Ze(t,e){var l=mt.p;try{return mt.p=t,e()}finally{mt.p=l}}var ne=Math.random().toString(36).slice(2),ge="__reactFiber$"+ne,Ke="__reactProps$"+ne,vn="__reactContainer$"+ne,jo="__reactEvents$"+ne,ev="__reactListeners$"+ne,lv="__reactHandles$"+ne,To="__reactResources$"+ne,vu="__reactMarker$"+ne,ri="__reactLoad$"+ne;function oi(t){delete t[ge],delete t[Ke],delete t[ev],delete t[lv]}function Xa(t){var e;if(e=t[ge])return e;for(var l=t.parentNode;l;){if(e=l[vn]||l[ge]){if(l=e.alternate,e.child!==null||l!==null&&l.child!==null)for(t=sh(t);t!==null;){if(l=t[ge])return l;t=sh(t)}return e}t=l,l=t.parentNode}return null}function yn(t){if(t=t[ge]||t[vn]){var e=t.tag;if(e===5||e===6||e===13||e===31||e===26||e===27||e===3)return t}return null}function yu(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t.stateNode;throw Error(f(33))}function gn(t){var e=t[To];return e||(e=t[To]={hoistableStyles:new Map,hoistableScripts:new Map}),e}function _e(t){t[vu]=!0}function _o(t){t[ri]=void 0}var Eo=new Set,No={};function La(t,e){pn(t,e),pn(t+"Capture",e)}function pn(t,Line truncated
|
||||
`).replace(V0,"")}function Qm(t,e){return e=Gm(e),Gm(t)===e}function Zt(t,e,l,a,n,u){switch(l){case"children":if(typeof a=="string")e==="body"||e==="textarea"&&a===""||xn(t,a);else if(typeof a=="number"||typeof a=="bigint")e!=="body"&&xn(t,""+a);else return;break;case"className":di(t,"class",a);break;case"tabIndex":di(t,"tabindex",a);break;case"dir":case"role":case"viewBox":case"width":case"height":di(t,l,a);break;case"style":Uo(t,a,u);return;case"data":if(e!=="object"){di(t,"data",a);break}case"src":case"href":if(a===""&&(e!=="a"||l!=="href")){t.removeAttribute(l);break}if(a==null||typeof a=="function"||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=mi(a),t.setAttribute(l,a);break;case"action":case"formAction":if(typeof a=="function"){t.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof u=="function"&&(l==="formAction"?(e!=="input"&&Zt(t,e,"name",n.name,n,null),Zt(t,e,"formEncType",n.formEncType,n,null),Zt(t,e,"formMethod",n.formMethod,n,null),Zt(t,e,"formTarget",n.formTarget,n,null)):(Zt(t,e,"encType",n.encType,n,null),Zt(t,e,"method",n.method,n,null),Zt(t,e,"target",n.target,n,null)));if(a==null||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=mi(a),t.setAttribute(l,a);break;case"onClick":a!=null&&(t.onclick=Ul);return;case"onScroll":a!=null&&Ot("scroll",t);return;case"onScrollEnd":a!=null&&Ot("scrollend",t);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(f(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(f(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"multiple":t.multiple=a&&typeof a!="function"&&typeof a!="symbol";break;case"muted":t.muted=a&&typeof a!="function"&&typeof a!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(a==null||typeof a=="function"||typeof a=="boolean"||typeof a=="symbol"){t.removeAttribute("xlink:href");break}l=mi(a),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"credentialless":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":a&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,""):t.removeAttribute(l);break;case"capture":case"download":a===!0?t.setAttribute(l,""):a!==!1&&a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":a!=null&&typeof a!="function"&&typeof a!="symbol"&&!isNaN(a)&&1<=a?t.setAttribute(l,a):t.removeAttribute(l);break;case"rowSpan":case"start":a==null||typeof a=="function"||typeof a=="symbol"||isNaN(a)?t.removeAttribute(l):t.setAttribute(l,a);break;case"popover":Ot("beforetoggle",t),Ot("toggle",t),fi(t,"popover",a);break;case"xlinkActuate":Il(t,"http://www.w3.org/1999/xlink","xlink:actuate",a);break;case"xlinkArcrole":Il(t,"http://www.w3.org/1999/xlink","xlink:arcrole",a);break;case"xlinkRole":Il(t,"http://www.w3.org/1999/xlink","xlink:role",a);break;case"xlinkShow":Il(t,"http://www.w3.org/1999/xlink","xlink:show",a);break;case"xlinkTitle":Il(t,"http://www.w3.org/1999/xlink","xlink:title",a);break;case"xlinkType":Il(t,"http://www.w3.org/1999/xlink","xlink:type",a);break;case"xmlBase":Il(t,"http://www.w3.org/XML/1998/namespace","xml:base",a);break;case"xmlLang":Il(t,"http://www.w3.org/XML/1998/namespace","xml:lang",a);break;case"xmlSpace":Il(t,"http://www.w3.org/XML/1998/namespace","xml:space",a);break;case"is":fi(t,"is",a);break;case"innerText":case"textContent":return;default:if(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")l=sv.get(l)||l,fi(t,l,a);else return}Bt=!0}function Hr(t,e,l,a,n,u){switch(l){case"style":Uo(t,a,u);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(f(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(f(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"children":if(typeof a=="string")xn(t,a);else if(typeof a=="number"||typeof a=="bigint")xn(t,""+a);else return;break;case"onScroll":a!=null&&Ot("scroll",t);return;case"onScrollEnd":a!=null&&Ot("scroLine truncated
|
||||
`+a.stack}}var ee=Object.prototype.hasOwnProperty,Pe=i.unstable_scheduleCallback,qe=i.unstable_cancelCallback,_l=i.unstable_shouldYield,ol=i.unstable_requestPaint,$t=i.unstable_now,kl=i.unstable_getCurrentPriorityLevel,mn=i.unstable_ImmediatePriority,Va=i.unstable_UserBlockingPriority,be=i.unstable_NormalPriority,Qa=i.unstable_LowPriority,ha=i.unstable_IdlePriority,su=i.log,ru=i.unstable_setDisableYieldValue,Wl=null,Me=null;function fl(t){if(typeof su=="function"&&ru(t),Me&&typeof Me.setStrictMode=="function")try{Me.setStrictMode(Wl,t)}catch{}}var Oe=Math.clz32?Math.clz32:du,ou=Math.log,fu=Math.LN2;function du(t){return t>>>=0,t===0?32:31-(ou(t)/fu|0)|0}var B=256,k=262144,P=4194304;function ct(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&-t;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function A(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,u=t.suspendedLanes,s=t.pingedLanes;t=t.warmLanes;var o=a&134217727;return o!==0?(a=o&~u,a!==0?n=ct(a):(s&=o,s!==0?n=ct(s):l||(l=o&~t,l!==0&&(n=ct(l))))):(o=a&~u,o!==0?n=ct(o):s!==0?n=ct(s):l||(l=a&~t,l!==0&&(n=ct(l)))),n===0?0:e!==0&&e!==n&&(e&u)===0&&(u=n&-n,l=e&-e,u>=l||u===32&&(l&4194048)!==0)?e:n}function $(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function st(t,e){(e&8)!==0&&(e|=e&32);var l=t.entangledLanes;if(l!==0)for(t=t.entanglements,l&=e;0<l;){var a=31-Oe(l),n=1<<a;e|=t[a],l&=~n}return e}function Yt(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function wt(){var t=P;return P<<=1,(P&62914560)===0&&(P=4194304),t}function je(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function Te(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function wc(t,e,l,a,n,u){var s=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var o=t.entanglements,m=t.expirationTimes,b=t.hiddenUpdates;for(l=s&~l;0<l;){var E=31-Oe(l),O=1<<E;o[E]=0,m[E]=-1;var S=b[E];if(S!==null)for(b[E]=null,E=0;E<S.length;E++){var _=S[E];_!==null&&(_.lane&=-536870913)}l&=~O}a!==0&&si(t,a,0),u!==0&&n===0&&t.tag!==0&&(t.suspendedLanes|=u&~(s&~e))}function si(t,e,l){t.pendingLanes|=e,t.suspendedLanes&=~e;var a=31-Oe(e);t.entangledLanes|=e,t.entanglements[a]=t.entanglements[a]|1073741824|l&261930}function hn(t,e){var l=t.entangledLanes|=e;for(t=t.entanglements;l;){var a=31-Oe(l),n=1<<a;n&e|t[a]&e&&(t[a]|=e),l&=~n}}function $l(t,e){var l=e&-e;return l=(l&42)!==0?1:wl(l),(l&(t.suspendedLanes|e))!==0?0:l}function wl(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function mu(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function hu(){var t=mt.p;return t!==0?t:(t=window.event,t===void 0?32:Ah(t.type))}function Ze(t,e){var l=mt.p;try{return mt.p=t,e()}finally{mt.p=l}}var ne=Math.random().toString(36).slice(2),ge="__reactFiber$"+ne,Ke="__reactProps$"+ne,vn="__reactContainer$"+ne,jo="__reactEvents$"+ne,ev="__reactListeners$"+ne,lv="__reactHandles$"+ne,To="__reactResources$"+ne,vu="__reactMarker$"+ne,ri="__reactLoad$"+ne;function oi(t){delete t[ge],delete t[Ke],delete t[ev],delete t[lv]}function Xa(t){var e;if(e=t[ge])return e;for(var l=t.parentNode;l;){if(e=l[vn]||l[ge]){if(l=e.alternate,e.child!==null||l!==null&&l.child!==null)for(t=sh(t);t!==null;){if(l=t[ge])return l;t=sh(t)}return e}t=l,l=t.parentNode}return null}function yn(t){if(t=t[ge]||t[vn]){var e=t.tag;if(e===5||e===6||e===13||e===31||e===26||e===27||e===3)return t}return null}function yu(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t.stateNode;throw Error(f(33))}function gn(t){var e=t[To];return e||(e=t[To]={hoistableStyles:new Map,hoistableScripts:new Map}),e}function _e(t){t[vu]=!0}function _o(t){t[ri]=void 0}var Eo=new Set,No={};function La(t,e){pn(t,e),pn(t+"Capture",e)}function pn(t,Line truncated
|
||||
`).replace(Q0,"")}function Vm(t,e){return e=Gm(e),Gm(t)===e}function Zt(t,e,l,a,n,u){switch(l){case"children":if(typeof a=="string")e==="body"||e==="textarea"&&a===""||xn(t,a);else if(typeof a=="number"||typeof a=="bigint")e!=="body"&&xn(t,""+a);else return;break;case"className":di(t,"class",a);break;case"tabIndex":di(t,"tabindex",a);break;case"dir":case"role":case"viewBox":case"width":case"height":di(t,l,a);break;case"style":Uo(t,a,u);return;case"data":if(e!=="object"){di(t,"data",a);break}case"src":case"href":if(a===""&&(e!=="a"||l!=="href")){t.removeAttribute(l);break}if(a==null||typeof a=="function"||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=mi(a),t.setAttribute(l,a);break;case"action":case"formAction":if(typeof a=="function"){t.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof u=="function"&&(l==="formAction"?(e!=="input"&&Zt(t,e,"name",n.name,n,null),Zt(t,e,"formEncType",n.formEncType,n,null),Zt(t,e,"formMethod",n.formMethod,n,null),Zt(t,e,"formTarget",n.formTarget,n,null)):(Zt(t,e,"encType",n.encType,n,null),Zt(t,e,"method",n.method,n,null),Zt(t,e,"target",n.target,n,null)));if(a==null||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=mi(a),t.setAttribute(l,a);break;case"onClick":a!=null&&(t.onclick=Ul);return;case"onScroll":a!=null&&Ot("scroll",t);return;case"onScrollEnd":a!=null&&Ot("scrollend",t);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(f(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(f(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"multiple":t.multiple=a&&typeof a!="function"&&typeof a!="symbol";break;case"muted":t.muted=a&&typeof a!="function"&&typeof a!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(a==null||typeof a=="function"||typeof a=="boolean"||typeof a=="symbol"){t.removeAttribute("xlink:href");break}l=mi(a),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"credentialless":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":a&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,""):t.removeAttribute(l);break;case"capture":case"download":a===!0?t.setAttribute(l,""):a!==!1&&a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":a!=null&&typeof a!="function"&&typeof a!="symbol"&&!isNaN(a)&&1<=a?t.setAttribute(l,a):t.removeAttribute(l);break;case"rowSpan":case"start":a==null||typeof a=="function"||typeof a=="symbol"||isNaN(a)?t.removeAttribute(l):t.setAttribute(l,a);break;case"popover":Ot("beforetoggle",t),Ot("toggle",t),fi(t,"popover",a);break;case"xlinkActuate":Il(t,"http://www.w3.org/1999/xlink","xlink:actuate",a);break;case"xlinkArcrole":Il(t,"http://www.w3.org/1999/xlink","xlink:arcrole",a);break;case"xlinkRole":Il(t,"http://www.w3.org/1999/xlink","xlink:role",a);break;case"xlinkShow":Il(t,"http://www.w3.org/1999/xlink","xlink:show",a);break;case"xlinkTitle":Il(t,"http://www.w3.org/1999/xlink","xlink:title",a);break;case"xlinkType":Il(t,"http://www.w3.org/1999/xlink","xlink:type",a);break;case"xmlBase":Il(t,"http://www.w3.org/XML/1998/namespace","xml:base",a);break;case"xmlLang":Il(t,"http://www.w3.org/XML/1998/namespace","xml:lang",a);break;case"xmlSpace":Il(t,"http://www.w3.org/XML/1998/namespace","xml:space",a);break;case"is":fi(t,"is",a);break;case"innerText":case"textContent":return;default:if(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")l=sv.get(l)||l,fi(t,l,a);else return}Bt=!0}function Hr(t,e,l,a,n,u){switch(l){case"style":Uo(t,a,u);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(f(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(f(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"children":if(typeof a=="string")xn(t,a);else if(typeof a=="number"||typeof a=="bigint")xn(t,""+a);else return;break;case"onScroll":a!=null&&Ot("scroll",t);return;case"onScrollEnd":a!=null&&Ot("scroLine truncated
|
||||
`;Yt&&(je+=Yt+`
|
||||
`),wt&&(je+=(Array.isArray(wt)?wt.join(`
|
||||
`):String(wt))+`
|
||||
`),je+=JSON.stringify(st,null,2).slice(0,4e3),jl(je),re("已返回登录信息,见下方")}catch(st){re("发起失败:"+st.message)}},[]),k=d.useCallback(async()=>{const A=X.trim();if(!A){F("请填要查配额的用户 id");return}try{const $=await at(W.adminQuotaUser(A));F("用户 "+A.slice(0,8)+":本月用量 "+ho($.used_usd_micros)+" / 配额 "+ho($.quota_usd_micros)+($.remaining_usd_micros!==null&&$.remaining_usd_micros!==void 0?"(剩余 "+ho($.remaining_usd_micros)+")":"")+" | 月份 "+($.month||""))}catch($){F(mo($))}},[X]),P=d.useCallback(async()=>{if(window.confirm("确定手动触发 sweep 吗?超阈值的陈旧 running 任务会被标为 interrupted。"))try{const $=(await at(W.adminSweep,"POST",{})).swept||[];F("sweep 完成,清理 "+$.length+" 个任务"+($.length?":"+$.join("、"):""))}catch(A){F(mo(A))}},[]),ct=d.useCallback(async()=>{try{const A=await at(W.adminBackup,"POST",{});F("备份完成:backend="+A.backend+" | 路径 "+A.path+" | 大小 "+A.size_bytes+" 字节 | 任务 "+A.task_count+" 行")}catch(A){F(mo(A))}},[]);return{keySaved:i,keyState:h,quotaText:p,healthText:C,adminGate:z,adminBody:q,adminResult:U,adminUserId:X,setAdminUserId:J,projects:V,projectId:St,setProjectId:jt,projectMsg:K,githubRepos:w,workers:nt,workersSummary:qt,workersError:kt,members:ot,memberMsg:G,memberError:tt,skills:xt,apps:L,mcpServers:y,skillMsg:ut,skillError:Ct,mcpMsg:Tt,mcpOauthMsg:mt,mcpOauthBox:Ga,envs:Xe,envCount:Qt,envMsg:Jl,envRepoLine:da,envRepoBar:zl,envRepoMsg:Tl,envSelect:oe,setEnvSelect:fe,models:rl,modelCustomVisible:ma,refreshHealth:qe,loadQuota:_l,probeAdmin:ol,refreshProjects:$t,loadGithubRepos:kl,loadRelayModels:mn,refreshWorkers:Qa,refreshEnvs:be,refreshMembers:Va,refreshSkills:ha,saveKeyInput:su,clearKeyInput:ru,createProject:Wl,createEnv:Me,archiveEnv:fl,saveEnvRepo:Oe,inviteMember:ou,changeMemberRole:fu,reloadMcp:du,mcpOauth:B,adminQuota:k,adminSweep:P,adminBackup:ct}}const vo=20;function ug(){const[i,r]=d.useState([]),[h,f]=d.useState(""),[p,N]=d.useState(""),[C,j]=d.useState(""),[z,R]=d.useState(""),[q,g]=d.useState(0),[U,F]=d.useState(0),[X,J]=d.useState({}),V=d.useRef(0),vt=d.useRef({status:"",executor:"",q:""});vt.current={status:p,executor:C,q:z};const St=d.useRef(""),jt=d.useCallback(async(ot,yt,G)=>{const Y=ot!==void 0?ot:yt;if(!Y){r([]),F(0),J({});return}St.current=Y;const tt=G!==void 0?G:V.current,gt=vt.current;try{const xt=await at(W.tasksPage(Y,vo,tt*vo,gt.status,gt.executor,gt.q));r(xt.tasks||[]),F(xt.total??(xt.tasks||[]).length),J(xt.status_counts||{}),f("")}catch(xt){f("加载失败:"+xt.message)}},[]),K=d.useCallback(ot=>{const yt=Math.max(0,ot);V.current=yt,g(yt)},[]),Nt=d.useCallback(ot=>{K(ot),jt(St.current||void 0,void 0,ot)},[jt,K]);d.useEffect(()=>{if(!St.current)return;const ot=setTimeout(()=>{jt(St.current,void 0,0)},300);return()=>clearTimeout(ot)},[p,C,z]);const w=d.useCallback(ot=>{N(ot),K(0)},[K]),Q=d.useCallback(ot=>{j(ot),K(0)},[K]),nt=d.useCallback(ot=>{R(ot),K(0)},[K]),Dt=d.useCallback(async ot=>{K(0),await jt(ot,void 0,0)},[jt,K]),qt=Math.max(1,Math.ceil(U/vo)),dt=U===0?"共 0 个任务":"共 "+U+" 个任务,第 "+(q+1)+" / "+qt+" 页",kt=i,H=d.useCallback(async(ot,yt)=>{const G={...ot};yt.length&&(G.upload_id=await lg(yt));const Y=await at(W.tasksCreate,"POST",G);return"任务已建:"+Ae(Y.id)},[]);return{tasks:i,taskError:h,statusFilter:p,setStatusFilter:w,executorFilter:C,setExecutorFilter:Q,searchText:z,setSearchText:nt,filteredTasks:kt,taskCountText:dt,refreshTasks:jt,refreshTasksFirstPage:Dt,page:q,totalPages:qt,taskTotal:U,taskStatusCounts:X,goToPage:Nt,dispatch:H}}function ig(i,r){const[h,f]=d.useState(null),[p,N]=d.useState(""),[C,j]=d.useState(""),[z,R]=d.useState(""),[q,g]=d.useState(""),[U,F]=d.useState(void 0),[X,J]=d.useState(!1),[V,vt]=d.useState(""),[St,jt]=d.useState([]),[K,Nt]=d.useState([]),[w,Q]=d.useState(""),[nt,Dt]=d.useState([]),[qt,dt]=d.useState(""),[kt,H]=d.useState(""),[ot,yt]=d.useState([]),[G,Y]=d.useState(""),[tt,gt]=d.useState(""),[xt,Se]=d.useState(""),[L,se]=d.useState(""),[y,D]=d.useState([]),[ut,et]=d.useState([]),[Ct,bt]=d.useState(""),[Tt,lt]=d.useState([]),[mt,re]=d.useState(""),[Ga,jl]=d.useState(""),[Xe,ae]=d.useState(""),[Qt,ye]=d.useState(""),[Jl,xe]=d.useState([]),[da,Le]=d.useState(""),[zl,Rl]=d.useState(null),Tl=d.useRef(null),Be=d.useRef(null),oe=d.useRef(null);oe.current=h;const fe=d.useCallback(()=>{if(Tl.current){try{Tl.current.abort()}catch{}Tl.current=null}Be.current&&(clearTimeout(Be.current),Be.current=null)},[]),rl=d.useCallback(async B=>{try{const k=await at(W.taskAgents(B));Nt(k.agents||[]),Q(k.note||"")}catch(k){Nt([]),Q("子代理链路加载失败:"+k.message)}},[]),Fl=d.useCallback(B=>{Be.current&&clearTimeout(Be.current),Be.current=setTimeout(()=>void rl(B),1e3)},[rl]),ma=d.useCallback(async B=>{if(!B.coordinator_parts){jt([]);return}try{const k=await at(W.tasksByParent(B.project_id||"",B.id));jt(k.tasks||[])}catch{jt([])}},[]),Dl=d.useCallback(async B=>{try{cLine truncated
|
||||
`),je+=JSON.stringify(st,null,2).slice(0,4e3),jl(je),re("已返回登录信息,见下方")}catch(st){re("发起失败:"+st.message)}},[]),k=d.useCallback(async()=>{const A=X.trim();if(!A){F("请填要查配额的用户 id");return}try{const $=await at(W.adminQuotaUser(A));F("用户 "+A.slice(0,8)+":本月用量 "+ho($.used_usd_micros)+" / 配额 "+ho($.quota_usd_micros)+($.remaining_usd_micros!==null&&$.remaining_usd_micros!==void 0?"(剩余 "+ho($.remaining_usd_micros)+")":"")+" | 月份 "+($.month||""))}catch($){F(mo($))}},[X]),P=d.useCallback(async()=>{if(window.confirm("确定手动触发 sweep 吗?超阈值的陈旧 running 任务会被标为 interrupted。"))try{const $=(await at(W.adminSweep,"POST",{})).swept||[];F("sweep 完成,清理 "+$.length+" 个任务"+($.length?":"+$.join("、"):""))}catch(A){F(mo(A))}},[]),ct=d.useCallback(async()=>{try{const A=await at(W.adminBackup,"POST",{});F("备份完成:backend="+A.backend+" | 路径 "+A.path+" | 大小 "+A.size_bytes+" 字节 | 任务 "+A.task_count+" 行")}catch(A){F(mo(A))}},[]);return{keySaved:i,keyState:h,quotaText:p,healthText:C,adminGate:z,adminBody:q,adminResult:U,adminUserId:X,setAdminUserId:J,projects:Q,projectId:St,setProjectId:jt,projectMsg:K,githubRepos:w,workers:nt,workersSummary:qt,workersError:kt,members:ot,memberMsg:G,memberError:tt,skills:xt,apps:L,mcpServers:y,skillMsg:ut,skillError:Ct,mcpMsg:Tt,mcpOauthMsg:mt,mcpOauthBox:Ga,envs:Xe,envCount:Vt,envMsg:Jl,envRepoLine:da,envRepoBar:zl,envRepoMsg:Tl,envSelect:oe,setEnvSelect:fe,models:rl,modelCustomVisible:ma,refreshHealth:qe,loadQuota:_l,probeAdmin:ol,refreshProjects:$t,loadGithubRepos:kl,loadRelayModels:mn,refreshWorkers:Va,refreshEnvs:be,refreshMembers:Qa,refreshSkills:ha,saveKeyInput:su,clearKeyInput:ru,createProject:Wl,createEnv:Me,archiveEnv:fl,saveEnvRepo:Oe,inviteMember:ou,changeMemberRole:fu,reloadMcp:du,mcpOauth:B,adminQuota:k,adminSweep:P,adminBackup:ct}}const vo=20;function ug(){const[i,r]=d.useState([]),[h,f]=d.useState(""),[p,N]=d.useState(""),[C,j]=d.useState(""),[z,R]=d.useState(""),[q,g]=d.useState(0),[U,F]=d.useState(0),[X,J]=d.useState({}),Q=d.useRef(0),vt=d.useRef({status:"",executor:"",q:""});vt.current={status:p,executor:C,q:z};const St=d.useRef(""),jt=d.useCallback(async(ot,yt,G)=>{const Y=ot!==void 0?ot:yt;if(!Y){r([]),F(0),J({});return}St.current=Y;const tt=G!==void 0?G:Q.current,gt=vt.current;try{const xt=await at(W.tasksPage(Y,vo,tt*vo,gt.status,gt.executor,gt.q));r(xt.tasks||[]),F(xt.total??(xt.tasks||[]).length),J(xt.status_counts||{}),f("")}catch(xt){f("加载失败:"+xt.message)}},[]),K=d.useCallback(ot=>{const yt=Math.max(0,ot);Q.current=yt,g(yt)},[]),Nt=d.useCallback(ot=>{K(ot),jt(St.current||void 0,void 0,ot)},[jt,K]);d.useEffect(()=>{if(!St.current)return;const ot=setTimeout(()=>{jt(St.current,void 0,0)},300);return()=>clearTimeout(ot)},[p,C,z]);const w=d.useCallback(ot=>{N(ot),K(0)},[K]),V=d.useCallback(ot=>{j(ot),K(0)},[K]),nt=d.useCallback(ot=>{R(ot),K(0)},[K]),Dt=d.useCallback(async ot=>{K(0),await jt(ot,void 0,0)},[jt,K]),qt=Math.max(1,Math.ceil(U/vo)),dt=U===0?"共 0 个任务":"共 "+U+" 个任务,第 "+(q+1)+" / "+qt+" 页",kt=i,H=d.useCallback(async(ot,yt)=>{const G={...ot};yt.length&&(G.upload_id=await lg(yt));const Y=await at(W.tasksCreate,"POST",G);return"任务已建:"+Ae(Y.id)},[]);return{tasks:i,taskError:h,statusFilter:p,setStatusFilter:w,executorFilter:C,setExecutorFilter:V,searchText:z,setSearchText:nt,filteredTasks:kt,taskCountText:dt,refreshTasks:jt,refreshTasksFirstPage:Dt,page:q,totalPages:qt,taskTotal:U,taskStatusCounts:X,goToPage:Nt,dispatch:H}}function ig(i,r){const[h,f]=d.useState(null),[p,N]=d.useState(""),[C,j]=d.useState(""),[z,R]=d.useState(""),[q,g]=d.useState(""),[U,F]=d.useState(void 0),[X,J]=d.useState(!1),[Q,vt]=d.useState(""),[St,jt]=d.useState([]),[K,Nt]=d.useState([]),[w,V]=d.useState(""),[nt,Dt]=d.useState([]),[qt,dt]=d.useState(""),[kt,H]=d.useState(""),[ot,yt]=d.useState([]),[G,Y]=d.useState(""),[tt,gt]=d.useState(""),[xt,Se]=d.useState(""),[L,se]=d.useState(""),[y,D]=d.useState([]),[ut,et]=d.useState([]),[Ct,bt]=d.useState(""),[Tt,lt]=d.useState([]),[mt,re]=d.useState(""),[Ga,jl]=d.useState(""),[Xe,ae]=d.useState(""),[Vt,ye]=d.useState(""),[Jl,xe]=d.useState([]),[da,Le]=d.useState(""),[zl,Rl]=d.useState(null),Tl=d.useRef(null),Be=d.useRef(null),oe=d.useRef(null);oe.current=h;const fe=d.useCallback(()=>{if(Tl.current){try{Tl.current.abort()}catch{}Tl.current=null}Be.current&&(clearTimeout(Be.current),Be.current=null)},[]),rl=d.useCallback(async B=>{try{const k=await at(W.taskAgents(B));Nt(k.agents||[]),V(k.note||"")}catch(k){Nt([]),V("子代理链路加载失败:"+k.message)}},[]),Fl=d.useCallback(B=>{Be.current&&clearTimeout(Be.current),Be.current=setTimeout(()=>void rl(B),1e3)},[rl]),ma=d.useCallback(async B=>{if(!B.coordinator_parts){jt([]);return}try{const k=await at(W.tasksByParent(B.project_id||"",B.id));jt(k.tasks||[])}catch{jt([])}},[]),Dl=d.useCallback(async B=>{try{cLine truncated
|
||||
|
||||
`);hn=wl.pop()||"";for(const mu of wl){const hu=mu.split(`
|
||||
`).filter(Ze=>Ze.indexOf("data: ")===0).map(Ze=>Ze.slice(6)).join(`
|
||||
@@ -64,17 +64,17 @@ Error generating stack: `+a.message+`
|
||||
`+k.result_text.slice(0,8e3)+`
|
||||
`),k.error_text&&(P+=`=== error ===
|
||||
`+k.error_text.slice(0,8e3)+`
|
||||
`),P||(P="(暂无 result/error 输出)"),N(P),g(""),F(void 0),Dl(B),J(!1),vt(""),re(""),ye(""),mn(k.status);const ct=i(k.project_id),A=ct==="maintainer"||ct==="owner",$=ct==="contributor"||A,st=uu.includes(k.status),Yt=[];["failed","interrupted","merge-failed"].includes(k.status)&&A||Yt.push("重试:不可用——仅 failed/interrupted/merge-failed 可重试"+(A?"":",且仅 maintainer 可重试")),!st&&$||Yt.push("取消:不可用——"+(st?"终态任务不可取消":"仅项目成员(contributor 及以上)可取消")),k.status==="completed"&&A||Yt.push("合并:不可用——仅 completed 可合并"+(A?"":",且仅 maintainer 可合并")),k.status==="merged"&&A||Yt.push("提 PR:不可用——仅 merged 可提 PR"+(A?"":",且仅 maintainer 可提 PR")),R(Yt.join(";")),j(""),Qa(B,k.status),rl(B),ma(k),Pe(B),ee(B),ol(B),$t(B),kl(B),qe(B),_l(B)}catch(k){f(null),N("加载失败:"+k.message)}},[fe,Qa,rl,ma,Pe,ee,ol,$t,kl,qe,_l,Dl,i]),Va=d.useCallback(async()=>{const B=oe.current;if(B){try{const k=await at(W.task(B.id));f(k);let P="";k.result_text&&(P+=`=== result ===
|
||||
`),P||(P="(暂无 result/error 输出)"),N(P),g(""),F(void 0),Dl(B),J(!1),vt(""),re(""),ye(""),mn(k.status);const ct=i(k.project_id),A=ct==="maintainer"||ct==="owner",$=ct==="contributor"||A,st=uu.includes(k.status),Yt=[];["failed","interrupted","merge-failed"].includes(k.status)&&A||Yt.push("重试:不可用——仅 failed/interrupted/merge-failed 可重试"+(A?"":",且仅 maintainer 可重试")),!st&&$||Yt.push("取消:不可用——"+(st?"终态任务不可取消":"仅项目成员(contributor 及以上)可取消")),k.status==="completed"&&A||Yt.push("合并:不可用——仅 completed 可合并"+(A?"":",且仅 maintainer 可合并")),k.status==="merged"&&A||Yt.push("提 PR:不可用——仅 merged 可提 PR"+(A?"":",且仅 maintainer 可提 PR")),R(Yt.join(";")),j(""),Va(B,k.status),rl(B),ma(k),Pe(B),ee(B),ol(B),$t(B),kl(B),qe(B),_l(B)}catch(k){f(null),N("加载失败:"+k.message)}},[fe,Va,rl,ma,Pe,ee,ol,$t,kl,qe,_l,Dl,i]),Qa=d.useCallback(async()=>{const B=oe.current;if(B){try{const k=await at(W.task(B.id));f(k);let P="";k.result_text&&(P+=`=== result ===
|
||||
`+k.result_text.slice(0,8e3)+`
|
||||
`),k.error_text&&(P+=`=== error ===
|
||||
`+k.error_text.slice(0,8e3)+`
|
||||
`),N(P||"(暂无 result/error 输出)")}catch{}ee(B.id),Pe(B.id),qe(B.id),_l(B.id),ma(B),rl(B.id),ol(B.id),$t(B.id),kl(B.id),Dl(B.id)}},[ee,Pe,qe,_l,ma,rl,ol,$t,kl,Dl]),ha=d.useCallback(async B=>{const k=oe.current;if(!k)return;const P={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!(P[B]&&!window.confirm(P[B])))try{const ct=await at(W.taskAction(k.id,B),"POST",{});j(B+" 已提交:"+JSON.stringify(ct).slice(0,300)),r(),await be(k.id)}catch(ct){j(B+" 失败:"+ct.message)}},[be,r]),su=d.useCallback(async()=>{const B=oe.current;if(B)try{const k=await at(W.taskCost(B.id));g(JSON.stringify(k,null,2).slice(0,4e3))}catch(k){g("查成本失败:"+k.message)}},[]),ru=d.useCallback(async B=>{const k=oe.current;if(k){if(!B.trim()){vt("分支消息必填");return}if(!uu.includes(k.status)){vt("仅终态任务可分支");return}try{const P=await at(W.taskFork(k.id),"POST",{message:B.trim()});vt("分支任务已建:"+P.id.slice(0,8)),r()}catch(P){vt("分支失败:"+P.message)}}},[r]),Wl=d.useCallback(async B=>{const k=oe.current;if(k)try{await at(W.taskReviews(k.id),"POST",B),dt("评审已登记(requested)"),await qe(k.id)}catch(P){dt("登记评审需求失败:"+P.message)}},[qe]),Me=d.useCallback(B=>{const k=oe.current;if(!k)return;const P=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");P!==null&&at(W.taskReviewFindings(k.id,B),"POST",{findings:P}).then(()=>void qe(k.id)).catch(ct=>dt("写 findings 失败:"+ct.message))},[qe]),fl=d.useCallback((B,k)=>{const P=oe.current;if(!P)return;const ct=k==="approved"?"approve":"deny";at(W.taskApprovalDecision(P.id,B,ct),"POST",{}).then(()=>{bt("已记录"+(k==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),_l(P.id)}).catch(A=>bt("决定失败:"+A.message))},[_l]),Oe=d.useCallback(async(B,k,P)=>{const ct=oe.current;if(ct){if(!k.trim()){re("目标任务 id 不能为空");return}if(!P.trim()){re("消息正文不能为空");return}try{const A=await at(W.taskMessagesTo(k.trim()),"POST",{from_task_id:ct.id,body:P.trim(),mode:B});re("已发送("+A.delivery+")"),await ol(ct.id)}catch(A){re("发送失败:"+A.message)}}},[ol]),ou=d.useCallback((B,k)=>{const P=oe.current;if(!P)return;if(!["completed","failed","interrupted"].includes(P.status)){ye("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const ct={};if(B==="rename"){if(!k.trim()){ye("新线程名不能为空");return}ct.name=k.trim()}at(W.taskThreadAction(P.id,B),"POST",ct).then(A=>{ye(B+" 已提交:"+JSON.stringify(A).slice(0,300)),$t(P.id),kl(P.id)}).catch(A=>ye(B+" 失败:"+A.message))},[$t,kl]),fu=d.useCallback(async B=>{const k=oe.current;if(!k)return;const P=B.trim();if(!P){Le("消息不能为空");return}try{uu.includes(k.status)&&k.status!=="merged"?(await at(W.taskContinue(k.id),"POST",{message:P}),Le("已重新排队,同一会话继续"),r(),await be(k.id)):["queued","running"].includes(k.status)?(await at(W.taskFollowups(k.id),"POST",{body:P,mode:"direct"}),Le("已直接发送"),await ee(k.id),await Pe(k.id)):Le("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(ct){Le("发送失败:"+ct.message)}},[be,r,ee,Pe]),du=d.useCallback(async()=>{const B=oe.current;if(!B){se("先选一个任务");return}try{const k=await at(W.taskTrace(B.id)),P=new Blob([JSON.stringify({task_id:B.id,status:k.status,note:k.note||"",count:(k.entries||[]).length,entries:k.entries||[]},null,2)],{type:"application/json"}),ct=document.createElement("a");ct.href=URL.createObjectURL(P),ct.download="trace-"+B.id.slice(0,8)+".json",document.body.appendChild(ct),ct.click(),ct.remove(),setTimeout(()=>URL.revokeObjectURL(ct.href),1e3),se("已下载完整链路 "+(k.entries||[]).length+" 条")}catch(k){se("下载失败:"+k.message)}},[]);return d.useEffect(()=>()=>fe(),[fe]),{currentTask:h,detailBody:p,actionMsg:C,actionReasons:z,costBox:q,costSummary:U,forkVisible:X,setForkVisible:J,forkMsg:V,coordChildren:St,agents:K,agentsNote:w,reviews:nt,reviewMsg:qt,reviewError:kt,traceEntries:ot,traceNote:G,traceDone:tt,traceKind:xt,setTraceKind:Se,traceMsg:L,followups:y,approvals:ut,approvalMsg:Ct,peerMessages:Tt,peerMsg:mt,threadText:Ga,threadGateNote:Xe,threadMsg:Qt,threadList:Jl,chatMsg:da,chatHistory:zl,showDetail:be,refreshDetailLists:Va,doAction:ha,taskCost:su,forkTask:ru,postReview:Wl,submitFindings:Me,decideApproval:fl,postPeer:Oe,threadAction:ou,sendChatMessage:fu,downloadTrace:du,refreshReviews:()=>void qe(),refreshApprovals:()=>void _l()}}function cg(i){const[r,h]=d.useState([]),[f,p]=d.useState(""),[N,C]=d.useState(null),[j,z]=d.useState(""),R=d.useCallback(async X=>{const J=X!==void 0?X:i();if(!J){h([]);return}try{const V=await at(W.projectAutomations(J));h(V.automations||[])}catch(V){p("自动化加载失败:"+V.message)}},Line truncated
|
||||
(链路摘要已截断至 300 字,完整回复见右侧任务结果)`:f,label:"助手"}}if(i.kind==="exec"||i.kind==="file"||i.kind==="tool"||i.kind==="call"){const f=i.kind==="exec"?"命令":i.kind==="file"?"文件":i.kind==="tool"?"工具":"调用";return{id:h,role:"tool",kind:i.kind,text:go(i),label:f}}return i.kind==="plan"?{id:h,role:"status",kind:i.kind,text:i.text||""}:i.kind==="reasoning"?{id:h,role:"status",kind:i.kind,text:"思考:"+(i.summary||"")}:i.kind==="agent-start"?{id:h,role:"status",kind:i.kind,text:"子代理开始:"+(i.name||"")+(i.prompt?"("+i.prompt+")":"")}:i.kind==="agent-done"?{id:h,role:"status",kind:i.kind,text:"子代理结束:"+(i.name||"")+" "+(i.status||"")}:{id:h,role:"status",kind:i.kind,text:go(i)}}function jg(){const i=Kt(),r=i.currentTask,h=d.useMemo(()=>{if(!r)return[];let z;if(i.chatHistory){z=i.chatHistory.messages.map(X=>({id:X.id,role:X.role,text:X.text,label:X.label,kind:X.kind}));const U=new Set(z.map(X=>X.id)),F=i.traceEntries.filter(X=>X.id&&!U.has(X.id)&&!String(X.id).startsWith("fu-")).map((X,J)=>Zh(X,J));z=[...z,...F]}else{const U=r.prompt?[{id:"task-prompt",role:"user",text:r.prompt}]:[],F=i.traceEntries.filter(J=>!String(J.id||"").startsWith("fu-")).map((J,V)=>Zh(J,V)),X=i.followups.map((J,V)=>({id:"followup-"+V,role:"user",text:String(J.body||""),label:"后续消息"}));z=[...U,...F,...X]}const R=i.traceKind,q=R?z.filter(U=>U.role==="user"||!U.kind||U.kind===R):z,g=i.traceDone?[{id:"trace-done",role:"status",text:"链路已结束("+i.traceDone+")"}]:[];return[...q,...g]},[r,i.chatHistory,i.traceEntries,i.followups,i.traceKind,i.traceDone]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示继续会话"});const f=uu.includes(r.status),p=["queued","running"].includes(r.status),N=f||p,C=f?"终态任务:发送走继续接口(同一会话再跑一轮)":p?"运行中/排队中:发送走后续消息直接发送(跑完当前轮后优先执行)":"merged/merge-failed 任务不可继续,也不可发后续消息",j=i.chatHistory?i.chatHistory.legacy_continue_count??null:null;return c.jsxs("div",{className:"chat-column",children:[c.jsx("div",{className:"muted",children:"实时对话"}),c.jsxs(Ht,{children:[c.jsxs("select",{"aria-label":"链路类型",value:i.traceKind,onChange:z=>i.setTraceKind(z.target.value),children:[c.jsx("option",{value:"",children:"链路类型:全部"}),["exec","file","call","tool","message","reasoning","plan","agent-start","agent-done"].map(z=>c.jsx("option",{value:z,children:z==="exec"?"命令":z==="file"?"文件":z==="call"?"调用":z==="tool"?"工具":z==="message"?"消息":z==="reasoning"?"思考":z==="plan"?"计划":"子代理"},z))]}),c.jsx("button",{onClick:i.downloadTrace,children:"下载完整链路"}),c.jsx("span",{className:"muted",children:i.traceMsg})]}),i.traceNote?c.jsx("div",{children:c.jsx("span",{className:"err",children:i.traceNote})}):null,j!==null&&j>0?c.jsxs("div",{className:"muted",children:["此前有 ",j," 条继续消息发生在持久化之前,未入库,上面历史不完整;部署后新发的继续消息会正常显示。"]}):null,i.chatHistory&&i.chatHistory.trace_note?c.jsx("div",{className:"muted",children:i.chatHistory.trace_note}):null,c.jsx(xg,{messages:h,empty:"(暂无会话消息)"}),c.jsx(bg,{disabled:!N,placeholder:"消息",onSend:z=>void i.sendChatMessage(z)}),c.jsxs("div",{className:"muted",children:[C,"。",i.chatMsg]}),c.jsx("div",{className:"muted",children:"对话时间线:首条是本任务 prompt(用户);助手回复、命令/文件/工具/调用、计划/思考/子代理开始结束都在同一列里,不再另起一份链路名单。流式进行中时新条目实时追加;你在底部时自动跟随,往上翻时暂停跟随,点“回到最新”恢复。"})]})}function Tg(){return Kt().currentTask?c.jsx("div",{className:"conversation",children:c.jsx(jg,{})}):c.jsx("div",{className:"conversation",children:c.jsx("div",{className:"muted",children:"在任务列表里点一条任务,对话和详情就切到该任务;也可以在上面派发一个新任务。"})})}function _g(){const i=Kt(),r=i.currentTask,[h,f]=d.useState("");if(!r)return null;const p=uu.includes(r.status),N=["failed","interrupted","merge-failed"].includes(r.status);return c.jsxs(c.Fragment,{children:[c.jsxs(Ht,{children:[c.jsx("button",{disabled:!N,title:N?"":"仅 failed/interrupted/merge-failed 可重试",onClick:()=>i.doAction("retry"),children:"重试"}),c.jsx("button",{className:"danger",disabled:p,title:p?"终态任务不可取消":"",onClick:()=>i.doAction("cancel"),children:"取消"}),c.jsx("button",{disabled:r.status!=="completed",title:r.status!=="completed"?"仅 completed 可合并":"",onClick:()=>i.doAction("merge"),children:"合并(completed → merged)"}),c.jsx("button",{disabled:r.status!=="merged",title:r.status!=="merged"?"仅 merged 可提 PR":"",onClick:()=>i.doAction("pr"),children:"提 PR(须 mergedLine truncated
|
||||
`),N(P||"(暂无 result/error 输出)")}catch{}ee(B.id),Pe(B.id),qe(B.id),_l(B.id),ma(B),rl(B.id),ol(B.id),$t(B.id),kl(B.id),Dl(B.id)}},[ee,Pe,qe,_l,ma,rl,ol,$t,kl,Dl]),ha=d.useCallback(async B=>{const k=oe.current;if(!k)return;const P={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!(P[B]&&!window.confirm(P[B])))try{const ct=await at(W.taskAction(k.id,B),"POST",{});j(B+" 已提交:"+JSON.stringify(ct).slice(0,300)),r(),await be(k.id)}catch(ct){j(B+" 失败:"+ct.message)}},[be,r]),su=d.useCallback(async()=>{const B=oe.current;if(B)try{const k=await at(W.taskCost(B.id));g(JSON.stringify(k,null,2).slice(0,4e3))}catch(k){g("查成本失败:"+k.message)}},[]),ru=d.useCallback(async B=>{const k=oe.current;if(k){if(!B.trim()){vt("分支消息必填");return}if(!uu.includes(k.status)){vt("仅终态任务可分支");return}try{const P=await at(W.taskFork(k.id),"POST",{message:B.trim()});vt("分支任务已建:"+P.id.slice(0,8)),r()}catch(P){vt("分支失败:"+P.message)}}},[r]),Wl=d.useCallback(async B=>{const k=oe.current;if(k)try{await at(W.taskReviews(k.id),"POST",B),dt("评审已登记(requested)"),await qe(k.id)}catch(P){dt("登记评审需求失败:"+P.message)}},[qe]),Me=d.useCallback(B=>{const k=oe.current;if(!k)return;const P=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");P!==null&&at(W.taskReviewFindings(k.id,B),"POST",{findings:P}).then(()=>void qe(k.id)).catch(ct=>dt("写 findings 失败:"+ct.message))},[qe]),fl=d.useCallback((B,k)=>{const P=oe.current;if(!P)return;const ct=k==="approved"?"approve":"deny";at(W.taskApprovalDecision(P.id,B,ct),"POST",{}).then(()=>{bt("已记录"+(k==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),_l(P.id)}).catch(A=>bt("决定失败:"+A.message))},[_l]),Oe=d.useCallback(async(B,k,P)=>{const ct=oe.current;if(ct){if(!k.trim()){re("目标任务 id 不能为空");return}if(!P.trim()){re("消息正文不能为空");return}try{const A=await at(W.taskMessagesTo(k.trim()),"POST",{from_task_id:ct.id,body:P.trim(),mode:B});re("已发送("+A.delivery+")"),await ol(ct.id)}catch(A){re("发送失败:"+A.message)}}},[ol]),ou=d.useCallback((B,k)=>{const P=oe.current;if(!P)return;if(!["completed","failed","interrupted"].includes(P.status)){ye("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const ct={};if(B==="rename"){if(!k.trim()){ye("新线程名不能为空");return}ct.name=k.trim()}at(W.taskThreadAction(P.id,B),"POST",ct).then(A=>{ye(B+" 已提交:"+JSON.stringify(A).slice(0,300)),$t(P.id),kl(P.id)}).catch(A=>ye(B+" 失败:"+A.message))},[$t,kl]),fu=d.useCallback(async B=>{const k=oe.current;if(!k)return;const P=B.trim();if(!P){Le("消息不能为空");return}try{uu.includes(k.status)&&k.status!=="merged"?(await at(W.taskContinue(k.id),"POST",{message:P}),Le("已重新排队,同一会话继续"),r(),await be(k.id)):["queued","running"].includes(k.status)?(await at(W.taskFollowups(k.id),"POST",{body:P,mode:"direct"}),Le("已直接发送"),await ee(k.id),await Pe(k.id)):Le("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(ct){Le("发送失败:"+ct.message)}},[be,r,ee,Pe]),du=d.useCallback(async()=>{const B=oe.current;if(!B){se("先选一个任务");return}try{const k=await at(W.taskTrace(B.id)),P=new Blob([JSON.stringify({task_id:B.id,status:k.status,note:k.note||"",count:(k.entries||[]).length,entries:k.entries||[]},null,2)],{type:"application/json"}),ct=document.createElement("a");ct.href=URL.createObjectURL(P),ct.download="trace-"+B.id.slice(0,8)+".json",document.body.appendChild(ct),ct.click(),ct.remove(),setTimeout(()=>URL.revokeObjectURL(ct.href),1e3),se("已下载完整链路 "+(k.entries||[]).length+" 条")}catch(k){se("下载失败:"+k.message)}},[]);return d.useEffect(()=>()=>fe(),[fe]),{currentTask:h,detailBody:p,actionMsg:C,actionReasons:z,costBox:q,costSummary:U,forkVisible:X,setForkVisible:J,forkMsg:Q,coordChildren:St,agents:K,agentsNote:w,reviews:nt,reviewMsg:qt,reviewError:kt,traceEntries:ot,traceNote:G,traceDone:tt,traceKind:xt,setTraceKind:Se,traceMsg:L,followups:y,approvals:ut,approvalMsg:Ct,peerMessages:Tt,peerMsg:mt,threadText:Ga,threadGateNote:Xe,threadMsg:Vt,threadList:Jl,chatMsg:da,chatHistory:zl,showDetail:be,refreshDetailLists:Qa,doAction:ha,taskCost:su,forkTask:ru,postReview:Wl,submitFindings:Me,decideApproval:fl,postPeer:Oe,threadAction:ou,sendChatMessage:fu,downloadTrace:du,refreshReviews:()=>void qe(),refreshApprovals:()=>void _l()}}function cg(i){const[r,h]=d.useState([]),[f,p]=d.useState(""),[N,C]=d.useState(null),[j,z]=d.useState(""),R=d.useCallback(async X=>{const J=X!==void 0?X:i();if(!J){h([]);return}try{const Q=await at(W.projectAutomations(J));h(Q.automations||[])}catch(Q){p("自动化加载失败:"+Q.message)}},Line truncated
|
||||
(链路摘要已截断至 300 字,完整回复见右侧任务结果)`:f,label:"助手"}}if(i.kind==="exec"||i.kind==="file"||i.kind==="tool"||i.kind==="call"){const f=i.kind==="exec"?"命令":i.kind==="file"?"文件":i.kind==="tool"?"工具":"调用";return{id:h,role:"tool",kind:i.kind,text:go(i),label:f}}return i.kind==="plan"?{id:h,role:"status",kind:i.kind,text:i.text||""}:i.kind==="reasoning"?{id:h,role:"status",kind:i.kind,text:"思考:"+(i.summary||"")}:i.kind==="agent-start"?{id:h,role:"status",kind:i.kind,text:"子代理开始:"+(i.name||"")+(i.prompt?"("+i.prompt+")":"")}:i.kind==="agent-done"?{id:h,role:"status",kind:i.kind,text:"子代理结束:"+(i.name||"")+" "+(i.status||"")}:{id:h,role:"status",kind:i.kind,text:go(i)}}function jg(){const i=Kt(),r=i.currentTask,h=d.useMemo(()=>{if(!r)return[];let z;if(i.chatHistory){z=i.chatHistory.messages.map(X=>({id:X.id,role:X.role,text:X.text,label:X.label,kind:X.kind}));const U=new Set(z.map(X=>X.id)),F=i.traceEntries.filter(X=>X.id&&!U.has(X.id)&&!String(X.id).startsWith("fu-")).map((X,J)=>Zh(X,J));z=[...z,...F]}else{const U=r.prompt?[{id:"task-prompt",role:"user",text:r.prompt}]:[],F=i.traceEntries.filter(J=>!String(J.id||"").startsWith("fu-")).map((J,Q)=>Zh(J,Q)),X=i.followups.map((J,Q)=>({id:"followup-"+Q,role:"user",text:String(J.body||""),label:"后续消息"}));z=[...U,...F,...X]}const R=i.traceKind,q=R?z.filter(U=>U.role==="user"||!U.kind||U.kind===R):z,g=i.traceDone?[{id:"trace-done",role:"status",text:"链路已结束("+i.traceDone+")"}]:[];return[...q,...g]},[r,i.chatHistory,i.traceEntries,i.followups,i.traceKind,i.traceDone]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示继续会话"});const f=uu.includes(r.status),p=["queued","running"].includes(r.status),N=f||p,C=f?"终态任务:发送走继续接口(同一会话再跑一轮)":p?"运行中/排队中:发送走后续消息直接发送(跑完当前轮后优先执行)":"merged/merge-failed 任务不可继续,也不可发后续消息",j=i.chatHistory?i.chatHistory.legacy_continue_count??null:null;return c.jsxs("div",{className:"chat-column",children:[c.jsx("div",{className:"muted",children:"实时对话"}),c.jsxs(Ht,{children:[c.jsxs("select",{"aria-label":"链路类型",value:i.traceKind,onChange:z=>i.setTraceKind(z.target.value),children:[c.jsx("option",{value:"",children:"链路类型:全部"}),["exec","file","call","tool","message","reasoning","plan","agent-start","agent-done"].map(z=>c.jsx("option",{value:z,children:z==="exec"?"命令":z==="file"?"文件":z==="call"?"调用":z==="tool"?"工具":z==="message"?"消息":z==="reasoning"?"思考":z==="plan"?"计划":"子代理"},z))]}),c.jsx("button",{onClick:i.downloadTrace,children:"下载完整链路"}),c.jsx("span",{className:"muted",children:i.traceMsg})]}),i.traceNote?c.jsx("div",{children:c.jsx("span",{className:"err",children:i.traceNote})}):null,j!==null&&j>0?c.jsxs("div",{className:"muted",children:["此前有 ",j," 条继续消息发生在持久化之前,未入库,上面历史不完整;部署后新发的继续消息会正常显示。"]}):null,i.chatHistory&&i.chatHistory.trace_note?c.jsx("div",{className:"muted",children:i.chatHistory.trace_note}):null,c.jsx(xg,{messages:h,empty:"(暂无会话消息)"}),c.jsx(bg,{disabled:!N,placeholder:"消息",onSend:z=>void i.sendChatMessage(z)}),c.jsxs("div",{className:"muted",children:[C,"。",i.chatMsg]}),c.jsx("div",{className:"muted",children:"对话时间线:首条是本任务 prompt(用户);助手回复、命令/文件/工具/调用、计划/思考/子代理开始结束都在同一列里,不再另起一份链路名单。流式进行中时新条目实时追加;你在底部时自动跟随,往上翻时暂停跟随,点“回到最新”恢复。"})]})}function Tg(){return Kt().currentTask?c.jsx("div",{className:"conversation",children:c.jsx(jg,{})}):c.jsx("div",{className:"conversation",children:c.jsx("div",{className:"muted",children:"在任务列表里点一条任务,对话和详情就切到该任务;也可以在上面派发一个新任务。"})})}function _g(){const i=Kt(),r=i.currentTask,[h,f]=d.useState("");if(!r)return null;const p=uu.includes(r.status),N=["failed","interrupted","merge-failed"].includes(r.status);return c.jsxs(c.Fragment,{children:[c.jsxs(Ht,{children:[c.jsx("button",{disabled:!N,title:N?"":"仅 failed/interrupted/merge-failed 可重试",onClick:()=>i.doAction("retry"),children:"重试"}),c.jsx("button",{className:"danger",disabled:p,title:p?"终态任务不可取消":"",onClick:()=>i.doAction("cancel"),children:"取消"}),c.jsx("button",{disabled:r.status!=="completed",title:r.status!=="completed"?"仅 completed 可合并":"",onClick:()=>i.doAction("merge"),children:"合并(completed → merged)"}),c.jsx("button",{disabled:r.status!=="merged",title:r.status!=="merged"?"仅 merged 可提 PR":"",onClick:()=>i.doAction("pr"),children:"提 PR(须 mergedLine truncated
|
||||
|
||||
`);for(;ot>=0;){const yt=dt.slice(0,ot);dt=dt.slice(ot+2);for(const G of yt.split(`
|
||||
`))if(G.startsWith("data: "))try{const Y=JSON.parse(G.slice(6));Y.waiting?j(Y.task_status==="interrupted"?"任务已中断,等待续跑…":Y.task_status==="paused"?"任务已暂停…":"等待会话…"):Y.image&&(f("data:image/jpeg;base64,"+Y.image),j("直播中"));const tt={};Y.holder&&(tt.holder=Y.holder),Y.url&&(tt.url=Y.url),Y.title!==void 0&&(tt.title=Y.title),Object.keys(tt).length>0&&N(gt=>({...gt||{},...tt}))}catch{}ot=dt.indexOf(`
|
||||
|
||||
`)}}j("流已断开")}catch(nt){if(nt.name==="AbortError")return;R(nt instanceof Error?nt.message:String(nt)),j("未开始")}finally{g(!1)}},[r]),V=d.useCallback(()=>{var w;(w=U.current)==null||w.abort(),g(!1),j("已停止")},[]),vt=d.useCallback(async w=>{if(r)try{const Q=await at(W.taskBrowserControl(r.id),"POST",{holder:w});N(Q),R("")}catch(Q){R(Q instanceof Error?Q.message:String(Q))}},[r]),St=d.useCallback(async w=>{if(!r||(p==null?void 0:p.holder)!=="user"||!F.current)return;const Q=F.current.getBoundingClientRect(),nt=Math.round((w.clientX-Q.left)/Q.width*1280),Dt=Math.round((w.clientY-Q.top)/Q.height*800);try{await at(W.taskBrowserInput(r.id),"POST",{type:"mouse_click",x:nt,y:Dt})}catch(qt){R(qt instanceof Error?qt.message:String(qt))}},[r,p]),jt=d.useCallback(async w=>{if(!(!r||(p==null?void 0:p.holder)!=="user")){w.preventDefault();try{w.key.length===1?await at(W.taskBrowserInput(r.id),"POST",{type:"type",text:w.key}):await at(W.taskBrowserInput(r.id),"POST",{type:"key",key:w.key})}catch(Q){R(Q instanceof Error?Q.message:String(Q))}}},[r,p]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情"});const K=(p==null?void 0:p.holder)||"—",Nt=p!==null;return c.jsxs("div",{children:[c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",alignItems:"center"},children:[c.jsxs("span",{children:["状态 ",c.jsx("b",{children:C})]}),c.jsxs("span",{className:"muted",children:["会话 ",Nt?"活跃":"未激活(轮次结束即消失)"]}),c.jsxs("span",{className:"muted",children:["控制权 ",K]}),p!=null&&p.url?c.jsx("span",{className:"muted",title:p.url,children:p.url.slice(0,80)}):null]}),c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginTop:8},children:[q?c.jsx("button",{type:"button",onClick:V,children:"停止观看"}):c.jsx("button",{type:"button",onClick:()=>void J(),children:"开始观看"}),c.jsx("button",{type:"button",onClick:()=>void X(),children:"刷新状态"}),K!=="user"?c.jsx("button",{type:"button",onClick:()=>void vt("user"),disabled:!Nt,children:"接管"}):c.jsx("button",{type:"button",onClick:()=>void vt("agent"),children:"交还给 Agent"})]}),z?c.jsx("div",{className:"muted",style:{marginTop:8},children:z}):null,K==="user"?c.jsx("div",{className:"muted",style:{marginTop:8},children:"你已接管:点画面即点击,键盘输入直达页面。交还后 Agent 续跑前会先快照确认你的改动。"}):null,c.jsx("div",{style:{marginTop:8},children:h?c.jsx("img",{ref:F,src:h,alt:"浏览器实时画面",tabIndex:0,onClick:w=>void St(w),onKeyDown:w=>void jt(w),style:{width:"100%",maxWidth:1280,cursor:K==="user"?"crosshair":"default",border:"1px solid var(--border, #ccc)"}}):c.jsx("div",{className:"muted",children:Nt?"点“开始观看”接入画面流(接管后点画面即点击)":"浏览器会话未激活:任务本轮未开浏览器,或轮次已结束。"})})]})}function Ng(){const i=Kt(),[r,h]=d.useState("uncommittedChanges"),[f,p]=d.useState(""),[N,C]=d.useState(""),[j,z]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("h3",{children:"评审队列"}),c.jsxs("select",{"aria-label":"评审目标",value:r,onChange:R=>h(R.target.value),children:[c.jsx("option",{value:"uncommittedChanges",children:"未提交改动(工作区)"}),c.jsx("option",{value:"baseBranch",children:"与分支的差异"}),c.jsx("option",{value:"commit",children:"指定提交"}),c.jsx("option",{value:"custom",children:"自定义评审指令"})]}),c.jsx("input",{className:"stacked",placeholder:"与分支的差异用:分支名(如 main)",style:{width:"100%"},value:f,onChange:R=>p(R.target.value)}),c.jsx("input",{className:"stacked",placeholder:"指定提交用:提交 sha",style:{width:"100%"},value:N,onChange:R=>C(R.target.value)}),c.jsx("textarea",{placeholder:"自定义评审指令用:评审指令正文",value:j,onChange:R=>z(R.target.value)}),c.jsxs(Ht,{children:[c.jsx("button",{className:"primary",onClick:()=>void i.postReview({target_type:r,branch:f.trim()||void 0,sha:N.trim()||void 0,instructions:j.trim()||void 0}),children:"登记评审需求"}),c.jsx("button",{onClick:i.refreshReviews,children:"刷新队列"}),c.jsx("span",{className:"muted",children:i.reviewMsg})]}),c.jsx("div",{className:"muted",children:"本基线只登记评审需求(requested),不自动跑评审;评审是否执行、结果如何,以评审队列里的记录为准。"}),i.reviewError?c.jsx("div",{className:"muted",children:i.reviewError}):i.reviews.length===0?c.jsx("div",{className:"muted",children:"点任务行查看详情后,点“刷新队列”查看评审记录"}):c.jsx(cu,{children:i.reviews.map(R=>{let q=R.target_type||"";return R.branch&&(q+=":"+R.branch),R.sha&&(q+=":"+String(R.sha).slice(0,12)),R.instructions&&(q+=":"+String(R.instructions).slice(0,80)),c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:"评审"})," ",Ae(R.id)," | ",q," | ",c.jsx("span",{className:"st-"+(R.status||""),children:RLine truncated
|
||||
`)}}j("流已断开")}catch(nt){if(nt.name==="AbortError")return;R(nt instanceof Error?nt.message:String(nt)),j("未开始")}finally{g(!1)}},[r]),Q=d.useCallback(()=>{var w;(w=U.current)==null||w.abort(),g(!1),j("已停止")},[]),vt=d.useCallback(async w=>{if(r)try{const V=await at(W.taskBrowserControl(r.id),"POST",{holder:w});N(V),R("")}catch(V){R(V instanceof Error?V.message:String(V))}},[r]),St=d.useCallback(async w=>{if(!r||(p==null?void 0:p.holder)!=="user"||!F.current)return;const V=F.current.getBoundingClientRect(),nt=Math.round((w.clientX-V.left)/V.width*1280),Dt=Math.round((w.clientY-V.top)/V.height*800);try{await at(W.taskBrowserInput(r.id),"POST",{type:"mouse_click",x:nt,y:Dt})}catch(qt){R(qt instanceof Error?qt.message:String(qt))}},[r,p]),jt=d.useCallback(async w=>{if(!(!r||(p==null?void 0:p.holder)!=="user")){w.preventDefault();try{w.key.length===1?await at(W.taskBrowserInput(r.id),"POST",{type:"type",text:w.key}):await at(W.taskBrowserInput(r.id),"POST",{type:"key",key:w.key})}catch(V){R(V instanceof Error?V.message:String(V))}}},[r,p]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情"});const K=(p==null?void 0:p.holder)||"—",Nt=p!==null;return c.jsxs("div",{children:[c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",alignItems:"center"},children:[c.jsxs("span",{children:["状态 ",c.jsx("b",{children:C})]}),c.jsxs("span",{className:"muted",children:["会话 ",Nt?"活跃":"未激活(轮次结束即消失)"]}),c.jsxs("span",{className:"muted",children:["控制权 ",K]}),p!=null&&p.url?c.jsx("span",{className:"muted",title:p.url,children:p.url.slice(0,80)}):null]}),c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginTop:8},children:[q?c.jsx("button",{type:"button",onClick:Q,children:"停止观看"}):c.jsx("button",{type:"button",onClick:()=>void J(),children:"开始观看"}),c.jsx("button",{type:"button",onClick:()=>void X(),children:"刷新状态"}),K!=="user"?c.jsx("button",{type:"button",onClick:()=>void vt("user"),disabled:!Nt,children:"接管"}):c.jsx("button",{type:"button",onClick:()=>void vt("agent"),children:"交还给 Agent"})]}),z?c.jsx("div",{className:"muted",style:{marginTop:8},children:z}):null,K==="user"?c.jsx("div",{className:"muted",style:{marginTop:8},children:"你已接管:点画面即点击,键盘输入直达页面。交还后 Agent 续跑前会先快照确认你的改动。"}):null,c.jsx("div",{style:{marginTop:8},children:h?c.jsx("img",{ref:F,src:h,alt:"浏览器实时画面",tabIndex:0,onClick:w=>void St(w),onKeyDown:w=>void jt(w),style:{width:"100%",maxWidth:1280,cursor:K==="user"?"crosshair":"default",border:"1px solid var(--border, #ccc)"}}):c.jsx("div",{className:"muted",children:Nt?"点“开始观看”接入画面流(接管后点画面即点击)":"浏览器会话未激活:任务本轮未开浏览器,或轮次已结束。"})})]})}function Ng(){const i=Kt(),[r,h]=d.useState("uncommittedChanges"),[f,p]=d.useState(""),[N,C]=d.useState(""),[j,z]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("h3",{children:"评审队列"}),c.jsxs("select",{"aria-label":"评审目标",value:r,onChange:R=>h(R.target.value),children:[c.jsx("option",{value:"uncommittedChanges",children:"未提交改动(工作区)"}),c.jsx("option",{value:"baseBranch",children:"与分支的差异"}),c.jsx("option",{value:"commit",children:"指定提交"}),c.jsx("option",{value:"custom",children:"自定义评审指令"})]}),c.jsx("input",{className:"stacked",placeholder:"与分支的差异用:分支名(如 main)",style:{width:"100%"},value:f,onChange:R=>p(R.target.value)}),c.jsx("input",{className:"stacked",placeholder:"指定提交用:提交 sha",style:{width:"100%"},value:N,onChange:R=>C(R.target.value)}),c.jsx("textarea",{placeholder:"自定义评审指令用:评审指令正文",value:j,onChange:R=>z(R.target.value)}),c.jsxs(Ht,{children:[c.jsx("button",{className:"primary",onClick:()=>void i.postReview({target_type:r,branch:f.trim()||void 0,sha:N.trim()||void 0,instructions:j.trim()||void 0}),children:"登记评审需求"}),c.jsx("button",{onClick:i.refreshReviews,children:"刷新队列"}),c.jsx("span",{className:"muted",children:i.reviewMsg})]}),c.jsx("div",{className:"muted",children:"本基线只登记评审需求(requested),不自动跑评审;评审是否执行、结果如何,以评审队列里的记录为准。"}),i.reviewError?c.jsx("div",{className:"muted",children:i.reviewError}):i.reviews.length===0?c.jsx("div",{className:"muted",children:"点任务行查看详情后,点“刷新队列”查看评审记录"}):c.jsx(cu,{children:i.reviews.map(R=>{let q=R.target_type||"";return R.branch&&(q+=":"+R.branch),R.sha&&(q+=":"+String(R.sha).slice(0,12)),R.instructions&&(q+=":"+String(R.instructions).slice(0,80)),c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:"评审"})," ",Ae(R.id)," | ",q," | ",c.jsx("span",{className:"st-"+(R.status||""),children:RLine truncated
|
||||
<style rel="stylesheet" crossorigin>:root{--text:#1f2328;--muted:#656d76;--subtle:#6e7781;--line:#d0d7de;--line-muted:#d8dee4;--paper:#ffffff;--card:#ffffff;--subtle-bg:#f6f8fa;--inset-bg:#f6f8fa;--accent:#0969da;--accent-emphasis:#0969da;--green:#2da44e;--green-hover:#2c974b;--success:#1a7f37;--danger:#d1242f;--danger-bg:#ffebe9;--attention:#9a6700;--done:#8250df;--selected-bg:#ddf4ff;--radius:6px;--shadow:0 1px 0 rgba(27,31,36,.04);--sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;--mono:ui-monospace,SFMono-Regular,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;--ctl:32px}*{box-sizing:border-box}html,body,#root{height:100%}body{font-family:var(--sans);margin:0;color:var(--text);background:#fff;line-height:1.5;font-size:14px}a{color:var(--accent);text-underline-offset:3px}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}.muted{color:var(--muted);font-size:12px;line-height:1.5}.err{color:var(--danger)}.ok{color:var(--success)}button{cursor:pointer;font-weight:500;border-radius:var(--radius);border:1px solid var(--line);background:#fff;color:var(--text);box-shadow:var(--shadow)}button:hover{background:var(--subtle-bg)}button:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}button.primary,button.seal{background:var(--green);border-color:#1b1f2426;color:#fff}button.primary:hover,button.seal:hover{background:var(--green-hover)}button.danger{color:var(--danger);border-color:var(--line);background:#fff}button.danger:hover{background:var(--danger-bg);color:var(--danger)}.icon-btn{width:32px;height:32px;padding:0;display:grid;place-items:center;border:1px solid var(--line);border-radius:var(--radius);background:#fff;color:var(--text);font-size:16px;line-height:1}.icon-btn:hover{background:var(--subtle-bg)}.dash-topbar{position:sticky;top:0;z-index:10;background:#24292f;color:#fff;border-bottom:1px solid rgba(255,255,255,.15);padding:10px 20px 8px}.dash-topbar .muted{color:#ffffffbf}.dash-topbar-row{display:flex;gap:14px;align-items:center;flex-wrap:wrap}.dash-brand{font-weight:600;font-size:16px;color:#fff}.dash-status{font-size:13px;font-weight:600;color:#ffffffbf}.dash-status.ok{color:#2da44e}.dash-stamp{margin-left:auto}.dash-nav{display:flex;gap:4px;flex-wrap:wrap;margin-top:8px}.dash-nav a{color:#fff;text-decoration:none;font-size:13px;font-weight:600;padding:6px 12px;border-radius:var(--radius)}.dash-nav a:hover{background:#ffffff1f;text-decoration:none}.tab-nav button{background:transparent;border:0;box-shadow:none;color:#fff;font-size:13px;font-weight:600;padding:6px 12px;border-radius:var(--radius)}.tab-nav button:hover{background:#ffffff1f}.tab-nav button.active{background:#ffffff2e}.dash-page-tabs{display:flex}.dash-page-tabs>.tab-content{grid-column:1 / -1;min-width:0;display:flex;flex-direction:column;gap:20px}.tab-workbench{display:grid;grid-template-columns:320px minmax(0,1fr);gap:20px;align-items:start}.tab-workbench>*{min-width:0}@media(max-width:899px){.tab-workbench{grid-template-columns:1fr}.tab-workbench #sec-dispatch{order:1}.tab-workbench #sec-tasks{order:2}.tab-workbench #sec-conv{order:3}}.dash-page{max-width:1180px;margin:0 auto;padding:20px;display:flex;flex-direction:column;gap:20px}.dash-page>*{min-width:0}.dash-section>*{min-width:0}.dash-section{scroll-margin-top:130px;display:flex;flex-direction:column;gap:16px}.dash-section.card{padding:16px}.dash-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.dash-card{background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:16px;min-width:0}.dash-card-label{color:var(--muted);font-size:12px;font-weight:600}.dash-card-value{font-size:24px;font-weight:600;line-height:1.3;overflow-wrap:anywhere}.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:none;padding:16px}.card h2{font-size:14px;margin:0 0 12px;font-weight:600}.card h3{font-size:14px;margin:24px 0 12px;padding-top:16px;border-top:1px solid var(--line);font-weight:600}.card h3:first-of-type{border-top:0;padding-top:0}.composer h2{font-size:16px}.composer textarea.prompt{min-height:148px;font-size:14px;padding:12px;border-radius:var(--radius)}.composer textarea.coord{font-family:var(--mono);font-size:13px}.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:12px 0}.bar>button,.bar>label{display:inline-flex;align-items:center;height:var(--ctl);min-height:var(--ctl);padding:0 16px;box-sizing:border-box;white-space:nowrap}.bar>input,.bar>select{height:var(--ctl);min-height:var(--ctl)}input,select,textarea,button{font:inherit;font-size:14px;padding:5px 12px;border-radius:var(--radius);border:1px solid var(--line);background:#fff;color:var(--text)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px #0969da4d}select{-moz-appearance:none;appearance:none;-webkit-appearance:none;cursLine truncated
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1826,6 +1826,16 @@ def _task_to_dict(row, attachments=None, total_tokens=None):
|
||||
# 列表接口由 _task_tokens_map 一次聚合传入;其它调用方
|
||||
# 不传时为 None(前端显示占位),绝不把“未算”写成 0。
|
||||
"total_tokens": total_tokens,
|
||||
# feat-chv-arch95plus 只读字段:仅当 CHV 任务且 worker
|
||||
# 已把 VM 死亡写进 interrupted_reason 时才出现,值为
|
||||
# True;其它情形一律不返回该字段(绝不把“未报告死亡”
|
||||
# 写成“VM 活着”,live VM 状态不在 server 进程里,
|
||||
# server 没有 VM 登记表,编不出来也不编)。
|
||||
**({"chv_vm_dead": True}
|
||||
if _row_get(row, "executor") == "cloud-hypervisor"
|
||||
and _row_get(row, "interrupted_reason")
|
||||
and "VM 中途死亡" in str(_row_get(row, "interrupted_reason"))
|
||||
else {}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
"""CHV 架构 9.5+ 提升轨测试(feat-chv-arch95plus,堆叠于 #252)。
|
||||
|
||||
只用标准库与 mock,绝不启动真实 VM;测试里只用 FAKE
|
||||
占位密钥,绝无真实密钥。覆盖:长函数提取后的行为不变、
|
||||
WarmPool facade 不调实验接口、#215 对齐接口形状、审计
|
||||
失败计数不中断、只读字段 chv_vm_dead 的派生规则、
|
||||
lockfile 校验脚本的通过/漂移/空清单。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
|
||||
from cloud.chv.hardening import validate_snapshot_url
|
||||
from cloud.chv.pool import reconcile_orphans_for_executor
|
||||
from cloud.sandbox import CloudHypervisorExecutor
|
||||
from cloud.server import _task_to_dict
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
|
||||
from cloud.chv.hardening import validate_snapshot_url
|
||||
from cloud.chv.pool import reconcile_orphans_for_executor
|
||||
from cloud.sandbox import CloudHypervisorExecutor
|
||||
from cloud.server import _task_to_dict
|
||||
|
||||
|
||||
def _row(**kw):
|
||||
base = {
|
||||
"id": "t1", "project_id": "p1", "prompt": "x",
|
||||
"status": "interrupted", "idempotency_key": None,
|
||||
"depends_on": "[]", "priority": 0, "created_by": "u",
|
||||
"created_at": 0, "updated_at": 0, "last_heartbeat": 0,
|
||||
}
|
||||
base.update(kw)
|
||||
return base
|
||||
|
||||
|
||||
class FakeExecutor:
|
||||
def __init__(self):
|
||||
self.acquire_called = False
|
||||
self.prewarm_called = False
|
||||
|
||||
def shutdown_vm(self, vm):
|
||||
return True
|
||||
|
||||
def _emit(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class Compat215Test(unittest.TestCase):
|
||||
def test_resolve_inside_blocks_escape_and_symlink(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
inside = os.path.join(d, "a.txt")
|
||||
with open(inside, "w") as fh:
|
||||
fh.write("x")
|
||||
self.assertEqual(resolve_inside(d, "a.txt"), inside)
|
||||
self.assertIsNone(resolve_inside(d, "../outside"))
|
||||
self.assertIsNone(resolve_inside(d, "/etc/passwd"))
|
||||
self.assertEqual(resolve_inside(d, ""), d)
|
||||
|
||||
def test_tmpfs_isolated_chv_and_unknown_fail_closed(self):
|
||||
isolated, _ = tmpfs_isolated("cloud-hypervisor", "/tmp")
|
||||
self.assertFalse(isolated)
|
||||
isolated, _ = tmpfs_isolated("unknown-kind", "/tmp")
|
||||
self.assertFalse(isolated)
|
||||
|
||||
def test_snapshot_url_uses_resolve_inside(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
state = os.path.join(d, "state")
|
||||
os.makedirs(state)
|
||||
path = os.path.join(state, "s.raw")
|
||||
with open(path, "w") as fh:
|
||||
fh.write("x")
|
||||
self.assertEqual(
|
||||
validate_snapshot_url("file://" + path, state), path)
|
||||
with self.assertRaises(Exception):
|
||||
validate_snapshot_url(
|
||||
"file:///etc/passwd", state)
|
||||
|
||||
def test_no_timeout_or_quota_map(self):
|
||||
import cloud.chv.compat215 as m
|
||||
self.assertFalse(hasattr(m, "resolve_module_timeout"))
|
||||
self.assertFalse(hasattr(m, "decide"))
|
||||
|
||||
|
||||
class PoolFacadeTest(unittest.TestCase):
|
||||
def test_facade_only_reconciles_never_acquires(self):
|
||||
# facade 内部构造 WarmPool:用假执行器 + 空 registry,
|
||||
# 只要它不调 acquire/prewarm 就返回 [] 且不炸。
|
||||
ex = FakeExecutor()
|
||||
# WarmPool 构造需要 state_dir 属性;没有则 registry 为空
|
||||
ex.state_dir = tempfile.mkdtemp()
|
||||
ex.warm_pool_size = 0
|
||||
ex.snapshot_url = None
|
||||
result = reconcile_orphans_for_executor(ex)
|
||||
self.assertEqual(result, [])
|
||||
self.assertFalse(ex.acquire_called)
|
||||
self.assertFalse(ex.prewarm_called)
|
||||
|
||||
|
||||
class AuditCounterTest(unittest.TestCase):
|
||||
def test_emit_failure_counts_and_does_not_raise(self):
|
||||
ex = CloudHypervisorExecutor.__new__(CloudHypervisorExecutor)
|
||||
ex.audit_callback = lambda *a, **k: (_ for _ in ()).throw(
|
||||
RuntimeError("db down"))
|
||||
ex.audit_failure_count = 0
|
||||
# _emit 不抛异常(任务不因此中断),计数 +1
|
||||
ex._emit("vm_boot", {"id": "t1"}, "vm=chv-t1")
|
||||
self.assertEqual(ex.audit_failure_count, 1)
|
||||
ex._emit("vm_boot", {"id": "t1"}, "vm=chv-t1")
|
||||
self.assertEqual(ex.audit_failure_count, 2)
|
||||
|
||||
|
||||
class ChvVmDeadFieldTest(unittest.TestCase):
|
||||
def test_only_chv_with_vm_death_reason_returns_true(self):
|
||||
row = _row(executor="cloud-hypervisor",
|
||||
interrupted_reason="VM 中途死亡,任务未完成")
|
||||
d = _task_to_dict(row)
|
||||
self.assertTrue(d.get("chv_vm_dead"))
|
||||
row2 = _row(executor="cloud-hypervisor",
|
||||
interrupted_reason="quota")
|
||||
self.assertNotIn("chv_vm_dead", _task_to_dict(row2))
|
||||
row3 = _row(executor="docker",
|
||||
interrupted_reason="VM 中途死亡,任务未完成")
|
||||
self.assertNotIn("chv_vm_dead", _task_to_dict(row3))
|
||||
row4 = _row(executor="cloud-hypervisor",
|
||||
interrupted_reason=None)
|
||||
self.assertNotIn("chv_vm_dead", _task_to_dict(row4))
|
||||
|
||||
|
||||
class LockfileScriptTest(unittest.TestCase):
|
||||
def _run(self, data, extra=None):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = os.path.join(d, "BUILDINFO.json")
|
||||
with open(p, "w") as fh:
|
||||
json.dump(data, fh)
|
||||
script = str(Path(__file__).resolve().parents[2]
|
||||
/ "chv-guest" / "check_lockfile.py")
|
||||
cmd = [sys.executable, script, p] + (extra or [])
|
||||
return subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
def test_pass_and_empty_and_drift(self):
|
||||
good = {"tag": "t", "versions": {"dpkg": ["a 1"],
|
||||
"pip": ["b==2"]}}
|
||||
self.assertEqual(self._run(good, ["--tag", "t"]).returncode, 0)
|
||||
bad = {"tag": "t", "versions": {"dpkg": [], "pip": []}}
|
||||
self.assertNotEqual(self._run(bad).returncode, 0)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
frozen = os.path.join(d, "frozen.json")
|
||||
with open(frozen, "w") as fh:
|
||||
json.dump({"dpkg": ["missing 9"], "pip": []}, fh)
|
||||
self.assertNotEqual(
|
||||
self._run(good, ["--frozen", frozen]).returncode, 0)
|
||||
|
||||
|
||||
class LongFunctionTest(unittest.TestCase):
|
||||
def test_no_chv_function_over_50_lines(self):
|
||||
import ast
|
||||
base = Path(__file__).resolve().parents[2] / "cloud" / "chv"
|
||||
over = []
|
||||
for f in base.glob("*.py"):
|
||||
tree = ast.parse(f.read_text())
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
n = node.end_lineno - node.lineno + 1
|
||||
if n > 50:
|
||||
over.append("%s:%s:%d" % (f.name, node.name, n))
|
||||
self.assertEqual(over, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -6368,6 +6368,17 @@ class Worker:
|
||||
final_written = self._set_status_if_running(task_id, final)
|
||||
if quota_stop and final_written and final == "interrupted":
|
||||
self._mark_interrupted_reason(task_id, "quota")
|
||||
# feat-chv-arch95plus:VM 死亡写回——执行器 _run_with_vm
|
||||
# 已把 vm_dead/interrupted_reason 放进 result,本处把它
|
||||
# 写进 tasks.interrupted_reason 列,server 的只读字段
|
||||
# chv_vm_dead 才能据此触发;否则该字段在生产里永远
|
||||
# 不出现,前端 VM 死亡横幅就是死代码。只写原因文本,
|
||||
# 不改状态本身(_mark 只在 status='interrupted' 时写)。
|
||||
if final_written and final == "interrupted" \
|
||||
and (result or {}).get("vm_dead"):
|
||||
self._mark_interrupted_reason(
|
||||
task_id, (result or {}).get("interrupted_reason")
|
||||
or "VM 中途死亡")
|
||||
finished_at = time.time()
|
||||
# P0 安全批后:CAS 没改到行(任务已被取消/回收)时
|
||||
# 绝不写回结果,否则迟到的旧进程会覆盖新一轮的结果
|
||||
|
||||
Reference in new issue
Block a user