diff --git a/clashoo/Makefile b/clashoo/Makefile index 0f5072ad..95f8fa26 100644 --- a/clashoo/Makefile +++ b/clashoo/Makefile @@ -1,18 +1,18 @@ include $(TOPDIR)/rules.mk PKG_NAME:=clashoo -PKG_SHORT_SHA:=5019cc0 -PKG_COMMIT_DATE:=2026.09.19 +PKG_SHORT_SHA:=3c947c7 +PKG_COMMIT_DATE:=2026.09.22 PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA) -PKG_RELEASE:=2 +PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src -PKG_SOURCE_VERSION:=5019cc090ed7cafb76643f964a76b1e97aee2985 -PKG_HASH:=52f127d0125a3fb8e17dc7fe48c8f01e831fbc56cf4c86d7674a11c990e71848 +PKG_SOURCE_VERSION:=3c947c76d4f48a938aace935a30a1da1364a1b1b +PKG_HASH:=6f557ac33177ca56ca477e22f4bf4196483bb487105ec45c28a4219522c4e652 PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA) PKG_LICENSE:=GPL3.0+ diff --git a/clashoo/files/etc/init.d/clashoo b/clashoo/files/etc/init.d/clashoo index f4f347f2..b7e25ab9 100755 --- a/clashoo/files/etc/init.d/clashoo +++ b/clashoo/files/etc/init.d/clashoo @@ -6,6 +6,10 @@ START=99 STOP=15 CORE_LOG_PATH="/var/log/clashoo/core.log" +SINGBOX_BIN_DIR="/usr/share/clashoo/bin" +SINGBOX_RUNTIME_DIR="/var/run/clashoo/sing-box" +SINGBOX_RUNTIME_CONFIG="$SINGBOX_RUNTIME_DIR/config.json" +SINGBOX_WORKDIR="$SINGBOX_RUNTIME_DIR/work" . /usr/share/clashoo/runtime/dns_helpers.sh 2>/dev/null || true @@ -67,11 +71,76 @@ _compute_start_wait() { fi } -# Wait up to $1 seconds for $2 (mihomo|sing-box) to be alive AND stable. -# Returns 0 if alive at end of window, 1 if dead/never-started. +selected_singbox_binary() { + local dcore + dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)" + case "$dcore" in + 4) echo "$SINGBOX_BIN_DIR/sing-box-stable" ;; + 5) echo "$SINGBOX_BIN_DIR/sing-box-alpha" ;; + *) return 1 ;; + esac +} + +_migrate_legacy_singbox_binary() { + local dcore target legacy staged + dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)" + target="$(selected_singbox_binary)" || return 1 + [ -e "$target" ] && return 0 + case "$dcore" in + 4) legacy="/usr/bin/sing-box-stable" ;; + 5) legacy="/usr/bin/sing-box-alpha" ;; + *) return 1 ;; + esac + [ -x "$legacy" ] || return 1 + mkdir -p "$SINGBOX_BIN_DIR" >/dev/null 2>&1 || return 1 + staged="${target}.$$" + rm -f "$staged" >/dev/null 2>&1 + cp -f "$legacy" "$staged" >/dev/null 2>&1 || { + rm -f "$staged" >/dev/null 2>&1 + return 1 + } + chmod 0755 "$staged" >/dev/null 2>&1 || { + rm -f "$staged" >/dev/null 2>&1 + return 1 + } + mv -f "$staged" "$target" >/dev/null 2>&1 || { + rm -f "$staged" >/dev/null 2>&1 + return 1 + } + return 0 +} + +_ensure_singbox_binary() { + local target + target="$(selected_singbox_binary)" || return 1 + [ -e "$target" ] || _migrate_legacy_singbox_binary || return 1 + [ -x "$target" ] +} + +_singbox_pid_owns_private_binary() { + local pid stable alpha exe cmdline + pid="$1" + stable="$SINGBOX_BIN_DIR/sing-box-stable" + alpha="$SINGBOX_BIN_DIR/sing-box-alpha" + [ -d "/proc/$pid" ] || return 1 + if command_exists readlink; then + exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null)" + if [ "$exe" = "$stable" ] || [ "$exe" = "$alpha" ]; then + return 0 + fi + fi + cmdline="$(tr '\000' ' ' < "/proc/$pid/cmdline" 2>/dev/null)" + case "$cmdline" in + "$stable"|"$stable "*|"$alpha"|"$alpha "*) return 0 ;; + esac + return 1 +} + _singbox_instance_pid() { - ubus call service list "{\"name\":\"sing-box\",\"verbose\":true}" 2>/dev/null \ - | jsonfilter -e '@["sing-box"].instances.*.pid' 2>/dev/null | head -n1 + local _pid + _pid="$(ubus call service list '{"name":"clashoo","verbose":true}' 2>/dev/null \ + | jsonfilter -e '@.clashoo.instances.singbox.pid' 2>/dev/null | head -n1)" + [ -n "$_pid" ] && _singbox_pid_owns_private_binary "$_pid" && echo "$_pid" } _singbox_running() { @@ -80,20 +149,6 @@ _singbox_running() { [ -n "$_pid" ] && [ -d "/proc/$_pid" ] } -_wait_core_alive() { - local max="${1:-6}" want="${2:-mihomo}" i=0 - while [ "$i" -lt "$max" ]; do - sleep 1 - if [ "$want" = "sing-box" ] || [ "$want" = "singbox" ]; then - _singbox_running && return 0 - else - _core_running && return 0 - fi - i=$((i + 1)) - done - return 1 -} - # Roll back partial state when start failed after backend rules / DNS were applied. # Reuses the same cleanup primitives as stop_service so behavior stays consistent. _rollback_failed_start() { @@ -249,35 +304,46 @@ sync_legacy_core_field() { } prepare_singbox_runtime() { - local active src redir_port tproxy_port mixed_port dns_port + local active src redir_port tproxy_port mixed_port dns_port tmp_config active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)" src="/usr/share/clashoo/config/singbox/${active}" + tmp_config="${SINGBOX_RUNTIME_CONFIG}.tmp.$$" - mkdir -p /etc/sing-box /usr/share/sing-box "$(dirname "$CORE_LOG_PATH")" >/dev/null 2>&1 - rm -f /usr/share/sing-box/cache.db >/dev/null 2>&1 + mkdir -p "$SINGBOX_RUNTIME_DIR" "$SINGBOX_WORKDIR" "$(dirname "$CORE_LOG_PATH")" >/dev/null 2>&1 || return 1 + rm -f "$tmp_config" >/dev/null 2>&1 if [ -n "$active" ] && [ -r "$src" ]; then - cp -f "$src" /etc/sing-box/config.json >/dev/null 2>&1 || return 1 - elif [ ! -r /etc/sing-box/config.json ]; then + cp -f "$src" "$tmp_config" >/dev/null 2>&1 || { + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + elif [ -r "$SINGBOX_RUNTIME_CONFIG" ]; then + cp -f "$SINGBOX_RUNTIME_CONFIG" "$tmp_config" >/dev/null 2>&1 || { + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + else _log_runtime_error "sing-box config.json is missing; please generate or select a sing-box profile first" + rm -f "$tmp_config" >/dev/null 2>&1 return 1 fi - # Core-only: skip clashoo normalize/takeover. Still version-migrate the - # imported config so an older-format config (e.g. from momo/homeproxy or a - # subscription) is accepted by the current sing-box core. The migrate is - # idempotent — a config already in the current format is left unchanged. - if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ]; then - [ -x /usr/bin/ucode ] && ucode /usr/share/clashoo/lib/migrate_singbox.uc /etc/sing-box/config.json >/dev/null 2>&1 - uci -q set sing-box.main.enabled='1' - uci -q set sing-box.main.user='root' - uci -q set sing-box.main.conffile='/etc/sing-box/config.json' - uci -q set sing-box.main.workdir='/usr/share/sing-box' - uci -q commit sing-box >/dev/null 2>&1 - return 0 - fi + command -v ucode >/dev/null 2>&1 || { + _log_runtime_error "ucode is required to prepare sing-box runtime config" + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + # Migrate every imported config, including core-only mode, before any + # Clashoo-specific normalization. The temp file is in the runtime dir so + # a failed migration/normalization never replaces the last good config. + ucode /usr/share/clashoo/lib/migrate_singbox.uc "$tmp_config" >/dev/null 2>&1 || { + _log_runtime_error "sing-box runtime config migration failed" + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } - if command -v ucode >/dev/null 2>&1; then + if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ]; then local has_tun_device dash_port dash_pass + has_tun_device=1 runtime_has_tun_device || has_tun_device=0 redir_port="$(uci -q get clashoo.config.redir_port 2>/dev/null)" @@ -294,44 +360,31 @@ prepare_singbox_runtime() { # routing_mark 6666 must match fw4.sh CORE_ROUTING_MARK (0x1a0a). # Never reuse PROXY_FWMARK (0x162) — it would pull egress to lo -> unreachable. ucode /usr/share/clashoo/lib/normalize_singbox_config.uc \ - /etc/sing-box/config.json "$redir_port" "$tproxy_port" "$mixed_port" "$has_tun_device" "6666" "$dns_port" "$dash_port" "$dash_pass" >/dev/null 2>&1 || { + "$tmp_config" "$redir_port" "$tproxy_port" "$mixed_port" "$has_tun_device" "6666" "$dns_port" "$dash_port" "$dash_pass" >/dev/null 2>&1 || { _log_runtime_error "sing-box runtime config normalization failed" + rm -f "$tmp_config" >/dev/null 2>&1 return 1 } fi - uci -q set sing-box.main.enabled='1' - uci -q set sing-box.main.user='root' - uci -q set sing-box.main.conffile='/etc/sing-box/config.json' - uci -q set sing-box.main.workdir='/usr/share/sing-box' - uci -q commit sing-box >/dev/null 2>&1 + mv -f "$tmp_config" "$SINGBOX_RUNTIME_CONFIG" >/dev/null 2>&1 || { + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } return 0 } -_sync_singbox_binary() { - # Sync /usr/bin/sing-box to the dcore-selected channel (stable/alpha), coexisting via separate paths. - local dcore src - dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)" - case "$dcore" in - 4) src="/usr/bin/sing-box-stable" ;; - 5) src="/usr/bin/sing-box-alpha" ;; - *) return 0 ;; - esac - [ -x "$src" ] || return 0 - [ "$src" -ef /usr/bin/sing-box ] && return 0 - ln -sf "$src" /usr/bin/sing-box -} - start_singbox_service() { - _sync_singbox_binary - if [ ! -x /usr/bin/sing-box ]; then + local bin + bin="$(selected_singbox_binary)" || return 1 + if ! _ensure_singbox_binary; then _log_runtime_error "sing-box core not found, please install the local sing-box binary first" return 1 fi prepare_singbox_runtime || return 1 # preflight dry-run with watchdog: bail on bad config without touching # firewall/dnsmasq, and never let a hung check freeze startup/UI. - _run_with_timeout 15 /usr/bin/sing-box -c /etc/sing-box/config.json check >> "$CORE_LOG_PATH" 2>&1 + _run_with_timeout 15 "$bin" -c "$SINGBOX_RUNTIME_CONFIG" check >> "$CORE_LOG_PATH" 2>&1 _pf_rc=$? if [ "$_pf_rc" -ne 0 ]; then _pf_reason="$(_preflight_reason)" @@ -344,20 +397,29 @@ start_singbox_service() { fi return 1 fi - /etc/init.d/sing-box start >/dev/null 2>&1 + procd_open_instance "singbox" + procd_set_param command "$bin" run -c "$SINGBOX_RUNTIME_CONFIG" -D "$SINGBOX_WORKDIR" + procd_set_param user root + procd_set_param file "$SINGBOX_RUNTIME_CONFIG" + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param limits core="unlimited" + procd_set_param limits nofile="1000000 1000000" + procd_set_param respawn 3600 5 5 + procd_close_instance } stop_singbox_service() { - uci -q set sing-box.main.enabled='0' && uci -q commit sing-box >/dev/null 2>&1 || true - - # Stop via /etc/init.d/sing-box stop (deregisters procd). Never pidof sing-box — co-installed plugins may share the binary. - [ -x /etc/init.d/sing-box ] && /etc/init.d/sing-box stop >/dev/null 2>&1 || true - local _wait=0 _pid + # procd_kill removes the named instance and disables its respawn; signaling + # the child through ubus would let procd recreate it behind our back. + procd_kill clashoo singbox >/dev/null 2>&1 || true + _pid="$(_singbox_instance_pid)" + [ -n "$_pid" ] || return 0 while [ "$_wait" -lt 3 ]; do _pid="$(_singbox_instance_pid)" [ -z "$_pid" ] && return 0 - [ -d "/proc/$_pid" ] || return 0 + _singbox_running || return 0 sleep 1 _wait=$((_wait + 1)) done @@ -1939,9 +2001,11 @@ _finish_geo_bootstrap() { _start_validate_core() { + local core bin core="$(uci -q get clashoo.config.core 2>/dev/null)" if is_singbox_selected; then - if [ ! -x /usr/bin/sing-box ]; then + bin="$(selected_singbox_binary)" + if [ -z "$bin" ] || ! _ensure_singbox_binary; then log_msg "sing-box core not found, please install the local sing-box binary first" "找不到本地 sing-box 内核,请先安装 sing-box 二进制" echo "Clashoo" > "$REAL_LOG" return 1 @@ -2168,10 +2232,6 @@ _start_launch_core() { } boot(){ - # sing-box is managed by Clashoo when selected; keep its own init.d from - # starting independently before Clashoo has prepared config/firewall/DNS. - uci -q set sing-box.main.enabled='0' && uci -q commit sing-box >/dev/null 2>&1 || true - if [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then runtime_state_reset runtime_state_set "health_status" "starting" @@ -2251,15 +2311,27 @@ start_service(){ else log_msg "Core-only mode: skipping firewall/DNS takeover" "仅内核模式:跳过防火墙与 DNS 接管" fi - # Sync-wait: if sing-box dies within window, roll back applied state. - # /etc/init.d/sing-box start forks immediately so we can poll here. - if ! _wait_core_alive "$(_compute_start_wait)" sing-box; then + # procd does not launch the named instance until start_service returns + # and rc.common submits the completed service definition. Watch it in + # the background, just like the mihomo path, then roll back only if the + # registered private instance never becomes healthy. + ( + exec 1000>&- + _bg_max="$(_compute_start_wait)" + _bg_wait=0 + while [ "$_bg_wait" -lt "$_bg_max" ]; do + sleep 1 + if _singbox_running; then + runtime_run_health_check_settle >/dev/null 2>&1 + exit 0 + fi + _bg_wait=$((_bg_wait + 1)) + done _rollback_failed_start - runtime_state_record_health "fail" "start:singbox_died_after_apply" + runtime_state_record_health "fail" "start:singbox_not_running" + echo " $(date "+%Y-%m-%d %H:%M:%S") - clashoo sing-box not detected within ${_bg_max}s, rolled back" >> /usr/share/clashoo/clashoo.txt echo "Clashoo" > "$REAL_LOG" - return 1 - fi - (exec 1000>&-; runtime_run_health_check_settle >/dev/null 2>&1) & + ) & echo "Clashoo" > "$REAL_LOG" return 0 fi @@ -2397,12 +2469,12 @@ reload_service() { if is_singbox_selected; then if _singbox_running; then - resolve_effective_tp_modes - runtime_state_record_preflight "singbox" - prepare_singbox_runtime >/dev/null 2>&1 - /etc/init.d/sing-box restart >/dev/null 2>&1 - singbox_dns_change >/dev/null 2>&1 - runtime_run_health_check_settle >/dev/null 2>&1 + # Use rc.common's transaction wrapper so the complete Clashoo service + # definition is rebuilt before the named instance is registered again. + # This intentionally reapplies Clashoo's own firewall/DNS state; calling + # start_service directly would skip that wrapper and procd setup. + stop_singbox_service >/dev/null 2>&1 + start fi elif _core_running; then resolve_effective_tp_modes diff --git a/clashoo/files/usr/share/clashoo/update/component_update.sh b/clashoo/files/usr/share/clashoo/update/component_update.sh index f3c6e7d1..2ad2eb2a 100755 --- a/clashoo/files/usr/share/clashoo/update/component_update.sh +++ b/clashoo/files/usr/share/clashoo/update/component_update.sh @@ -377,7 +377,6 @@ restore_config_backup() { clashoo_was_running() { /etc/init.d/clashoo status >/dev/null 2>&1 && return 0 - /etc/init.d/sing-box status >/dev/null 2>&1 && return 0 return 1 } diff --git a/clashoo/files/usr/share/clashoo/update/core_download.sh b/clashoo/files/usr/share/clashoo/update/core_download.sh index d38eed3d..69e0f6f0 100755 --- a/clashoo/files/usr/share/clashoo/update/core_download.sh +++ b/clashoo/files/usr/share/clashoo/update/core_download.sh @@ -9,6 +9,11 @@ TARGET_DCORE="$1" CORETYPE=${TARGET_DCORE:-$(uci get clashoo.config.dcore 2>/dev/null)} MIRROR_PREFIX=$(uci get clashoo.config.core_mirror_prefix 2>/dev/null) CUSTOM_CORE_URL=$(uci get clashoo.config.core_download_url 2>/dev/null) +SINGBOX_STABLE_TARGET="${CLASHOO_SINGBOX_STABLE_TARGET:-/usr/share/clashoo/bin/sing-box-stable}" +SINGBOX_ALPHA_TARGET="${CLASHOO_SINGBOX_ALPHA_TARGET:-/usr/share/clashoo/bin/sing-box-alpha}" +SINGBOX_TMP_TEMPLATE="${CLASHOO_SINGBOX_TMP_TEMPLATE:-/tmp/clashoo-singbox.XXXXXX}" +CORE_DOWNLOAD_LOCK_DIR="${CLASHOO_CORE_DOWNLOAD_LOCK_DIR:-/var/lock/clashoo-core-download.lock}" +CORE_DOWNLOAD_LOCK_HELD=0 CORE_INSTALLED=0 CONNECT_TIMEOUT=15 REQUEST_TIMEOUT=30 @@ -24,6 +29,64 @@ write_log() { echo " $(date "+%Y-%m-%d %H:%M:%S") - $1" >> "$LOG_FILE" } +core_download_pid_alive() { + local pid + pid="$1" + [ -d "/proc/$pid" ] && return 0 + kill -0 "$pid" >/dev/null 2>&1 +} + +acquire_core_download_lock() { + local owner parent + parent="$(dirname "$CORE_DOWNLOAD_LOCK_DIR")" + mkdir -p "$parent" >/dev/null 2>&1 || return 1 + while :; do + if mkdir "$CORE_DOWNLOAD_LOCK_DIR" 2>/dev/null; then + printf '%s\n' "$$" > "$CORE_DOWNLOAD_LOCK_DIR/pid" || { + rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1 || true + return 1 + } + CORE_DOWNLOAD_LOCK_HELD=1 + return 0 + fi + + if [ ! -e "$CORE_DOWNLOAD_LOCK_DIR/pid" ]; then + # A fresh owner creates the directory before writing its PID. Only + # remove an actually empty directory; otherwise treat it as busy. + if rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1; then + continue + fi + return 1 + fi + owner="$(sed -n '1p' "$CORE_DOWNLOAD_LOCK_DIR/pid" 2>/dev/null)" + case "$owner" in + '' ) return 1 ;; + *[!0-9]*) + rm -f "$CORE_DOWNLOAD_LOCK_DIR/pid" >/dev/null 2>&1 || return 1 + rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1 || return 1 + ;; + *) + if core_download_pid_alive "$owner"; then + return 1 + fi + rm -f "$CORE_DOWNLOAD_LOCK_DIR/pid" >/dev/null 2>&1 || return 1 + rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1 || return 1 + ;; + esac + done +} + +release_core_download_lock() { + local owner + [ "$CORE_DOWNLOAD_LOCK_HELD" = "1" ] || return 0 + owner="$(sed -n '1p' "$CORE_DOWNLOAD_LOCK_DIR/pid" 2>/dev/null)" + if [ "$owner" = "$$" ]; then + rm -f "$CORE_DOWNLOAD_LOCK_DIR/pid" >/dev/null 2>&1 + rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1 || true + fi + CORE_DOWNLOAD_LOCK_HELD=0 +} + finalize() { if [ "$CORE_INSTALLED" = "1" ]; then if [ -n "$TARGET_DCORE" ]; then @@ -55,6 +118,7 @@ finalize() { fi fi rm -f /var/run/core_update >/dev/null 2>&1 + release_core_download_lock } timed_out() { @@ -120,8 +184,12 @@ fetch_url_try() { # Route core downloads through the running core in kernel-only mode (shared # logic in proxy_lib.sh: handles smart core + sing-box's own profile port). -. /usr/share/clashoo/update/proxy_lib.sh -detect_proxy() { clashoo_detect_proxy; } +if [ "${CLASHOO_CORE_DOWNLOAD_LIB_ONLY:-0}" = "1" ]; then + detect_proxy() { :; } +else + . /usr/share/clashoo/update/proxy_lib.sh + detect_proxy() { clashoo_detect_proxy; } +fi download_file_try() { url="$1" @@ -323,60 +391,163 @@ detect_openwrt_arch() { map_openwrt_arch "$MODELTYPE" } -install_singbox_openwrt_asset() { - local pkg work rootdir +singbox_target_path() { + case "$CORETYPE" in + 5) printf '%s\n' "$SINGBOX_ALPHA_TARGET" ;; + *) printf '%s\n' "$SINGBOX_STABLE_TARGET" ;; + esac +} + +extract_tar_file() { + local archive dest + archive="$1" + dest="$2" + case "$archive" in + *.gz) tar -xzf "$archive" -C "$dest" >/dev/null 2>&1 ;; + *.xz) tar -xJf "$archive" -C "$dest" >/dev/null 2>&1 || tar -xf "$archive" -C "$dest" >/dev/null 2>&1 ;; + *.zst) + if command -v unzstd >/dev/null 2>&1; then + unzstd -c "$archive" 2>/dev/null | tar -xf - -C "$dest" >/dev/null 2>&1 + elif command -v zstd >/dev/null 2>&1; then + zstd -dc "$archive" 2>/dev/null | tar -xf - -C "$dest" >/dev/null 2>&1 + else + return 1 + fi + ;; + *) tar -xf "$archive" -C "$dest" >/dev/null 2>&1 || tar -xzf "$archive" -C "$dest" >/dev/null 2>&1 ;; + esac +} + +extract_ar_data_tar() { + local archive dest offset name size member + archive="$1" + dest="$2" + offset=8 + [ "$(dd if="$archive" bs=8 count=1 2>/dev/null)" = '!' ] || return 1 + while :; do + # Read fixed-width ar fields directly. Do not put the full 60-byte + # header in command substitution: shell command substitution strips + # trailing newlines/spaces and makes length checks unreliable in ash. + name="$(dd if="$archive" bs=1 skip="$offset" count=16 2>/dev/null | sed 's/[[:space:]].*$//')" + name="${name%/}" + size="$(dd if="$archive" bs=1 skip=$((offset + 48)) count=10 2>/dev/null | tr -d '[:space:]')" + case "$size" in + ''|*[!0-9]*) return 1 ;; + esac + member="$dest/$name" + case "$name" in + data.tar.*) + dd if="$archive" of="$member" bs=1 skip=$((offset + 60)) count="$size" 2>/dev/null || return 1 + printf '%s\n' "$member" + return 0 + ;; + esac + offset=$((offset + 60 + size)) + [ $((offset % 2)) -eq 0 ] || offset=$((offset + 1)) + done +} + +extract_singbox_payload() { + local pkg rootdir outer workdir payload data_tar candidate pkg="$1" - work="/tmp/singbox-openwrt-pkg" - rootdir="/tmp/singbox-openwrt-root" - - rm -rf "$work" "$rootdir" 2>/dev/null - mkdir -p "$work" "$rootdir" - - tar -xzf "$pkg" -C "$work" >/dev/null 2>&1 || return 1 - [ -f "$work/data.tar.gz" ] || return 1 - tar -xzf "$work/data.tar.gz" -C "$rootdir" >/dev/null 2>&1 || return 1 - [ -f "$rootdir/usr/bin/sing-box" ] || return 1 - - if ! install_with_rollback "$rootdir/usr/bin/sing-box" "/usr/bin/sing-box"; then + rootdir="$2" + outer="${rootdir}.outer" + workdir="$(dirname "$rootdir")" + payload="$workdir/sing-box.payload" + data_tar="" + rm -f "$payload" 2>/dev/null + rm -rf "$rootdir" "$outer" 2>/dev/null + if ! mkdir -p "$rootdir" "$outer"; then + rm -rf "$rootdir" "$outer" 2>/dev/null return 1 fi - if [ -f "$rootdir/etc/init.d/sing-box" ]; then - cp -f "$rootdir/etc/init.d/sing-box" /etc/init.d/sing-box >/dev/null 2>&1 || true - chmod 755 /etc/init.d/sing-box >/dev/null 2>&1 || true + # IPK assets are ar/tar containers with a compressed data.tar.* member; + # official fallback archives contain sing-box directly below a version dir. + if tar -tf "$pkg" 2>/dev/null | grep -Eq '(^|/)data\.tar\.(gz|xz|zst)$'; then + extract_tar_file "$pkg" "$outer" || { + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + } + for candidate in "$outer"/data.tar.*; do + [ -f "$candidate" ] || continue + data_tar="$candidate" + break + done + if [ -z "$data_tar" ] || ! extract_tar_file "$data_tar" "$rootdir"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + else + data_tar="$(extract_ar_data_tar "$pkg" "$outer" 2>/dev/null || true)" + if [ -n "$data_tar" ]; then + if ! extract_tar_file "$data_tar" "$rootdir"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + else + if ! extract_tar_file "$pkg" "$rootdir"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + fi fi - if [ -f "$rootdir/etc/config/sing-box" ] && [ ! -f /etc/config/sing-box ]; then - cp -f "$rootdir/etc/config/sing-box" /etc/config/sing-box >/dev/null 2>&1 || true + candidate="$(find "$rootdir" -type f -name sing-box 2>/dev/null | head -n 1)" + if [ -n "$candidate" ] && [ -f "$candidate" ]; then + if ! cp -f "$candidate" "$payload"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + rm -rf "$rootdir" "$outer" 2>/dev/null + printf '%s\n' "$payload" + return 0 fi - - if [ -f "$rootdir/etc/sing-box/config.json" ] && [ ! -f /etc/sing-box/config.json ]; then - mkdir -p /etc/sing-box >/dev/null 2>&1 - cp -f "$rootdir/etc/sing-box/config.json" /etc/sing-box/config.json >/dev/null 2>&1 || true - fi - - rm -rf "$work" "$rootdir" >/dev/null 2>&1 - return 0 + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 } -install_singbox_openwrt_apk_asset() { - local pkg +cleanup_singbox_extract_workdir() { + local workdir rootdir + workdir="$1" + rootdir="$workdir/root" + rm -rf "$rootdir" "${rootdir}.outer" "$workdir" 2>/dev/null +} + +install_singbox_openwrt_asset() { + local pkg target workdir rootdir bin extract_rc rc pkg="$1" - command -v apk >/dev/null 2>&1 || return 1 - apk add --allow-untrusted --force-overwrite "$pkg" >/dev/null 2>&1 || return 1 - [ -x /usr/bin/sing-box ] || return 1 - return 0 + target="${2:-$(singbox_target_path)}" + workdir="$(mktemp -d "$SINGBOX_TMP_TEMPLATE" 2>/dev/null)" || return 1 + rootdir="$workdir/root" + bin="$(extract_singbox_payload "$pkg" "$rootdir")" + extract_rc=$? + if [ "$extract_rc" -eq 0 ]; then + install_with_rollback "$bin" "$target" "${VER:-}" + rc=$? + else + rc=1 + fi + cleanup_singbox_extract_workdir "$workdir" + return "$rc" } install_singbox_tar_asset() { - local archive bin + local archive target workdir rootdir bin extract_rc rc archive="$1" - rm -rf /tmp/singbox-extract >/dev/null 2>&1 - mkdir -p /tmp/singbox-extract - tar -xzf "$archive" -C /tmp/singbox-extract >/dev/null 2>&1 || return 1 - bin="$(find /tmp/singbox-extract -name 'sing-box' -type f 2>/dev/null | head -n 1)" - [ -n "$bin" ] || return 1 - install_with_rollback "$bin" "/usr/bin/sing-box" + target="${2:-$(singbox_target_path)}" + workdir="$(mktemp -d "$SINGBOX_TMP_TEMPLATE" 2>/dev/null)" || return 1 + rootdir="$workdir/root" + bin="$(extract_singbox_payload "$archive" "$rootdir")" + extract_rc=$? + if [ "$extract_rc" -eq 0 ]; then + install_with_rollback "$bin" "$target" "${VER:-}" + rc=$? + else + rc=1 + fi + cleanup_singbox_extract_workdir "$workdir" + return "$rc" } fetch_latest_tag() { @@ -756,12 +927,25 @@ pick_mihomo_asset() { } install_binary() { + local src dst dir staged src="$1" dst="$2" - mkdir -p "$(dirname "$dst")" - rm -f "$dst" - mv "$src" "$dst" - chmod 755 "$dst" + dir="$(dirname "$dst")" + staged="$dir/.$(basename "$dst").tmp.$$" + mkdir -p "$dir" || return 1 + rm -f "$staged" + cp -f "$src" "$staged" || { + rm -f "$staged" + return 1 + } + chmod 755 "$staged" || { + rm -f "$staged" + return 1 + } + mv -f "$staged" "$dst" || { + rm -f "$staged" + return 1 + } } backup_binary() { @@ -785,6 +969,7 @@ restore_binary() { } verify_binary() { + local bin bin="$1" [ -x "$bin" ] || return 1 "$bin" -v >/dev/null 2>&1 && return 0 @@ -792,6 +977,18 @@ verify_binary() { return 1 } +verify_binary_version() { + local bin expected output actual + bin="$1" + expected="$2" + [ -n "$expected" ] || return 0 + output="$({ "$bin" version 2>/dev/null || "$bin" -v 2>/dev/null; } || true)" + [ -n "$output" ] || return 1 + actual="$(printf '%s\n' "$output" | sed -n 's/^[[:space:]]*sing-box[[:space:]][[:space:]]*version[[:space:]][[:space:]]*\([^[:space:]][^[:space:]]*\).*/\1/p' | head -n 1)" + [ -n "$actual" ] || return 1 + [ "$actual" = "$expected" ] +} + installed_version_matches() { version_file="$1" expected_version="$2" @@ -801,11 +998,13 @@ installed_version_matches() { } install_with_rollback() { + local tmpfile target expected_version tmpfile="$1" target="$2" + expected_version="${3:-}" chmod 755 "$tmpfile" 2>/dev/null - if ! verify_binary "$tmpfile"; then + if ! verify_binary "$tmpfile" || ! verify_binary_version "$tmpfile" "$expected_version"; then write_log "新内核预热校验失败,保留当前内核" rm -f "$tmpfile" 2>/dev/null return 1 @@ -822,6 +1021,18 @@ install_with_rollback() { return 0 } +# Unit tests source the extraction helpers without entering the normal UCI and +# network download path. This hook has no effect in production execution. +if [ "${CLASHOO_CORE_DOWNLOAD_LIB_ONLY:-0}" = "1" ]; then + return 0 2>/dev/null || exit 0 +fi + +if ! acquire_core_download_lock; then + write_log "已有内核下载任务运行,拒绝并发执行" + exit 1 +fi +trap finalize EXIT + # UCI template defaults to amd64-compatible. Auto-detect when MODELTYPE # is missing, or when MODELTYPE is an amd64 variant but the device is not. if [ -z "$MODELTYPE" ]; then @@ -844,7 +1055,6 @@ fi rm -f /tmp/clash.gz /tmp/clash /usr/share/clashoo/core_down_complete 2>/dev/null touch /var/run/core_update 2>/dev/null -trap finalize EXIT write_log "内核下载任务启动" write_log "下载架构:${MODELTYPE:-未设置}" @@ -898,28 +1108,17 @@ if [ "$CORETYPE" = "4" ] || [ "$CORETYPE" = "5" ]; then [ -z "$TAG" ] && write_log "获取 sing-box 预发布版版本号失败" && exit 1 fi VER="${TAG#v}" + SINGBOX_TARGET="$(singbox_target_path)" installed=0 if [ -n "$OPENWRT_ARCH" ]; then - if command -v apk >/dev/null 2>&1; then - ASSET="sing-box_${VER}_openwrt_${OPENWRT_ARCH}.apk" - URL="https://github.com/SagerNet/sing-box/releases/download/${TAG}/${ASSET}" - write_log "优先尝试 OpenWrt APK 包: ${ASSET}" - if download_with_mirrors "$URL" /tmp/singbox-openwrt.apk raw && install_singbox_openwrt_apk_asset /tmp/singbox-openwrt.apk; then - installed=1 - else - write_log "OpenWrt APK 包安装失败,尝试 OpenWrt IPK 包" - fi - fi - if [ "$installed" -ne 1 ]; then - ASSET="sing-box_${VER}_openwrt_${OPENWRT_ARCH}.ipk" - URL="https://github.com/SagerNet/sing-box/releases/download/${TAG}/${ASSET}" - write_log "尝试 OpenWrt IPK 包: ${ASSET}" - if download_with_mirrors "$URL" /tmp/singbox-openwrt.ipk raw && install_singbox_openwrt_asset /tmp/singbox-openwrt.ipk; then - installed=1 - else - write_log "OpenWrt IPK 包安装失败,尝试 tar 包兜底" - fi + ASSET="sing-box_${VER}_openwrt_${OPENWRT_ARCH}.ipk" + URL="https://github.com/SagerNet/sing-box/releases/download/${TAG}/${ASSET}" + write_log "尝试提取 OpenWrt IPK 包: ${ASSET}" + if download_with_mirrors "$URL" /tmp/singbox-openwrt.ipk raw && install_singbox_openwrt_asset /tmp/singbox-openwrt.ipk "$SINGBOX_TARGET"; then + installed=1 + else + write_log "OpenWrt IPK 提取失败,尝试官方 musl tar 包" fi fi @@ -927,7 +1126,7 @@ if [ "$CORETYPE" = "4" ] || [ "$CORETYPE" = "5" ]; then for ASSET in "sing-box-${VER}-linux-${SINGBOX_ARCH}-musl.tar.gz" "sing-box-${VER}-linux-${SINGBOX_ARCH}.tar.gz"; do URL="https://github.com/SagerNet/sing-box/releases/download/${TAG}/${ASSET}" write_log "尝试下载 sing-box 归档包: ${ASSET}" - if download_with_mirrors "$URL" /tmp/singbox.tar.gz gzip && install_singbox_tar_asset /tmp/singbox.tar.gz; then + if download_with_mirrors "$URL" /tmp/singbox.tar.gz gzip && install_singbox_tar_asset /tmp/singbox.tar.gz "$SINGBOX_TARGET"; then installed=1 break fi @@ -940,16 +1139,14 @@ if [ "$CORETYPE" = "4" ] || [ "$CORETYPE" = "5" ]; then fi CORE_INSTALLED=1 - # 稳定版与 Alpha 分路径归档,避免互相覆盖;同时保留旧字段供老逻辑读 + # 稳定版与 Alpha 分路径保存,避免互相覆盖;版本文件保持兼容。 if [ "$CORETYPE" = "4" ]; then - cp -f /usr/bin/sing-box /usr/bin/sing-box-stable 2>/dev/null printf '%s\n' "$TAG" > "/usr/share/clashoo/singbox_stable_version" else - cp -f /usr/bin/sing-box /usr/bin/sing-box-alpha 2>/dev/null printf '%s\n' "$TAG" > "/usr/share/clashoo/singbox_alpha_version" fi printf '%s\n' "$TAG" > "/usr/share/clashoo/singbox_version" - rm -f /tmp/singbox.tar.gz /tmp/singbox-openwrt.ipk /tmp/singbox-openwrt.apk + rm -f /tmp/singbox.tar.gz /tmp/singbox-openwrt.ipk rm -rf /tmp/singbox-extract /tmp/singbox-openwrt-pkg /tmp/singbox-openwrt-root touch /usr/share/clashoo/core_down_complete write_log "sing-box 更新完成: $TAG" diff --git a/clashoo/files/usr/share/clashoo/update/proxy_lib.sh b/clashoo/files/usr/share/clashoo/update/proxy_lib.sh index 5070958e..e9664420 100644 --- a/clashoo/files/usr/share/clashoo/update/proxy_lib.sh +++ b/clashoo/files/usr/share/clashoo/update/proxy_lib.sh @@ -1,4 +1,8 @@ # shellcheck shell=sh +# Clashoo's sing-box is private; HomeProxy owns the public system binary. +CLASHOO_SINGBOX_BIN_DIR="/usr/share/clashoo/bin" +CLASHOO_SINGBOX_RUNTIME_CONFIG="/var/run/clashoo/sing-box/config.json" + # Shared proxy detection for clashoo's own outbound requests. # # clashoo's self-initiated downloads (update checks, component/panel/core/geoip @@ -16,7 +20,7 @@ # Port source depends on the running Clashoo kernel. The liveness gate uses the # procd owner instead of a binary name, because other proxy plugins may run the # same binaries: -# sing-box -> /etc/sing-box/config.json mixed inbound +# sing-box -> private runtime config mixed inbound # (an imported profile may carry its own port) # mihomo / clash-meta / smart -> /etc/clashoo/config.yaml mixed-port # (a custom config's port may differ from uci) @@ -25,11 +29,8 @@ clashoo_running_core_type() { _cdp_type="$(uci -q get clashoo.config.core_type 2>/dev/null)" case "$_cdp_type" in singbox) - # Clashoo owns this service only after it has configured the - # packaged sing-box init script for its own runtime config. - [ "$(uci -q get sing-box.main.conffile 2>/dev/null)" = "/etc/sing-box/config.json" ] || return 1 - _cdp_service="sing-box" - _cdp_filter='@["sing-box"].instances.*.pid' + _cdp_service="clashoo" + _cdp_filter='@.clashoo.instances.singbox.pid' ;; *) _cdp_type="mihomo" @@ -41,6 +42,20 @@ clashoo_running_core_type() { _cdp_pid="$(ubus call service list "{\"name\":\"$_cdp_service\",\"verbose\":true}" 2>/dev/null \ | jsonfilter -e "$_cdp_filter" 2>/dev/null | head -n1)" [ -n "$_cdp_pid" ] && [ -d "/proc/$_cdp_pid" ] || return 1 + if [ "$_cdp_type" = "singbox" ]; then + local _cdp_exe _cdp_cmdline + _cdp_exe="$(readlink -f "/proc/$_cdp_pid/exe" 2>/dev/null)" + case "$_cdp_exe" in + "$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha") ;; + *) + _cdp_cmdline="$(tr '\000' ' ' < "/proc/$_cdp_pid/cmdline" 2>/dev/null)" + case "$_cdp_cmdline" in + "$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable "*|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha "*) ;; + *) return 1 ;; + esac + ;; + esac + fi printf '%s' "$_cdp_type" } @@ -48,7 +63,7 @@ clashoo_detect_proxy() { _cdp_type="$(clashoo_running_core_type)" || return 0 _cdp_port="" if [ "$_cdp_type" = "singbox" ]; then - _cdp_port="$(jsonfilter -i /etc/sing-box/config.json \ + _cdp_port="$(jsonfilter -i "$CLASHOO_SINGBOX_RUNTIME_CONFIG" \ -e '@.inbounds[@.type="mixed"].listen_port' 2>/dev/null | head -n 1)" else # prefer mixed-port (HTTP+SOCKS) over plain port; a single two-pattern diff --git a/clashoo/files/usr/share/clashoo/update/update_china_ip.sh b/clashoo/files/usr/share/clashoo/update/update_china_ip.sh index 766651b6..3797e8f8 100755 --- a/clashoo/files/usr/share/clashoo/update/update_china_ip.sh +++ b/clashoo/files/usr/share/clashoo/update/update_china_ip.sh @@ -7,8 +7,8 @@ NFT_DIR="${NFT_DIR:-/usr/share/clashoo/nftables}" TARGET_V4="${NFT_DIR}/geoip_cn.nft" TARGET_V6="${NFT_DIR}/geoip6_cn.nft" FW4_SCRIPT="${FW4_SCRIPT:-/usr/share/clashoo/net/fw4.sh}" -RUNTIME_STATE_FILE="${RUNTIME_STATE_FILE:-/tmp/clashoo/runtime_state}" PROC_ROOT="${PROC_ROOT:-/proc}" +SINGBOX_BIN_DIR="${SINGBOX_BIN_DIR:-/usr/share/clashoo/bin}" TMP_V4="/tmp/china_ip.txt.$$" TMP_V6="/tmp/china_ipv6.txt.$$" OUT_V4="/tmp/geoip_cn.nft.$$" @@ -136,29 +136,43 @@ else fi instance_pid() { - ubus call service list "{\"name\":\"$1\",\"verbose\":true}" 2>/dev/null | - jsonfilter -e "@[\"$1\"].instances.*.pid" 2>/dev/null | head -n1 + local service="$1" + local instance="${2:-}" + local filter + if [ -n "$instance" ]; then + filter="@.${service}.instances.${instance}.pid" + else + filter="@[\"${service}\"].instances.*.pid" + fi + ubus call service list "{\"name\":\"$service\",\"verbose\":true}" 2>/dev/null | + jsonfilter -e "$filter" 2>/dev/null | head -n1 } pid_running() { [ -n "$1" ] && [ -d "$PROC_ROOT/$1" ] } -clashoo_manages_singbox() { - [ -r "$RUNTIME_STATE_FILE" ] || return 1 - grep -qx 'configured_core=singbox' "$RUNTIME_STATE_FILE" 2>/dev/null || return 1 - case "$(sed -n 's/^health_detail=//p' "$RUNTIME_STATE_FILE" 2>/dev/null | head -n1)" in - service_stopped|service_disabled|boot_disabled|preflight:*|start:*) return 1 ;; +singbox_pid_owns_private_binary() { + local pid="$1" exe cmdline + pid_running "$pid" || return 1 + exe="$(readlink -f "$PROC_ROOT/$pid/exe" 2>/dev/null)" + case "$exe" in + "$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-alpha") return 0 ;; esac - return 0 + cmdline="$(tr '\000' ' ' < "$PROC_ROOT/$pid/cmdline" 2>/dev/null)" + case "$cmdline" in + "$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-stable "*|"$SINGBOX_BIN_DIR/sing-box-alpha"|"$SINGBOX_BIN_DIR/sing-box-alpha "*) return 0 ;; + esac + return 1 } selected_core_running() { local name pid if [ "$(uci -q get clashoo.config.core_type 2>/dev/null)" = "singbox" ]; then - bool_enabled "$(uci -q get sing-box.main.enabled 2>/dev/null)" || return 1 - clashoo_manages_singbox || return 1 - name="sing-box" + name="clashoo" + pid="$(instance_pid "$name" singbox)" + singbox_pid_owns_private_binary "$pid" + return $? else name="clashoo" fi diff --git a/luci-app-baidudrive/Makefile b/luci-app-baidudrive/Makefile index 83e4fa83..ac36127f 100644 --- a/luci-app-baidudrive/Makefile +++ b/luci-app-baidudrive/Makefile @@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=baidudrive PKG_VERSION:=1.0.2 -PKG_RELEASE:=1 -LUCI_DEPENDS:=+baidudrive +luci-compat +PKG_RELEASE:=2 +LUCI_DEPENDS:=+baidudrive +luci-compat +luci-lib-linkeaseauth LUCI_MINIFY_CSS:=0 LUCI_MINIFY_JS:=0 diff --git a/luci-app-baidudrive/luasrc/controller/baidudrive.lua b/luci-app-baidudrive/luasrc/controller/baidudrive.lua index 5a18a97b..120f58fe 100644 --- a/luci-app-baidudrive/luasrc/controller/baidudrive.lua +++ b/luci-app-baidudrive/luasrc/controller/baidudrive.lua @@ -1,29 +1,39 @@ -local http = require "luci.http" - module("luci.controller.baidudrive", package.seeall) function index() - if not nixio.fs.access("/etc/config/baidudrive") then - return - end - local page - page = entry({"admin", "services", "baidudrive"}, cbi("baidudrive"), _("BaiduDrive"), 100) - page.dependent = true + if not nixio.fs.access("/etc/config/baidudrive") then return end + entry({"admin", "services", "baidudrive"}, cbi("baidudrive"), _("BaiduDrive"), 100).dependent = true entry({"admin", "services", "baidudrive_status"}, call("baidudrive_status")) + + local open = entry({"admin", "services", "baidudrive", "open"}, call("baidudrive_open")) + open.leaf = true + open.dependent = false + open.sysauth = "root" + open.sysauth_authenticator = "htmlauth" +end + +local function compat() + local dispatcher = require "luci.dispatcher" + return require("luci.model.linkease.apps_compat").new({ + http = require "luci.http", + resolver = require("luci.model.linkease.apps_openwrt").new(), + auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth") + }) end function baidudrive_status() local sys = require "luci.sys" - local uci = require "luci.model.uci".cursor() - local port = uci:get_first("baidudrive", "baidudrive", "port") or "10780" - local app_running = (sys.call("pidof baidudrive >/dev/null") == 0) - local sdk_running = (sys.call("pidof baiduNas >/dev/null") == 0) - local status = { + local uci = require("luci.model.uci").cursor() + local app_running = sys.call("pidof baidudrive >/dev/null") == 0 + local sdk_running = sys.call("pidof baiduNas >/dev/null") == 0 + compat():legacy_status("baidudrive", { running = app_running and sdk_running, app_running = app_running, sdk_running = sdk_running, - port = port - } - luci.http.prepare_content("application/json") - luci.http.write_json(status) + port = uci:get_first("baidudrive", "baidudrive", "port") or "10780" + }) +end + +function baidudrive_open() + compat():open("baidudrive") end diff --git a/luci-app-chatgpt-web/.github/workflows/main.yml b/luci-app-chatgpt-web/.github/workflows/main.yml new file mode 100644 index 00000000..3de4bc6d --- /dev/null +++ b/luci-app-chatgpt-web/.github/workflows/main.yml @@ -0,0 +1,49 @@ +name: Build + +on: + push: + tags: + - v* + +jobs: + build: + name: Build ${{ matrix.arch }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + arch: + + - aarch64_generic + - i386_pentium-mmx + - loongarch64_generic + - mips_mips32 + - mipsel_24kc + - mipsel_mips32 + - x86_64 + + steps: + - uses: actions/checkout@main + with: + fetch-depth: 0 + - name: Building packages + uses: sbwml/openwrt-gh-action-sdk@go1.24 + env: + ARCH: ${{ matrix.arch }}-openwrt-24.10 + FEEDNAME: packages_ci + PACKAGES: luci-app-chatgpt-web + NO_REFRESH_CHECK: true + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.arch }} + path: bin/packages/${{ matrix.arch }}/packages_ci/*.ipk + + - name: Upload packages + uses: ncipollo/release-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + allowUpdates: true + replacesArtifacts: true + artifacts: "bin/packages/${{ matrix.arch }}/packages_ci/*.ipk" diff --git a/luci-app-chatgpt-web/Makefile b/luci-app-chatgpt-web/Makefile new file mode 100644 index 00000000..b5cae97b --- /dev/null +++ b/luci-app-chatgpt-web/Makefile @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-3.0-only +# +# Copyright (C) 2023-2025 sirpdboy https://github.com/sirpdboy/luci-app-chatgpt-web +# +# This is free software, licensed under the Apache License, Version 2.0 . +# +include $(TOPDIR)/rules.mk + +NAME:=chatgpt-web +PKG_NAME:=luci-app-$(NAME) +PKG_VERSION:=1.2.1 +PKG_RELEASE:=16 + +LUCI_TITLE:=LuCI Support for chatgpt-web Client +LUCI_PKGARCH:=all + +define Package/$(PKG_NAME)/conffiles +/etc/config/chatgpt-web +endef +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-chatgpt-web/README.md b/luci-app-chatgpt-web/README.md new file mode 100644 index 00000000..ab5de559 --- /dev/null +++ b/luci-app-chatgpt-web/README.md @@ -0,0 +1,179 @@ +## 访问数:![hello](https://views.whatilearened.today/views/github/sirpdboy/deplives.svg)[![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) + + + +

+
luci-app-chatgpt-web
+

+ +

+OpenWrt +Chrome +Safari +Firefox + GitHub release +Downloads +

+ + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg) + +请 **认真阅读完毕** 本页面,本页面包含注意事项和如何使用。 + +## 功能说明 + +luci-app-chatgpt Pure ChatGPT by OPENWRT based on OpenAI API + +luci-app-chatgpt 在OPENWRT上实现ChatGPT项目,基于OpenAI API + + + +- [luci-app-chatgpt](#功能说明) + - [特性](#特性) + - [编译方法](#编译方法) + - [使用方法](#使用方法) + - [自定义选项](#自定义选项)。 + - [说明](#说明) + - [界面](#界面) + - [捐助](#捐助) + + + +## 特性 + +- 无须第三依赖,直接使用官方API接口。 +- 支持复制/更新/刷新会话,语音输入,朗读等功能,以及众多[自定义选项](#自定义选项)。 +- 支持搜索会话,导入导出会话和设置,自定义头像,自定义系统角色等。 + +参考项目: +[markdown-it](https://github.com/markdown-it/markdown-it), +[highlight.js](https://github.com/highlightjs/highlight.js), +[github-markdown-css](https://github.com/sindresorhus/github-markdown-css), +[chatgpt-html](https://github.com/slippersheepig/chatgpt-html), +[markdown-it-copy](https://github.com/ReAlign/markdown-it-copy), +[markdown-it-texmath](https://github.com/goessner/markdown-it-texmath), +[awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) + + +## 编译方法 + +- 将luci-app-chatgpt添加至 LEDE/OpenWRT 源码的方法。 + +### 下载源码方法: + + ```Brach + + # 下载源码 + + git clone https://github.com/sirpdboy/chatgpt-web.git package/luci-app-chatgpt + make menuconfig + + ``` +### 配置菜单 + + ```Brach + make menuconfig + # 找到 LuCI -> Applications, 选择 luci-app-chatgpt, 保存后退出。 + ``` + +### 编译 + + ```Brach + # 编译固件 + make package/luci-app-chatgpt/compile V=s + ``` + +## 使用方法 + + - 打开luci-app-chatgpt基本设置,填写API密钥、GPT模型、填写OpenAI接口、选择用户图像等。 + + - 可选GPT模型,默认gpt-3.5,当前使用gpt-4模型需通过openai的表单申请。 + + - OpenAI接口当本地可正常访问`api.openai.com`,填写`https://api.openai.com/` + + - 不可正常访问`api.openai.com`,填写其反代地址,注意:反代接口响应需添加跨域Header `Access-Control-Allow-Origin` + + - chatgpt-web页面就可以正常使用了,如果需要设置更多请看 自定义设置设置语音和系统角色等。 + +## 自定义选项 + +- 左边栏支持,清除对话,搜索会话,新建/重命名/删除(会话/文件夹)导出/导入/重置会话和设置数据,显示本地存储。 + +- 可选系统角色,默认不开启,有四个预设角色,后期会加入更多角色。 + +- 可选角色性格,默认灵活创新,对应接口文档的top_p参数。 + +- 可选回答质量,默认平衡,对应接口文档的temperature参数。 + +- 修改打字机速度,默认较快,值越大速度越快。 + +- 允许连续对话,默认开启,对话中包含上下文信息,会导致api费用增加。 + +- 允许长回复,默认关闭,**开启后可能导致api费用增加,并丢失大部分上下文,对于一些要发送`继续`才完整的回复,不用发`继续`了。** + +- 选择语音,默认Bing语音,支持Azure语音和系统语音,可分开设置提问语音和回答语音。 + +- 音量,默认最大。 + +- 语速,默认正常。 + +- 音调,默认正常。 + +- 允许连续朗读,默认开启,连续郎读到所有对话结束。 + +- 允许自动朗读,默认关闭,自动朗读新的回答。**(iOS需打开设置-自动播放视频预览,Mac上Safari需打开此网站的设置-允许全部自动播放)** + +- 支持语音输入,默认识别为普通话,可长按语音按钮修改识别选项。**语音识别必需条件:使用chrome内核系浏览器 + https网页或本地网页。** 如点击语音按钮没反应,可能是未授予麦克风权限或者没安装麦克风设备。 + + +## 说明 + +-源码来源:https://github.com/sirpdboy/chatgpt-web +-你可以随意使用其中的源码,但请注明出处。 + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg) + +## 界面 + +![screenshots](./doc/chatgpt1.png) + +![screenshots](./doc/chatgpt2.png) + +![screenshots](./doc/chatgpt3.png) + + +## 使用与授权相关说明 + +- 本人开源的所有源码,任何引用需注明本处出处,如需修改二次发布必告之本人,未经许可不得做于任何商用用途。 + +# My other project + +- 路由安全看门狗 :https://github.com/sirpdboy/luci-app-watchdog +- 网络速度测试 :https://github.com/sirpdboy/luci-app-netspeedtest +- 计划任务插件(原定时设置) : https://github.com/sirpdboy/luci-app-taskplan +- 关机功能插件 : https://github.com/sirpdboy/luci-app-poweroffdevice +- opentopd主题 : https://github.com/sirpdboy/luci-theme-opentopd +- kucat酷猫主题: https://github.com/sirpdboy/luci-theme-kucat +- kucat酷猫主题设置工具: https://github.com/sirpdboy/luci-app-kucat-config +- NFT版上网时间控制插件: https://github.com/sirpdboy/luci-app-timecontrol +- 家长控制: https://github.com/sirpdboy/luci-theme-parentcontrol +- 定时限速: https://github.com/sirpdboy/luci-app-eqosplus +- 系统高级设置 : https://github.com/sirpdboy/luci-app-advanced +- ddns-go动态域名: https://github.com/sirpdboy/luci-app-ddns-go +- 进阶设置(系统高级设置+主题设置kucat/agron/opentopd): https://github.com/sirpdboy/luci-app-advancedplus +- 网络设置向导: https://github.com/sirpdboy/luci-app-netwizard +- 一键分区扩容: https://github.com/sirpdboy/luci-app-partexp +- lukcy大吉: https://github.com/sirpdboy/luci-app-lukcy + +## 捐助 + +![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明3.jpg) + +| 图飞了😂 | 图飞了😂 | +| :-----------------: | :-------------: | +|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | + + + 图飞了😂 + + diff --git a/luci-app-chatgpt-web/doc/chatgpt1.png b/luci-app-chatgpt-web/doc/chatgpt1.png new file mode 100644 index 00000000..f715b006 Binary files /dev/null and b/luci-app-chatgpt-web/doc/chatgpt1.png differ diff --git a/luci-app-chatgpt-web/doc/chatgpt2.png b/luci-app-chatgpt-web/doc/chatgpt2.png new file mode 100644 index 00000000..7de824a2 Binary files /dev/null and b/luci-app-chatgpt-web/doc/chatgpt2.png differ diff --git a/luci-app-chatgpt-web/doc/chatgpt3.png b/luci-app-chatgpt-web/doc/chatgpt3.png new file mode 100644 index 00000000..9d280941 Binary files /dev/null and b/luci-app-chatgpt-web/doc/chatgpt3.png differ diff --git a/luci-app-chatgpt-web/luasrc/controller/chatgpt-web.lua b/luci-app-chatgpt-web/luasrc/controller/chatgpt-web.lua new file mode 100644 index 00000000..c100132c --- /dev/null +++ b/luci-app-chatgpt-web/luasrc/controller/chatgpt-web.lua @@ -0,0 +1,17 @@ +-- Copyright (C) 2023-2025 sirpdboy https://github.com/sirpdboy/luci-app-chatgpt-web +-- Licensed to the public under the Apache License 2.0. + +module("luci.controller.chatgpt-web", package.seeall) + +function index() + if not nixio.fs.access("/etc/config/chatgpt-web") then + return + end + + local e = entry({"admin", "services", "chatgpt-web"}, alias("admin", "services", "chatgpt-web", "setting"),_("Chatgpt Web"), 20) + e.dependent=false + e.acl_depends = { "luci-app-chatgpt-web" } + entry({"admin", "services", "chatgpt-web", "setting"}, cbi("chatgpt-web"), _("Base Setting"), 10).leaf=true + entry({"admin", "services", "chatgpt-web", "chatgpt-web"}, cbi("chatgpt",{hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}), _("Chatgpt Web"), 30).leaf=true +end + diff --git a/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt-web.lua b/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt-web.lua new file mode 100644 index 00000000..38f5e7da --- /dev/null +++ b/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt-web.lua @@ -0,0 +1,36 @@ +-- Copyright (C) 2023-2025 sirpdboy https://github.com/sirpdboy/https://github.com/sirpdboy/luci-app-chatgpt-web + +local m, s ,o + +m = Map("chatgpt-web") +m.title = translate("Chatgpt Web") +m.description = translate("ChatGPT-Web is the version used on OPENWRT of OPENAI")..translate("
For specific usage, see:")..translate("GitHub @sirpdboy/chatgpt-web\ ") + +s = m:section(TypedSection, "basic", translate("Base Setting")) +s.addremove = false +s.anonymous = true + + +o = s:option(Value,"userpic",translate("Character images")) +o:value("girl.jpg",translate("girl")) +o:value("girl2.jpg",translate("girl2")) +o:value("girl3.jpg",translate("girl3")) +o:value("boy.jpg",translate("boy")) +o:value("boy2.jpg",translate("boy2")) +o:value("boy3.jpg",translate("boy3")) +o:value("dog.jpg",translate("dog")) +o:value("cartoon.jpg",translate("cartoon")) +o:value("cartoonboy.jpg",translate("cartoonboy")) +o:value("cartoonboy2.jpg",translate("cartoonboy2")) +o:value("cartoongirl.jpg",translate("cartoongirl")) +o:value("cartoongirl2.jpg",translate("cartoongirl2")) +o.rmempty = true +o.default="girl.jpg" + + +o=s:option(TextValue, "systemrole" ,translate("Default Role Settings")) +o.rows = 5 +o.rmempty=false + + +return m diff --git a/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt.lua b/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt.lua new file mode 100644 index 00000000..162de6d0 --- /dev/null +++ b/luci-app-chatgpt-web/luasrc/model/cbi/chatgpt.lua @@ -0,0 +1,9 @@ +-- Copyright (C) 2023-2025 sirpdboy https://github.com/sirpdboy/luci-app-chatgpt-web +require("luci.util") +local m, s + +m = Map("chatgpt-web",translate("")) + +m:section(SimpleSection).template = "chatgpt-web" + +return m diff --git a/luci-app-chatgpt-web/luasrc/view/chatgpt-web.htm b/luci-app-chatgpt-web/luasrc/view/chatgpt-web.htm new file mode 100644 index 00000000..078e5ac7 --- /dev/null +++ b/luci-app-chatgpt-web/luasrc/view/chatgpt-web.htm @@ -0,0 +1,6845 @@ +<%# + Copyright 2023-2025 sirpdboy Wich https://github.com/sirpdboy/luci-app-chatgpt-web + Licensed to the public under the Apache License 2.0. +-%> +<% +local fs = require "nixio.fs" +local dsp = require "luci.dispatcher" +local uci = luci.model.uci.cursor() +local apikeys = uci:get_first('chatgpt-web', 'basic', 'apikey') +local userpic = "/luci-static/chatgpt-web/user/" .. uci:get_first('chatgpt-web', 'basic', 'userpic') +local sysrole = uci:get_first('chatgpt-web', 'basic', 'systemrole') + +-%> + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
ChatGPT
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + 4o + + +
+
+
+ + +
+ + +
+
+
+
+ +
+
+
+
+ + +
+ + +
+
+
+
+
+
+ + + +
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
ChatGPT
+
DeepSeek
+
Azure
+
Gemini
+
Claude
+
+
+ +
+
+
+
ChatGPT
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/luci-app-chatgpt-web/po/zh-cn/chatgpt-web.po b/luci-app-chatgpt-web/po/zh-cn/chatgpt-web.po new file mode 100644 index 00000000..91654dbd --- /dev/null +++ b/luci-app-chatgpt-web/po/zh-cn/chatgpt-web.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +msgid "chatgpt-web" +msgstr "ChatGPT-Web" + +msgid "ChatGPT-Web is the version used on OPENWRT of OPENAI" +msgstr "ChatGPT-Web是OPENAI的OPENWRT上使用的WEB版本,不需要第三方依赖,绿色安全直接使用OPENAI的API接口" + +msgid "Base Setting" +msgstr "基本设置" + +msgid "GPT Model" +msgstr "GPT模型" + +msgid "Character images" +msgstr "人物图像" + +msgid "boy" +msgstr "男生" + +msgid "boy2" +msgstr "男生2" + +msgid "boy3" +msgstr "男生3" + +msgid "girl" +msgstr "女生" + +msgid "girl2" +msgstr "女生2" + +msgid "girl3" +msgstr "女生3" + +msgid "dog" +msgstr "狗" + +msgid "cartoon" +msgstr "卡通图像" + +msgid "cartoonboy" +msgstr "卡通男孩" + +msgid "cartoonboy2" +msgstr "卡通小男孩" + +msgid "cartoongirl" +msgstr "卡通女生" + +msgid "cartoongirl2" +msgstr "卡通女生2" + +msgid "OpenAI interface" +msgstr "OpenAI接口" + +msgid "API Key" +msgstr "API密钥" + +msgid "Enter your own API key, and if you do not have one, enter the official website API to generate it:" +msgstr "输入自己的API密钥(sk-开头),没有的进官网API生成:" + +msgid "Click to open" +msgstr "点击前往" + +msgid "Default Role Settings" +msgstr "默认角色设置" + +msgid "
For specific usage, see:" +msgstr "
具体使用方法参见:" diff --git a/luci-app-chatgpt-web/po/zh_Hans/chatgpt-web.po b/luci-app-chatgpt-web/po/zh_Hans/chatgpt-web.po new file mode 100644 index 00000000..91654dbd --- /dev/null +++ b/luci-app-chatgpt-web/po/zh_Hans/chatgpt-web.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +msgid "chatgpt-web" +msgstr "ChatGPT-Web" + +msgid "ChatGPT-Web is the version used on OPENWRT of OPENAI" +msgstr "ChatGPT-Web是OPENAI的OPENWRT上使用的WEB版本,不需要第三方依赖,绿色安全直接使用OPENAI的API接口" + +msgid "Base Setting" +msgstr "基本设置" + +msgid "GPT Model" +msgstr "GPT模型" + +msgid "Character images" +msgstr "人物图像" + +msgid "boy" +msgstr "男生" + +msgid "boy2" +msgstr "男生2" + +msgid "boy3" +msgstr "男生3" + +msgid "girl" +msgstr "女生" + +msgid "girl2" +msgstr "女生2" + +msgid "girl3" +msgstr "女生3" + +msgid "dog" +msgstr "狗" + +msgid "cartoon" +msgstr "卡通图像" + +msgid "cartoonboy" +msgstr "卡通男孩" + +msgid "cartoonboy2" +msgstr "卡通小男孩" + +msgid "cartoongirl" +msgstr "卡通女生" + +msgid "cartoongirl2" +msgstr "卡通女生2" + +msgid "OpenAI interface" +msgstr "OpenAI接口" + +msgid "API Key" +msgstr "API密钥" + +msgid "Enter your own API key, and if you do not have one, enter the official website API to generate it:" +msgstr "输入自己的API密钥(sk-开头),没有的进官网API生成:" + +msgid "Click to open" +msgstr "点击前往" + +msgid "Default Role Settings" +msgstr "默认角色设置" + +msgid "
For specific usage, see:" +msgstr "
具体使用方法参见:" diff --git a/luci-app-chatgpt-web/root/etc/config/chatgpt-web b/luci-app-chatgpt-web/root/etc/config/chatgpt-web new file mode 100644 index 00000000..a7a1636a --- /dev/null +++ b/luci-app-chatgpt-web/root/etc/config/chatgpt-web @@ -0,0 +1,8 @@ + +config basic + option apiHost 'https://api.openai.com/' + option modelver 'gpt-3.5-turbo' + option userpic 'girl.jpg' + option apikey '' + option systemrole '你是一个乐于助人的专业助手,尽量简明扼要地回答' + diff --git a/luci-app-chatgpt-web/root/usr/share/rpcd/acl.d/luci-app-chatgpt-web.json b/luci-app-chatgpt-web/root/usr/share/rpcd/acl.d/luci-app-chatgpt-web.json new file mode 100644 index 00000000..c8c06f50 --- /dev/null +++ b/luci-app-chatgpt-web/root/usr/share/rpcd/acl.d/luci-app-chatgpt-web.json @@ -0,0 +1,11 @@ +{ + "luci-app-chatgpt-web": { + "description": "Grant UCI access for luci-app-chatgpt-web", + "read": { + "uci": [ "chatgpt-web" ] + }, + "write": { + "uci": [ "chatgpt-web" ] + } + } +} diff --git a/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/11.11.1github.min.css b/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/11.11.1github.min.css new file mode 100644 index 00000000..275239a7 --- /dev/null +++ b/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/11.11.1github.min.css @@ -0,0 +1,10 @@ +pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0} \ No newline at end of file diff --git a/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/chatgpt-web.css b/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/chatgpt-web.css new file mode 100644 index 00000000..7a24e376 --- /dev/null +++ b/luci-app-chatgpt-web/root/www/luci-static/chatgpt-web/chatgpt-web.css @@ -0,0 +1,2166 @@ + + .chat_window { + display: flex; + width: 100%; + max-width: 1920px; + height: 888px; + border-radius: 8px; + background-color: rgba(255,255,255,0.5); + overflow: auto; + } + + .chat_window>.overlay { + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + background-color: rgba(0, 0, 0, .3); + z-index: 90; + opacity: 0; + visibility: hidden; + cursor: pointer; + transition: all 120ms ease-in-out; + } + + .chat_window .mainContent .cbi-input-invalid { + color: var(--title-color); + border-bottom-color: var(--title-color); + } + + + @media screen and (min-width: 800px) { + + .mainContent { + width: calc(100% - 250px); + } + + .chat_window>.nav { + position: relative; + margin-left: -250px; + transition: margin-left 250ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + + .show-nav .nav { + margin-left: 0; + } + + #sysDialog { + max-width: 600px; + } + + .sysContent { + display: flex; + } + + .sysSwitch { + flex-shrink: 0; + width: 160px; + } + + .sysSwitch>div { + padding-left: 6px; + } + + .sysDetail { + margin-left: 12px; + margin-top: 5px; + } + } + + @media screen and (max-width: 800px) { + + .mainContent { + width: 100%; + } + + .chat_window>.nav { + position: absolute !important; + left: -250px; + transition: left 250ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + + .show-nav .nav { + left: 0; + box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 10px -5px, rgba(0, 0, 0, 0.14) 0px 16px 24px 2px, rgba(0, 0, 0, 0.12) 0px 6px 30px 5px; + } + + .show-nav .overlay { + visibility: visible; + opacity: 1; + } + + #sysDialog { + max-width: 400px; + } + + .sysSwitch { + display: flex; + } + + .sysSwitch>div { + width: 50%; + justify-content: center; + } + + .sysDetail { + margin-top: 8px; + } + } + .full_window { + max-width: none; + max-height: none; + } + + .chat_window>.nav { + width: 250px; + background-color: rgba(var(--primary-rgbbody),1); + color: var(--body-color); + top: 0; + z-index: 99; + flex-shrink: 0; + display: flex; + flex-direction: column; + margin-left: -250px; + transition: left 120ms ease-in-out; + } + + .mainContent { + height: 100%; + position: relative; + width: 100%; + display: flex; + flex-direction: column; + flex: 1; + } + + .show-nav .nav { + margin-left: 0; + } + .show-nav .chatnav { + margin-left: 0; + box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 10px -5px, rgba(0, 0, 0, 0.14) 0px 16px 24px 2px, rgba(0, 0, 0, 0.12) 0px 6px 30px 5px; + } + + .show-nav .overlay { + opacity: 1; + visibility: visible; + } + .top_menu { + width: 100%; + background-color: rgba(255,255,255,0.5); + height: 50px; + padding: 5px 0; + } + + .top_menu .toggler { + margin-left: 10px; + width: 40px; + height: 40px; + float: left; + padding: 5px 7px; + border-radius: 4px; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + } + + .top_menu .toggler:hover { + background: #e0e0e0; + } + + .top_menu .toggler .button { + width: 26px; + height: 4px; + border-radius: 4px; + position: absolute; + pointer-events: none; + } + + .top_menu .toggler .button.close { + margin-top: 3px; + background-color: #000; + } + + .top_menu .toggler .button.minimize { + margin-top: 13px; + background-color: #000; + } + + .top_menu .toggler .button.maximize { + margin-top: 23px; + background-color: #000; + } + + .top_menu .title { + font-size: 20px; + height: 40px; + line-height: 40px; + position: relative; + pointer-events: none; + } + + #selector { + position: absolute; + top: 50%; + left: 50%; + padding: 0 4px 0 10px; + cursor: pointer; + font-size: 20px; + transform: translate(-50%, -50%); + pointer-events: auto; + border-radius: 4px; + -webkit-user-select: none; + user-select: none; + display: flex; + align-items: center; + } + + #selector:hover { + color: var(--title-color); + } + + #modelVer { + margin-left: 6px; + margin-right: 4px; + height: 40px; + line-height: 38px; + } + + .messages { + position: relative; + flex: 1; + overflow-x: hidden; + overflow-y: auto; + font-size: 16px; + width: 100%; + text-align: center; + } + + .messages::-webkit-scrollbar, + + #chatlog .markdown-body>pre::-webkit-scrollbar, + #chatlog .markdown-body>pre>code::-webkit-scrollbar, + #chatlog .responseBody pre>code::-webkit-scrollbar, + #setDialog::-webkit-scrollbar, + .allList::-webkit-scrollbar, + .sysDetail::-webkit-scrollbar, + #apiSelect::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + .messages::-webkit-scrollbar-track, + + #chatlog .markdown-body>pre::-webkit-scrollbar-track, + #chatlog .markdown-body>pre>code::-webkit-scrollbar-track, + #chatlog .responseBody pre>code::-webkit-scrollbar-track, + #setDialog::-webkit-scrollbar-track, + .allList::-webkit-scrollbar-track, + .sysDetail::-webkit-scrollbar-track, + #apiSelect::-webkit-scrollbar-track { + background-clip: padding-box; + background: transparent; + border: none; + } + + .messages::-webkit-scrollbar-corner, + + #chatlog .markdown-body>pre::-webkit-scrollbar-corner, + #chatlog .markdown-body>pre>code::-webkit-scrollbar-corner, + #chatlog .responseBody pre>code::-webkit-scrollbar-corner, + #setDialog::-webkit-scrollbar-corner, + .allList::-webkit-scrollbar-corner, + .sysDetail::-webkit-scrollbar-corner, + #apiSelect::-webkit-scrollbar-corner { + background-color: transparent; + } + + .messages::-webkit-scrollbar-thumb, + + #chatlog .markdown-body>pre::-webkit-scrollbar-thumb, + #chatlog .markdown-body>pre>code::-webkit-scrollbar-thumb, + #chatlog .responseBody pre>code::-webkit-scrollbar-thumb, + #setDialog::-webkit-scrollbar-thumb, + .allList::-webkit-scrollbar-thumb, + .sysDetail::-webkit-scrollbar-thumb, + #apiSelect::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.1); + background-clip: padding-box; + border: solid transparent; + border-radius: 10px; + } + + .messages::-webkit-scrollbar-thumb:hover, + + #chatlog .markdown-body>pre::-webkit-scrollbar-thumb:hover, + #chatlog .markdown-body>pre>code::-webkit-scrollbar-thumb:hover, + #chatlog .responseBody pre>code::-webkit-scrollbar-thumb:hover, + #setDialog::-webkit-scrollbar-thumb:hover, + .allList::-webkit-scrollbar-thumb:hover, + .sysDetail::-webkit-scrollbar-thumb:hover, + #apiSelect::-webkit-scrollbar-thumb:hover { + background-color: rgba(0, 0, 0, 0.4); + } + + #chatlog { + word-wrap: break-word; + text-align: start; + } + + #chatlog>div { + padding: 18px 40px 18px 54px; + } + .chatAvatar { + position: absolute; + width: 30px; + height: 30px; + border-radius: 2px; + } + + .request>.chatAvatar { + top: 18px; + left: 13px; + margin-top: 15px; + margin-bottom: 15px; + } + .response>.chatAvatar { + background: #10a37f; + top: 18px; + left: 13px; + margin-top: 15px; + margin-bottom: 15px; + } + + .gptAvatar>svg { + margin-top: 3px; + margin-left: 3px; + } + + .chatAvatar>img { + display: block; + width: 100%; + height: 100%; + border-radius: 2px; + } + + .modelAvatar>svg, + .chatAvatar>svg { + display: block; + margin-top: 4px; + margin-left: 4px; + } + + #chatlog .request { + position: relative; + background-color: var(--inputborder-color); + display: flex; + justify-content: center; + } + + + .response .responseBody { + display: flex; + justify-content: flex-end; + margin: 18px 45px 18px 20px; + font-size: 15px; + + } + .requestBody { + white-space: pre-wrap; + margin: 18px; + flex: 1; + } + + #chatlog .response { + background-color: var(--inputborder-color); + position: relative; + } + + .response .markdown-body { + margin: 18px 65px 18px 0px; + flex: 1; + font-size: 15px; + background-color: rgba(255,255,255,0)!important; + + } + + #chatlog .markdown-body a { + padding: 0 1px 0 2px; + } + + #chatlog .markdown-body pre { + overflow-x: auto; + padding: 10px; + position: relative; + white-space: pre-wrap; + background-color: var(--inputborder-color)!important; + + } + + #chatlog .markdown-body .mermaid { + margin-bottom: 8px; + padding: 0 10px 0 10px; + position: relative; + border-radius: 6px; + } + + .mermaid>svg { + cursor: pointer; + max-height: 600px; + display: block; + } + + #chatlog .markdown-body pre>code { + overflow-x: auto; + white-space: pre-wrap; + display: block; + } + + #chatlog .markdown-body ul { + list-style-type: disc; + } + + .m-mdic-copy-wrapper { + position: absolute; + top: 5px; + right: 16px; + -webkit-user-select: none; + user-select: none; + } + + .m-mdic-copy-wrapper span.u-mdic-copy-code_lang { + position: absolute; + top: 3px; + right: calc(100% + 4px); + font-family: system-ui; + font-size: 12px; + line-height: 18px; + color: #bbb; + } + + .m-mdic-copy-wrapper div.u-mdic-copy-notify { + position: absolute; + top: 0; + right: 0; + padding: 3px 6px; + border: 0; + border-radius: 3px; + background: none; + font-family: system-ui; + font-size: 12px; + line-height: 18px; + color: #555; + outline: none; + right: 100%; + padding-right: 4px; + } + + .m-mdic-copy-wrapper button.u-mdic-copy-btn { + position: relative; + top: 0; + right: 0; + padding: 3px 6px; + border: 0; + border-radius: 3px; + background: none; + font-family: system-ui; + font-size: 12px; + line-height: 18px; + color: #bbb; + outline: none; + cursor: pointer; + transition: color 0.2s; + } + + .m-mdic-copy-wrapper span.u-mdic-copy-code_lang::before, + .m-mdic-copy-wrapper div.u-mdic-copy-notify::before, + .m-mdic-copy-wrapper button.u-mdic-copy-btn::before { + content: attr(text); + } + + .m-mdic-copy-wrapper button.u-mdic-copy-btn:hover { + color: #555; + } + + #stopChat { + display: none; + margin: 0 auto; + margin-top: 3px; + width: 80px; + height: 32px; + text-align: center; + line-height: 32px; + color: white; + background: #f8b26a; + cursor: pointer; + border-radius: 3px; + position: sticky; + bottom: 2px; + justify-content: center; + align-items: center; + } + + #stopChat>svg { + margin-right: 8px; + } + + #stopChat:hover { + background: #f0aa60; + } + + .bottom_wrapper { + position: relative; + width: 100%; + padding: 10px 10px; + max-width: 91%; + margin: 0 auto; + } + + .bottom_wrapper .message_input_wrapper { + width: calc(100% - 134px); + position: relative; + text-align: left; + } + + .bottom_wrapper .message_input_wrapper .message_input_text { + border-radius: 4px; + border: none; + outline: none; + resize: none; + background-color: var(--inputborder-color); + height: 47px; + font-size: 16px; + max-height: 200px; + padding: 13px 0 13px 13px; + width: 100%!important; + display: block; + transition: background-color 250ms; + } + + .bottom_wrapper .message_input_wrapper .message_input_text:focus { + background-color: var(--inputborder-color); + } + + .bottom_wrapper .message_input_wrapper .message_input_text::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + } + + #sendbutton { + width: 70px; + height: 47px; + font-size: 18px; + font-weight: bold; + color: #fff; + background-color: #7ac516; + padding: 0.5rem 0.5rem; + transition: all 0.2s linear; + position: absolute; + right: 65px; + bottom: 10px; + cursor: not-allowed; + } + + .activeSendBtn { + background-color: #99c959 !important; + cursor: pointer !important; + } + + .activeSendBtn:hover { + background-color: #90c050 !important; + } + + .clearConv { + position: absolute; + right: 10px; + bottom: 10px; + width: 47px; + padding: 0.5rem 0.5rem; + height: 47px; + } + + .clearConv>svg { + margin: 0 auto; + top: 50%; + left: 50%; + } + + .clearConv:hover { + background-color: var(--body-hover-bgcolor); + } + + .clearConv svg:first-child { + display: none; + } + + .clearConv svg:nth-child(2) { + display: block; + } + + .closeConv { + background: #e0e0e0;; + } + + .closeConv:hover { + background: #e8e8e8; + } + + .closeConv svg:first-child { + display: block; + } + + .closeConv svg:nth-child(2) { + display: none; + } + + .loaded>span { + display: inline-block; + } + + .loaded>svg { + display: none; + } + + .loading { + background: rgba(0,0,0,0) !important; + } + + .loading>span { + display: none; + } + + .loading>svg { + display: block; + } + + .switch-slide { + display: inline-block; + vertical-align: middle; + } + + .switch-slide-label { + display: block; + width: 38px; + height: 18px; + background: #909090; + border-radius: 30px; + cursor: pointer; + position: relative; + -webkit-transition: 0.3s ease; + transition: 0.3s ease; + } + + .switch-slide-label:after { + content: ""; + display: block; + width: 16px; + height: 16px; + border-radius: 100%; + background: #fff; + box-shadow: 0 1px 1px rgba(0, 0, 0, .1); + position: absolute; + left: 1px; + top: 1px; + -webkit-transform: translateZ(0); + transform: translateZ(0); + -webkit-transition: all 250ms; + transition: all 250ms; + } + + .switch-slide input:checked+label { + background: #99c959; + -webkit-transition: all 250ms; + transition: all 250ms; + } + + .switch-slide input:checked+label:after { + left: 21px; + } + + .settings { + padding: 0px 7px; + margin-top: -40px; + float: right; + border-radius: 4px; + cursor: pointer; + top: 5px; + } + + + .setBtn { + margin-left: 2px; + cursor: pointer; + padding: 5px; + border: none; + border-radius: 4px; + } + + .setBtn>svg { + display: block; + color: #909090; + } + + .setBtn:hover { + background: #e0e0e0;; + } + + #setting { + right: 15px; + } + + #toggleFull { + right: 56px; + } + + #toggleLight *, + #toggleFull * { + pointer-events: none; + } + + .showSetting { + background-color: var(--inputborder-color); + } + + .showModels { + background-color: var(--inputborder-color); + } + + .showModels>svg:last-child { + transform: scaleY(-1); + } + + #modelDialog { + position: absolute; + z-index: 2; + background: rgba(var(--primary-rgbbody), 1); + width: 240px; + left: 50%; + transform: translateX(-50%); + top: 46px; + overflow-y: auto; + max-height: calc(100% - 55px); + -webkit-user-select: none; + user-select: none; + border-radius: 4px; + padding: 8px 5px 8px 5px; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); + } + + .modelSingle { + height: 40px; + border-radius: 3px; + cursor: pointer; + line-height: 40px; + display: flex; + align-items: center; + position: relative; + } + + .modelSingle * { + pointer-events: none; + } + + .modelSingle:hover { + background-color: var(--body-hover-bgcolor); + } + + .activeModel::before { + content: ""; + position: absolute; + right: 15px; + top: 7px; + width: 8px; + height: 16px; + border-style: solid; + border-width: 0 4px 4px 0; + transform: rotate(45deg); + } + + .modelAvatar { + margin-left: 6px; + margin-right: 10px; + width: 30px; + flex-shrink: 0; + border-radius: 2px; + height: 30px; + } + + #setDialog { + position: absolute; + z-index: 2; + background-color: rgba(var(--primary-rgbbody),1); + width: 320px; + right: 6px; + top: 46px; + overflow-y: auto; + max-height: calc(100% - 55px); + -webkit-user-select: none; + user-select: none; + border-radius: 5px; + padding: 8px 12px 8px 12px; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); + } + + #setDialog input { + width: 100%; + } + + #setDialog .inlineTitle { + display: inline-block; + width: 80px; + line-height: 16px; + vertical-align: middle; + } + + #convOption, + #speechOption, + #speechDetail, + #recOption { + margin-bottom: 6px; + } + + #convOption>div, + #speechOption>div, + #speechDetail>div, + #recOption div { + margin-top: 10px; + } + + #voiceRecSetting select, #speechDetail select { + color: var(--title-color); + + } + .inputTextClass { + outline: none; + border-radius: 2px; + margin-top: 3px; + height: 32px; + font-size: 15px; + padding-left: 6px; + border: none; + } + + .areaTextClass { + width: 100%; + height: 150px; + white-space: normal; + resize: none; + padding: 6px; + } + + input[type="range"] { + -webkit-appearance: none; + appearance: none; + display: block; + margin: 4px 0 3px 0; + height: 8px; + background: #909090; + border-radius: 5px; + background-image: linear-gradient(#99c959, #99c959); + background-size: 100% 100%; + background-repeat: no-repeat; + } + + input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + height: 15px; + width: 15px; + border-radius: 50%; + background: #99c959; + cursor: ew-resize; + } + + input[type=range]::-webkit-slider-runnable-track { + -webkit-appearance: none; + box-shadow: none; + border: none; + background: transparent; + } + + .justSetLine { + display: flex; + justify-content: space-between; + align-items: center; + } + + .justSetBtn { + height: 32px; + border-radius: 3px; + line-height: 32px; + background-color: var(--inputborder-color); + text-align: center; + padding: 0px 8px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } + + .justSetBtn:hover { + background-color: #d0d0d0; + } + + .justSetBtn>svg { + margin-right: 3px; + } + + .readyTestVoice>div:not(:first-child) { + display: none; + } + + .pauseTestVoice>div:nth-child(1), + .pauseTestVoice>div:nth-child(3) { + display: none; + } + + .resumeTestVoice>div:nth-child(1), + .resumeTestVoice>div:nth-child(2) { + display: none; + } + + .presetSelect>div { + display: inline-block; + } + + .presetSelect select { + outline: none; + border-radius: 3px; + width: 120px; + border-color: rgba(0, 0, 0, .3); + } + + .selectDef { + display: flex; + justify-content: space-between; + font-size: 13px; + color: #909090; + } + + #preSetSpeech { + width: 100%; + outline: none; + height: 30px; + line-height: 30px; + font-size: 14px; + margin-top: 5px; + border-radius: 3px; + border-color: rgba(0, 0, 0, .3); + } + + .mdOption { + position: absolute; + right: 0px; + top: 0; + width: 50px; + height: 100%; + pointer-events: none; + } + + .mdOption>div { + pointer-events: auto; + cursor: pointer; + } + + .mdOption svg * { + pointer-events: none; + } + + .refreshReq svg:not(:first-child) { + display: none; + } + + .halfRefReq svg:not(:nth-child(2)) { + display: none; + } + + .optionItems { + position: absolute; + bottom: -5px; + display: flex; + justify-content: space-between; + visibility: visible; + z-index: 1; + color: #808080; + } + + .optionItems:hover { + visibility: visible; + } + + .request:hover .optionItems, + .request:hover .voiceCls, + .response:hover .optionItems, + .response:hover .voiceCls { + visibility: visible; + } + + .optionItem { + border-radius: 10px; + height: 24px; + width: 32px; + border: 1px solid #e0e0e0;; + background-color: #f7f7f8; ; + display: flex !important; + justify-content: center; + align-items: center; + } + + .optionItem * { + pointer-events: none; + } + + .optionItem:hover { + background: #e0e0e0;; + } + + .voiceCls { + position: relative; + height: 100%; + visibility: visible; + display: flex; + align-items: center; + } + + .voiceCls>svg { + color: #c0c0c0; + display: block; + margin-left: 5px; + position: relative; + pointer-events: none; + } + + .voiceCls:hover>svg { + color: #808080; + } + + .showVoiceCls, + .showVoiceCls .markdown-body { + background: #e0e0e0 !important; + } + + .showVoiceCls .voiceCls { + visibility: visible !important; + } + + .showEditReq { + position: sticky !important; + top: 0; + bottom: 0; + z-index: 1; + } + + .showEditReq, + .showEditReq .markdown-body { + background: #e0e0e0 !important; + } + + .readyVoice svg:not(:first-child) { + display: none; + } + + .pauseVoice svg:not(:nth-child(2)) { + display: none; + } + + .resumeVoice svg:not(:nth-child(3)) { + display: none; + } + + #voiceTypes>span { + border-radius: 3px; + margin-left: 4px; + cursor: pointer; + color: var(--title-color); + padding: 1px 5px; + } + + #voiceTypes>span:hover { + background-color: var(--body-hover-bgcolor); + } + + .selVoiceType { + background-color: var(--inputborder-color); + } + + .navHeader { + width: 100%; + padding: 5px 10px; + display: flex; + justify-content: space-between; + } + + #newChat { + text-align: center; + width: 80%; + height: 40px; + border-radius: 3px; + background: var(--inputborder-color); + display: flex; + align-items: center; + justify-content: center; + -webkit-user-select: none; + user-select: none; + cursor: pointer; + flex: 1; + } + + .navHeader>div:hover { + background: var(--body-hover-bgcolor)!important; + } + + #newChat>svg { + margin-right: 2px; + } + + #newFolder { + height: 40px; + width: 40px; + margin-left: 10px; + border-radius: 3px; + background: var(--inputborder-color); + cursor: pointer; + position: relative; + -webkit-user-select: none; + user-select: none; + } + + #newFolder>svg { + display: block; + margin: 8px auto; + } + + .extraChat { + padding: 2px 10px 6px 10px; + position: relative; + } + + #searchChat { + width: 100%; + height: 36px; + padding-left: 10px; + padding-right: 16px; + font-size: 16px; + outline: none; + border: none; + background: var(--inputborder-color); + border-radius: 3px; + } + + #searchChat:focus { + background: var(--inputborder-color); + } + + #searchChat:placeholder-shown+#clearSearch { + display: none; + } + + #clearSearch { + position: absolute; + right: 12px; + top: 8px; + cursor: pointer; + } + + #clearSearch:hover { + color: var(--title-color); + } + + #clearSearch>svg { + display: block; + } + + .seledSearch { + background: var(--inputborder-color)!important; + } + + #matchCaseSearch { + position: absolute; + right: 12px; + top: 8px; + cursor: pointer; + border-radius: 3px; + color: var(--inputborder-color); + } + + #matchCaseSearch:hover { + background: var(--body-hover-bgcolor); + } + + #matchCaseSearch>svg { + display: block; + } + + .navFooter { + padding-bottom: 8px; + background: var(--inputborder-color); + } + + .navFooter .divider { + width: 100%; + border-top: 1px solid var(--inputborder-color); + margin: 4px 0; + } + + .navFunc { + padding-top: 5px; + display: flex; + justify-content: space-evenly; + } + + .navFunc svg { + display: block; + } + + .navFunc>div, + .navFunc>label { + border-radius: 20px; + text-align: center; + padding: 8px 8px; + font-size: 14px; + cursor: pointer; + } + + .navFunc>div:hover, + .navFunc>label:hover { + background: var(--body-hover-bgcolor); + } + + .navFooter .links { + text-align: center; + -webkit-user-select: none; + user-select: none; + } + + .navFooter .links a { + text-decoration: none; + } + + + .allList { + width: 100%; + flex: 1; + overflow-y: auto; + } + + #chatList { + min-height: 50px; + } + + .dragingLi { + filter: brightness(90%); + } + + .dragingChat { + background: #e8e8e8; + } + + .expandFolder>.headLi>svg { + transform: rotate(90deg); + } + + .expandFolder>.chatsInFolder { + display: block; + } + + .chatsInFolder { + display: none; + margin-left: 22px; + padding-left: 2px; + border-left: 1px solid #909090; ; + } + + .headLi, + .chatLi { + cursor: pointer; + width: 100%; + height: 50px; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + } + + .headLi *, + .chatLi * { + pointer-events: none; + } + + .headLi>svg, + .chatLi>svg { + margin-left: 10px; + pointer-events: none; + } + + .folderOption svg, + .chatOption svg { + pointer-events: auto; + } + + .headLi svg *, + .chatLi svg * { + pointer-events: none; + } + + .headLi .folderInfo { + position: absolute; + left: 40px; + height: 40px; + max-width: calc(100% - 115px); + } + + .chatLi .chatInfo { + position: absolute; + left: 45px; + height: 40px; + max-width: calc(100% - 100px); + } + + .folderInfo *, + .chatInfo * { + -webkit-user-select: none; + user-select: none; + } + + .chatInfo span { + background: var(--inputborder-color)!important; + } + + .headLi .folderName, + .chatLi .chatName { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + line-height: 20px; + height: 20px; + } + + .headLi .folderNum, + .chatLi .chatPre { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + font-size: 12px; + line-height: 20px; + height: 20px; + } + + .headLi .folderOption, + .chatLi .chatOption { + visibility: hidden; + display: flex; + margin-right: 5px; + } + + .folderLi .chatLi>svg { + margin-left: 5px; + } + + .folderLi .chatLi .chatInfo { + left: 35px; + max-width: calc(100% - 85px); + } + + .folderLi .chatLi #activeChatEdit { + left: 32px; + width: calc(100% - 60px) + } + + .folderLi:hover { + background: var(--body-hover-bgcolor)!important; + } + + .chatLi:hover { + background: var(--body-hover-bgcolor)!important; + } + + .headLi:hover .folderOption, + .chatLi:hover .chatOption { + visibility: visible !important; + } + + .activeFolder, + .activeChatLi { + background: var(--inputborder-color)!important; + } + + .activeChatLi .chatOption { + visibility: visible !important; + } + + .folderOption>svg:hover, + .chatOption>svg:hover { + color: var(--title-color); + } + + #activeChatEdit { + position: absolute; + left: 42px; + font-size: 16px; + border-radius: 2px; + color: var(--title-color); + background-color: rgba(var(--primary-rgbbody),1); + outline: none; + border: none; + pointer-events: auto; + height: 24px; + line-height: 24px; + width: calc(100% - 75px); + padding: 20px 3px; + z-index: 1; + } + + #loadMask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + background-color: #edeff2; + } + + #loadMask>div { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + text-align: center; + } + + @keyframes loading { + + 0%, + 100% { + transform: scale(0); + } + + 50% { + transform: scale(1); + } + } + + .loadingCSSIcon { + position: relative; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .loadingCSSIcon div { + width: 15%; + height: 0; + padding-bottom: 15%; + border-radius: 50%; + animation: loading 1s cubic-bezier(0.3, 0, 0.7, 1) infinite; + } + + .loadingCSSIcon div:nth-child(1) { + background: #e15b64; + animation-delay: -0.4s + } + + .loadingCSSIcon div:nth-child(2) { + background: #f8b26a; + animation-delay: -0.2s + } + + .loadingCSSIcon div:nth-child(3) { + background: #99c959; + animation-delay: 0s; + } + + #loadMask>div>:first-child { + font-size: 40px; + color: #909090; ; + } + + #loadMask>div>:last-child { + width: 140px; + height: 70px; + margin: 0 auto; + } + + #voiceRec { + position: absolute; + right: 0; + top: 0; + width: 47px; + height: 100%; + } + + .message_if_voice { + padding-right: 47px !important; + } + + #voiceRecIcon { + width: 100%; + height: 100%; + text-align: center; + cursor: pointer; + position: relative; + } + + #voiceRecIcon:hover>svg { + color: #808080; + } + + #voiceRecIcon>svg { + width: 38px; + height: 38px; + color: #b0b0b0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + #voiceRecIcon>svg .animVoice { + display: none; + } + + .voiceRecing>svg { + color: #99c959 !important; + } + + .voiceRecing .animVoice { + display: inline !important; + transform-origin: 0 64%; + animation-duration: 1.5s; + animation-name: scaleVoice; + animation-timing-function: ease; + animation-iteration-count: infinite; + } + + .voiceLong .animVoice { + display: inline !important; + transform-origin: 0 64%; + animation-duration: 0.3s; + animation-name: longVoice; + animation-timing-function: ease-in-out; + animation-iteration-count: 1; + } + + @keyframes longVoice { + 0% { + transform: scaleY(0); + } + + 100% { + transform: scaleY(1); + } + } + + @keyframes scaleVoice { + 0% { + transform: scaleY(0.28); + } + + 20% { + transform: scaleY(0.60); + } + + 28% { + transform: scaleY(0.28); + } + + 36% { + transform: scaleY(0.45); + } + + 44% { + transform: scaleY(0.28); + } + + 52% { + transform: scaleY(0.45); + } + + 62% { + transform: scaleY(0.80); + } + + 72% { + transform: scaleY(0.80); + } + + 90% { + transform: scaleY(0.28); + } + + 100% { + transform: scaleY(0.28); + } + } + + #voiceRecSetting { + display: none; + position: absolute; + top: -70px; + left: -26px; + z-index: 1; + padding: 4px 4px; + -webkit-user-select: none; + user-select: none; + border-radius: 3px; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); + } + + #voiceRecSetting select { + width: 102px; + outline: none; + height: 28px; + border-radius: 3px; + border-color: rgba(0, 0, 0, .3); + } + + .presetModelCls label { + margin-right: 8px; + } + + .presetModelCls select { + height: 30px; + line-height: 30px; + margin-top: 2px; + font-size: 15px; + } + + .modelSwitch, + .setSwitch { + display: flex; + } + + .modelSwitch { + margin-bottom: 10px; + -webkit-user-select: none; + user-select: none; + } + + .modelSwitch>div, + .setSwitch>div { + border-radius: 3px; + width: calc(100% / 3); + padding: 8px; + line-height: 1; + text-align: center; + cursor: pointer; + } + + .modelSwitch>div:hover, + .setSwitch>div:hover { + background-color: var(--body-hover-bgcolor); + } + + .activeSwitch { + background-color: var(--inputborder-color); + } + + .checkLoad { + height: 32px; + border-radius: 3px; + line-height: 32px; + background: #d0d0d0; + text-align: center; + display: flex; + justify-content: center; + cursor: pointer; + } + + .checkLoad:hover { + background: #c0c0c0; + } + + .voiceChecking { + background-color: #c0c0c0 !important; + } + + .voiceChecking>svg { + display: inline !important; + } + + .checkLoad>svg { + display: none; + margin-right: 8px; + height: 32px; + width: 64px; + } + + #preSetSystem { + height: 30px; + line-height: 30px; + } + + #sysMask { + display: none; + position: fixed; + z-index: 200; + top: 0; + left: 0; + bottom: 0; + right: 0; + cursor: pointer; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, .4); + -webkit-tap-highlight-color: transparent; + } + + #sysDialog { + position: relative; + background: rgba(var(--primary-rgbbody),1); + cursor: auto; + max-height: 100%; + width: 100%; + display: flex; + flex-direction: column; + border-radius: 4px; + padding: 20px; + } + + .sysTitle { + font-size: 20px; + font-weight: bold; + margin-bottom: 8px; + -webkit-user-select: none; + user-select: none; + } + + .sysSwitch, + .sysSwitch>div * { + pointer-events: none; + } + + .sysSwitch>div { + border-radius: 3px; + height: 32px; + line-height: 32px; + text-align: center; + cursor: pointer; + pointer-events: auto; + font-weight: bold; + display: flex; + align-items: center; + -webkit-user-select: none; + user-select: none; + } + + .sysSwitch>div>svg { + margin-right: 4px; + } + + .sysSwitch>div:hover { + background-color: var(--body-hover-bgcolor); + } + + .sysDetail { + overflow-y: auto; + flex: 1; + margin-top: 4px; + } + + #closeSet { + position: absolute; + right: 0px; + top: 0px; + cursor: pointer; + padding: 10px 14px; + } + + #closeSet:hover { + color: #000; + } + + .setContent { + margin-top: 8px; + } + + .setNotNormalFlow { + position: relative; + } + + .setTitle { + margin-bottom: 6px; + font-weight: bold; + -webkit-user-select: none; + user-select: none; + } + + .setDetail { + margin: 0 10px; + -webkit-user-select: none; + user-select: none; + } + + .autoSelect>label, + .autoSelect>input { + cursor: pointer; + } + + .dataDetail { + display: flex; + } + + .dataDetail svg { + margin-right: 4px; + } + + .dataDetail>div, + .dataDetail>label { + border-radius: 3px; + text-align: center; + padding: 6px 8px; + margin-right: 12px; + font-size: 15px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + } + + .dataDetail>div:hover, + .dataDetail>label:hover { + background-color: var(--body-hover-bgcolor); + } + + .keyOptionDetail { + display: flex; + margin-top: 10px; + } + + #resetHotKey { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + font-size: 15px; + border-radius: 3px; + padding: 6px 8px; + margin-left: 90px; + } + + #resetHotKey>svg { + margin-right: 4px; + } + + #resetHotKey:hover { + background-color:var(--body-hover-bgcolor); + } + + .hotKeyDetail>div { + position: relative; + height: 30px; + margin-bottom: 2px; + } + + .hotKeyDetail label { + line-height: 30px; + } + + .hotKeyDetail select { + position: absolute; + left: 110px; + outline: none; + border-radius: 3px; + width: 120px; + border-color: rgba(0, 0, 0, .3); + height: 30px; + font-size: 15px; + } + + .avatarDetail { + display: flex; + margin-top: 2px; + } + + .avatarDetail img { + border-radius: 2px; + width: 32px; + height: 32px; + flex-shrink: 0; + margin-right: 10px; + margin-top: 2px; + } + + .inputDetail input { + outline: none; + border-radius: 3px; + padding-left: 8px; + font-size: 15px; + width: 100%; + height: 34px; + border: 1px solid rgba(0, 0, 0, .3); + } + + .themeDetail { + display: flex; + width: 180px; + justify-content: space-between; + pointer-events: none; + } + + .themeDetail svg { + display: block; + } + + .themeDetail>div { + pointer-events: auto; + border-radius: 20px; + text-align: center; + padding: 8px 8px; + font-size: 14px; + cursor: pointer; + } + + .themeDetail>div * { + pointer-events: none; + } + + .themeDetail>div:hover { + background-color: var(--body-hover-bgcolor); + } + + .darkTheme>div:first-child { + background-color: var(--inputborder-color); + } + + .lightTheme>div:nth-child(2) { + background-color: var(--inputborder-color); + } + + .autoTheme>div:nth-child(3) { + background-color: var(--inputborder-color); + } + + .langDetail { + width: 110px; + } + + .enLang>div:first-child { + background-color: var(--inputborder-color); + } + + .zhLang>div:nth-child(2) { + background-color: var(--inputborder-color); + } + + #customAutoSet input { + width: 100px; + height: 30px; + line-height: 30px; + font-size: 15px; + outline: none; + border: 1px solid rgba(0, 0, 0, .3); + text-align: center; + border-radius: 3px; + background: #fff; + color: #24292f; + } + + #customAutoSet label { + margin-right: 8px; + } + + .progressBar { + position: relative; + width: 100%; + height: 12px; + border-radius: 6px; + background: #e0e0e0; + overflow: hidden; + } + + .nowProgress { + position: absolute; + left: 0; + top: 0; + height: 12px; + min-width: 1px; + border-radius: 6px; + background: #99c959; + } + + .progressDetail { + display: flex; + justify-content: space-between; + font-size: 15px; + } + + .cursorCls { + background-color: var(--inputborder-color); + width: 4px; + animation: 1s cursor-blinker infinite step-start; + } + + @keyframes cursor-blinker { + 0% { + opacity: 0; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0; + } + } + + .apiSelsContainer { + position: relative; + z-index: 1; + } + + #apiSelect { + position: absolute; + top: 64px; + padding: 4px 0; + width: calc(100% - 20px); + left: 10px; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.15); + max-height: 180px; + overflow-y: auto; + -webkit-user-select: none; + user-select: none; + } + + #apiSelect>div { + pointer-events: auto; + cursor: pointer; + font-size: 15px; + padding: 6px 0 6px 8px; + height: 36px; + display: flex; + justify-content: space-between; + align-items: center; + } + + #apiSelect>div:hover { + background-color:var(--body-hover-bgcolor); + } + + #apiSelect>div>span { + height: 100%; + line-height: 23px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + #apiSelect>div * { + pointer-events: none; + } + + .delApiOption:hover { + background-color: var(--body-hover-bgcolor); + } + + .delApiOption { + width: 36px; + height: 36px; + flex-shrink: 0; + pointer-events: auto !important; + } + + .delApiOption>svg { + margin: 6px; + display: block; + } + + @keyframes textInOut { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + .response[data-loading="true"]>.responseBody>*:not(.cursorCls) { + animation: textInOut 0.48s ease-in-out forwards; + } + + .cotBtn { + width: 120px; + height: 32px; + font-size: 14px; + user-select: none; + background-color: var(--inputborder-color); + position: relative; + padding-left: 14px; + line-height: 32px; + border-radius: 8px; + margin-bottom: 12px; + cursor: pointer; + } + + .cotBtn:hover { + background-color: var(--body-hover-bgcolor); + } + + .cotBtn.endCot { + width: 142px; + } + + .cotBtn::after { + content: ''; + display: inline-block; + position: absolute; + right: 15px; + top: 14px; + width: 6px; + height: 6px; + transform: rotate(-45deg); + } + + .cotBtn.down::after { + top: 11px; + transform: rotate(135deg); + } + + .cotContent { + background-color: var(--inputborder-color); + padding-left: 12px; + font-size: 14px; + } + + .cotContent>:last-child { + margin-bottom: 12px !important; + } + + .hide { + display: none; + } + +