Merge branch 'main' of https://git.unissense.tech/mula/dashboard-nanobot
commit
e7ee26d727
|
|
@ -48,8 +48,6 @@ DEFAULT_BOT_SYSTEM_TIMEZONE=Asia/Shanghai
|
||||||
|
|
||||||
# Panel access protection (deployment secret, not stored in sys_setting)
|
# Panel access protection (deployment secret, not stored in sys_setting)
|
||||||
PANEL_ACCESS_PASSWORD=change_me_panel_password
|
PANEL_ACCESS_PASSWORD=change_me_panel_password
|
||||||
WORKSPACE_PREVIEW_SIGNING_SECRET=change_me_workspace_preview_signing_secret
|
|
||||||
WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS=3600
|
|
||||||
|
|
||||||
# Browser credential requests must use an explicit CORS allowlist (deployment security setting).
|
# Browser credential requests must use an explicit CORS allowlist (deployment security setting).
|
||||||
# If frontend and backend are served under the same origin via nginx `/api` proxy,
|
# If frontend and backend are served under the same origin via nginx `/api` proxy,
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ REDIS_DEFAULT_TTL=60
|
||||||
|
|
||||||
# Optional panel-level access password for all backend API/WS calls.
|
# Optional panel-level access password for all backend API/WS calls.
|
||||||
PANEL_ACCESS_PASSWORD=
|
PANEL_ACCESS_PASSWORD=
|
||||||
WORKSPACE_PREVIEW_SIGNING_SECRET=
|
|
||||||
WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS=3600
|
|
||||||
|
|
||||||
# Explicit CORS allowlist for browser credential requests.
|
# Explicit CORS allowlist for browser credential requests.
|
||||||
# For local development, the backend defaults to common Vite dev origins.
|
# For local development, the backend defaults to common Vite dev origins.
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from sqlmodel import Session
|
||||||
from core.database import get_session
|
from core.database import get_session
|
||||||
from models.bot import BotInstance
|
from models.bot import BotInstance
|
||||||
from schemas.system import WorkspaceFileUpdateRequest, WorkspacePreviewUrlRequest
|
from schemas.system import WorkspaceFileUpdateRequest, WorkspacePreviewUrlRequest
|
||||||
|
from services.platform_system_settings_service import get_workspace_preview_token_ttl_seconds
|
||||||
from services.workspace_service import (
|
from services.workspace_service import (
|
||||||
create_workspace_html_preview_url,
|
create_workspace_html_preview_url,
|
||||||
get_workspace_tree_data,
|
get_workspace_tree_data,
|
||||||
|
|
@ -77,10 +78,13 @@ def create_workspace_preview_url(
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
ttl_seconds = payload.ttl_seconds
|
||||||
|
if ttl_seconds is None:
|
||||||
|
ttl_seconds = get_workspace_preview_token_ttl_seconds(session)
|
||||||
return create_workspace_html_preview_url(
|
return create_workspace_html_preview_url(
|
||||||
bot_id=bot_id,
|
bot_id=bot_id,
|
||||||
path=payload.path,
|
path=payload.path,
|
||||||
ttl_seconds=payload.ttl_seconds,
|
ttl_seconds=ttl_seconds,
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/api/bots/{bot_id}/workspace/download")
|
@router.get("/api/bots/{bot_id}/workspace/download")
|
||||||
|
|
|
||||||
|
|
@ -42,19 +42,6 @@ REQUIRED_TABLES = (
|
||||||
"topic_item",
|
"topic_item",
|
||||||
)
|
)
|
||||||
|
|
||||||
REQUIRED_SYS_SETTING_KEYS = (
|
|
||||||
"page_size",
|
|
||||||
"chat_pull_page_size",
|
|
||||||
"auth_token_ttl_hours",
|
|
||||||
"auth_token_max_active",
|
|
||||||
"upload_max_mb",
|
|
||||||
"allowed_attachment_extensions",
|
|
||||||
"workspace_download_extensions",
|
|
||||||
"speech_enabled",
|
|
||||||
"activity_event_retention_days",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_required_tables() -> None:
|
def _validate_required_tables() -> None:
|
||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
missing = [table_name for table_name in REQUIRED_TABLES if not inspector.has_table(table_name)]
|
missing = [table_name for table_name in REQUIRED_TABLES if not inspector.has_table(table_name)]
|
||||||
|
|
@ -65,30 +52,14 @@ def _validate_required_tables() -> None:
|
||||||
"Run scripts/init-full-db.sh or apply scripts/sql/create-tables.sql before starting the backend."
|
"Run scripts/init-full-db.sh or apply scripts/sql/create-tables.sql before starting the backend."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_required_sys_settings() -> None:
|
|
||||||
placeholders = ", ".join(f":k{i}" for i, _ in enumerate(REQUIRED_SYS_SETTING_KEYS))
|
|
||||||
params = {f"k{i}": key for i, key in enumerate(REQUIRED_SYS_SETTING_KEYS)}
|
|
||||||
with engine.connect() as conn:
|
|
||||||
rows = conn.execute(
|
|
||||||
text(f'SELECT key FROM "{SYS_SETTING_TABLE}" WHERE key IN ({placeholders})'),
|
|
||||||
params,
|
|
||||||
).scalars().all()
|
|
||||||
present = {str(row or "").strip() for row in rows if str(row or "").strip()}
|
|
||||||
missing = [key for key in REQUIRED_SYS_SETTING_KEYS if key not in present]
|
|
||||||
if missing:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Database seed data is not initialized. "
|
|
||||||
f"Missing sys_setting keys: {', '.join(missing)}. "
|
|
||||||
"Run scripts/init-full-db.sh or apply scripts/sql/init-data.sql before starting the backend."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def init_database() -> None:
|
def init_database() -> None:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
conn.execute(text("SELECT 1"))
|
conn.execute(text("SELECT 1"))
|
||||||
_validate_required_tables()
|
_validate_required_tables()
|
||||||
_validate_required_sys_settings()
|
from services.platform_system_settings_service import validate_required_system_settings
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
validate_required_system_settings(session)
|
||||||
|
|
||||||
|
|
||||||
def get_session():
|
def get_session():
|
||||||
|
|
|
||||||
|
|
@ -189,15 +189,6 @@ DEFAULT_PAGE_SIZE: Final[int] = 10
|
||||||
DEFAULT_CHAT_PULL_PAGE_SIZE: Final[int] = 60
|
DEFAULT_CHAT_PULL_PAGE_SIZE: Final[int] = 60
|
||||||
DEFAULT_AUTH_TOKEN_TTL_HOURS: Final[int] = _env_int("AUTH_TOKEN_TTL_HOURS", 24, 1, 720)
|
DEFAULT_AUTH_TOKEN_TTL_HOURS: Final[int] = _env_int("AUTH_TOKEN_TTL_HOURS", 24, 1, 720)
|
||||||
DEFAULT_AUTH_TOKEN_MAX_ACTIVE: Final[int] = _env_int("AUTH_TOKEN_MAX_ACTIVE", 2, 1, 20)
|
DEFAULT_AUTH_TOKEN_MAX_ACTIVE: Final[int] = _env_int("AUTH_TOKEN_MAX_ACTIVE", 2, 1, 20)
|
||||||
WORKSPACE_PREVIEW_SIGNING_SECRET: Final[str] = str(
|
|
||||||
os.getenv("WORKSPACE_PREVIEW_SIGNING_SECRET") or DATABASE_URL
|
|
||||||
).strip()
|
|
||||||
WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS: Final[int] = _env_int(
|
|
||||||
"WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS",
|
|
||||||
3600,
|
|
||||||
60,
|
|
||||||
86400,
|
|
||||||
)
|
|
||||||
DEFAULT_BOT_SYSTEM_TIMEZONE: Final[str] = str(
|
DEFAULT_BOT_SYSTEM_TIMEZONE: Final[str] = str(
|
||||||
os.getenv("DEFAULT_BOT_SYSTEM_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai"
|
os.getenv("DEFAULT_BOT_SYSTEM_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai"
|
||||||
).strip() or "Asia/Shanghai"
|
).strip() or "Asia/Shanghai"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ from schemas.platform import SystemSettingItem
|
||||||
|
|
||||||
DEFAULT_ALLOWED_ATTACHMENT_EXTENSIONS: tuple[str, ...] = ()
|
DEFAULT_ALLOWED_ATTACHMENT_EXTENSIONS: tuple[str, ...] = ()
|
||||||
DEFAULT_ACTIVITY_EVENT_RETENTION_DAYS = 7
|
DEFAULT_ACTIVITY_EVENT_RETENTION_DAYS = 7
|
||||||
|
DEFAULT_WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS = 3600
|
||||||
ACTIVITY_EVENT_RETENTION_SETTING_KEY = "activity_event_retention_days"
|
ACTIVITY_EVENT_RETENTION_SETTING_KEY = "activity_event_retention_days"
|
||||||
|
WORKSPACE_PREVIEW_TOKEN_TTL_SETTING_KEY = "workspace_preview_token_ttl_seconds"
|
||||||
SETTING_KEYS = (
|
SETTING_KEYS = (
|
||||||
"page_size",
|
"page_size",
|
||||||
"chat_pull_page_size",
|
"chat_pull_page_size",
|
||||||
|
|
@ -30,7 +32,7 @@ SETTING_KEYS = (
|
||||||
"workspace_download_extensions",
|
"workspace_download_extensions",
|
||||||
"speech_enabled",
|
"speech_enabled",
|
||||||
)
|
)
|
||||||
PROTECTED_SETTING_KEYS = set(SETTING_KEYS) | {ACTIVITY_EVENT_RETENTION_SETTING_KEY}
|
PROTECTED_SETTING_KEYS = set(SETTING_KEYS) | {ACTIVITY_EVENT_RETENTION_SETTING_KEY, WORKSPACE_PREVIEW_TOKEN_TTL_SETTING_KEY}
|
||||||
SYSTEM_SETTING_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
SYSTEM_SETTING_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
||||||
"page_size": {
|
"page_size": {
|
||||||
"name": "分页大小",
|
"name": "分页大小",
|
||||||
|
|
@ -95,6 +97,15 @@ SYSTEM_SETTING_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
||||||
"is_public": False,
|
"is_public": False,
|
||||||
"sort_order": 30,
|
"sort_order": 30,
|
||||||
},
|
},
|
||||||
|
WORKSPACE_PREVIEW_TOKEN_TTL_SETTING_KEY: {
|
||||||
|
"name": "工作区预览 Token 过期秒数",
|
||||||
|
"category": "workspace",
|
||||||
|
"description": "HTML 预览地址中临时访问 Token 的默认有效时长,单位秒。",
|
||||||
|
"value_type": "integer",
|
||||||
|
"value": DEFAULT_WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS,
|
||||||
|
"is_public": False,
|
||||||
|
"sort_order": 31,
|
||||||
|
},
|
||||||
"speech_enabled": {
|
"speech_enabled": {
|
||||||
"name": "语音识别开关",
|
"name": "语音识别开关",
|
||||||
"category": "speech",
|
"category": "speech",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from services.platform_settings_core import (
|
||||||
ACTIVITY_EVENT_RETENTION_SETTING_KEY,
|
ACTIVITY_EVENT_RETENTION_SETTING_KEY,
|
||||||
PROTECTED_SETTING_KEYS,
|
PROTECTED_SETTING_KEYS,
|
||||||
SYSTEM_SETTING_DEFINITIONS,
|
SYSTEM_SETTING_DEFINITIONS,
|
||||||
|
WORKSPACE_PREVIEW_TOKEN_TTL_SETTING_KEY,
|
||||||
_normalize_setting_key,
|
_normalize_setting_key,
|
||||||
_read_setting_value,
|
_read_setting_value,
|
||||||
_setting_item_from_row,
|
_setting_item_from_row,
|
||||||
|
|
@ -30,15 +31,19 @@ def _prune_deprecated_system_settings(session: Session) -> None:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
def validate_required_system_settings(session: Session) -> None:
|
def _missing_required_system_settings(session: Session) -> List[str]:
|
||||||
_prune_deprecated_system_settings(session)
|
|
||||||
stmt = select(PlatformSetting.key).where(PlatformSetting.key.in_(REQUIRED_SYSTEM_SETTING_KEYS))
|
stmt = select(PlatformSetting.key).where(PlatformSetting.key.in_(REQUIRED_SYSTEM_SETTING_KEYS))
|
||||||
present = {
|
present = {
|
||||||
str(key or "").strip()
|
str(key or "").strip()
|
||||||
for key in session.exec(stmt).all()
|
for key in session.exec(stmt).all()
|
||||||
if str(key or "").strip()
|
if str(key or "").strip()
|
||||||
}
|
}
|
||||||
missing = [key for key in REQUIRED_SYSTEM_SETTING_KEYS if key not in present]
|
return [key for key in REQUIRED_SYSTEM_SETTING_KEYS if key not in present]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_required_system_settings(session: Session) -> None:
|
||||||
|
_prune_deprecated_system_settings(session)
|
||||||
|
missing = _missing_required_system_settings(session)
|
||||||
if missing:
|
if missing:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Database seed data is not initialized. "
|
"Database seed data is not initialized. "
|
||||||
|
|
@ -118,3 +123,22 @@ def get_activity_event_retention_days(session: Session) -> int:
|
||||||
"Fix the row manually or reapply scripts/sql/init-data.sql."
|
"Fix the row manually or reapply scripts/sql/init-data.sql."
|
||||||
) from exc
|
) from exc
|
||||||
return max(1, min(3650, value))
|
return max(1, min(3650, value))
|
||||||
|
|
||||||
|
|
||||||
|
def get_workspace_preview_token_ttl_seconds(session: Session) -> int:
|
||||||
|
validate_required_system_settings(session)
|
||||||
|
row = session.get(PlatformSetting, WORKSPACE_PREVIEW_TOKEN_TTL_SETTING_KEY)
|
||||||
|
if row is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Database seed data is not initialized. "
|
||||||
|
f"Missing sys_setting key: {WORKSPACE_PREVIEW_TOKEN_TTL_SETTING_KEY}. "
|
||||||
|
"Run scripts/init-full-db.sh or apply scripts/sql/init-data.sql before starting the backend."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
value = int(_read_setting_value(row))
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"sys_setting value is invalid for key: {WORKSPACE_PREVIEW_TOKEN_TTL_SETTING_KEY}. "
|
||||||
|
"Fix the row manually or reapply scripts/sql/init-data.sql."
|
||||||
|
) from exc
|
||||||
|
return max(60, value)
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ import json
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from core.settings import WORKSPACE_PREVIEW_SIGNING_SECRET, WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS
|
from core.settings import DATABASE_URL
|
||||||
|
|
||||||
HTML_PREVIEW_EXTENSIONS = {".html", ".htm"}
|
HTML_PREVIEW_EXTENSIONS = {".html", ".htm"}
|
||||||
|
DEFAULT_WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS = 3600
|
||||||
|
_WORKSPACE_PREVIEW_SIGNING_KEY = DATABASE_URL
|
||||||
|
|
||||||
|
|
||||||
def _b64url_encode(raw: bytes) -> str:
|
def _b64url_encode(raw: bytes) -> str:
|
||||||
|
|
@ -31,7 +33,7 @@ def is_html_preview_path(path: str) -> bool:
|
||||||
def create_workspace_preview_token(bot_id: str, path: str, ttl_seconds: Optional[int] = None) -> Dict[str, Any]:
|
def create_workspace_preview_token(bot_id: str, path: str, ttl_seconds: Optional[int] = None) -> Dict[str, Any]:
|
||||||
normalized_bot_id = str(bot_id or "").strip()
|
normalized_bot_id = str(bot_id or "").strip()
|
||||||
normalized_path = normalize_workspace_preview_path(path)
|
normalized_path = normalize_workspace_preview_path(path)
|
||||||
ttl = max(60, min(int(ttl_seconds or WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS), 86400))
|
ttl = max(60, int(ttl_seconds or DEFAULT_WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS))
|
||||||
expires_at = int(time.time()) + ttl
|
expires_at = int(time.time()) + ttl
|
||||||
payload = {
|
payload = {
|
||||||
"bot_id": normalized_bot_id,
|
"bot_id": normalized_bot_id,
|
||||||
|
|
@ -42,7 +44,7 @@ def create_workspace_preview_token(bot_id: str, path: str, ttl_seconds: Optional
|
||||||
payload_json = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
payload_json = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
||||||
body = _b64url_encode(payload_json)
|
body = _b64url_encode(payload_json)
|
||||||
signature = hmac.new(
|
signature = hmac.new(
|
||||||
WORKSPACE_PREVIEW_SIGNING_SECRET.encode("utf-8"),
|
_WORKSPACE_PREVIEW_SIGNING_KEY.encode("utf-8"),
|
||||||
body.encode("ascii"),
|
body.encode("ascii"),
|
||||||
hashlib.sha256,
|
hashlib.sha256,
|
||||||
).digest()
|
).digest()
|
||||||
|
|
@ -59,7 +61,7 @@ def resolve_workspace_preview_token(token: str) -> Optional[Dict[str, Any]]:
|
||||||
return None
|
return None
|
||||||
body, signature_raw = raw_token.split(".", 1)
|
body, signature_raw = raw_token.split(".", 1)
|
||||||
expected_signature = hmac.new(
|
expected_signature = hmac.new(
|
||||||
WORKSPACE_PREVIEW_SIGNING_SECRET.encode("utf-8"),
|
_WORKSPACE_PREVIEW_SIGNING_KEY.encode("utf-8"),
|
||||||
body.encode("ascii"),
|
body.encode("ascii"),
|
||||||
hashlib.sha256,
|
hashlib.sha256,
|
||||||
).digest()
|
).digest()
|
||||||
|
|
|
||||||
|
|
@ -487,11 +487,12 @@ export function ChannelConfigModal({
|
||||||
<div className="ops-config-collapsed-meta">{summary}</div>
|
<div className="ops-config-collapsed-meta">{summary}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ops-config-card-actions">
|
<div className="ops-config-card-actions">
|
||||||
|
{!dashboardChannel ? (
|
||||||
|
<>
|
||||||
<label className="field-label">
|
<label className="field-label">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={channel.is_active}
|
checked={channel.is_active}
|
||||||
disabled={dashboardChannel}
|
|
||||||
onChange={(e) => onUpdateChannelLocal(idx, { is_active: e.target.checked })}
|
onChange={(e) => onUpdateChannelLocal(idx, { is_active: e.target.checked })}
|
||||||
style={{ marginRight: 6 }}
|
style={{ marginRight: 6 }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -499,13 +500,15 @@ export function ChannelConfigModal({
|
||||||
</label>
|
</label>
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="btn btn-danger btn-sm wizard-icon-btn"
|
className="btn btn-danger btn-sm wizard-icon-btn"
|
||||||
disabled={isSavingChannel || dashboardChannel}
|
disabled={isSavingChannel}
|
||||||
onClick={() => void onRemoveChannel(channel)}
|
onClick={() => void onRemoveChannel(channel)}
|
||||||
tooltip={labels.remove}
|
tooltip={labels.remove}
|
||||||
aria-label={labels.remove}
|
aria-label={labels.remove}
|
||||||
>
|
>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="ops-plain-icon-btn"
|
className="ops-plain-icon-btn"
|
||||||
onClick={() => onToggleExpandedChannel(uiKey)}
|
onClick={() => onToggleExpandedChannel(uiKey)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ArrowUp, ChevronLeft, Clock3, Command, Download, Eye, FileText, Mic, Paperclip, Plus, RefreshCw, RotateCcw, Square, X } from 'lucide-react';
|
import { ArrowUp, ChevronLeft, Clock3, Command, Download, Eye, FileText, Mic, Paperclip, Plus, RefreshCw, RotateCcw, Square, X } from 'lucide-react';
|
||||||
import type { ChangeEventHandler, KeyboardEventHandler, RefObject } from 'react';
|
import { useRef, useState, type ChangeEventHandler, type DragEvent, type DragEventHandler, type KeyboardEventHandler, type RefObject } from 'react';
|
||||||
|
|
||||||
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||||||
import { normalizeAssistantMessageText } from '../../../shared/text/messageText';
|
import { normalizeAssistantMessageText } from '../../../shared/text/messageText';
|
||||||
|
|
@ -26,6 +26,7 @@ interface DashboardChatComposerProps {
|
||||||
filePickerRef: RefObject<HTMLInputElement | null>;
|
filePickerRef: RefObject<HTMLInputElement | null>;
|
||||||
allowedAttachmentExtensions: string[];
|
allowedAttachmentExtensions: string[];
|
||||||
onPickAttachments: ChangeEventHandler<HTMLInputElement>;
|
onPickAttachments: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
onDropAttachments: (files: FileList) => Promise<void> | void;
|
||||||
controlCommandPanelOpen: boolean;
|
controlCommandPanelOpen: boolean;
|
||||||
controlCommandPanelRef: RefObject<HTMLDivElement | null>;
|
controlCommandPanelRef: RefObject<HTMLDivElement | null>;
|
||||||
onToggleControlCommandPanel: () => void;
|
onToggleControlCommandPanel: () => void;
|
||||||
|
|
@ -125,6 +126,7 @@ export function DashboardChatComposer({
|
||||||
filePickerRef,
|
filePickerRef,
|
||||||
allowedAttachmentExtensions,
|
allowedAttachmentExtensions,
|
||||||
onPickAttachments,
|
onPickAttachments,
|
||||||
|
onDropAttachments,
|
||||||
controlCommandPanelOpen,
|
controlCommandPanelOpen,
|
||||||
controlCommandPanelRef,
|
controlCommandPanelRef,
|
||||||
onToggleControlCommandPanel,
|
onToggleControlCommandPanel,
|
||||||
|
|
@ -159,6 +161,46 @@ export function DashboardChatComposer({
|
||||||
}: DashboardChatComposerProps) {
|
}: DashboardChatComposerProps) {
|
||||||
const showInterruptSubmitAction = submitActionMode === 'interrupt';
|
const showInterruptSubmitAction = submitActionMode === 'interrupt';
|
||||||
const hasComposerDraft = Boolean(String(command || '').trim()) || pendingAttachments.length > 0 || Boolean(quotedReply);
|
const hasComposerDraft = Boolean(String(command || '').trim()) || pendingAttachments.length > 0 || Boolean(quotedReply);
|
||||||
|
const [isDraggingAttachments, setIsDraggingAttachments] = useState(false);
|
||||||
|
const dragDepthRef = useRef(0);
|
||||||
|
const canDropAttachments = canChat && !isUploadingAttachments && !isVoiceRecording && !isVoiceTranscribing;
|
||||||
|
const hasDraggedFiles = (event: DragEvent<HTMLDivElement>) =>
|
||||||
|
Array.from(event.dataTransfer?.types || []).includes('Files');
|
||||||
|
|
||||||
|
const handleAttachmentDragEnter: DragEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
if (!hasDraggedFiles(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!canDropAttachments) return;
|
||||||
|
dragDepthRef.current += 1;
|
||||||
|
setIsDraggingAttachments(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAttachmentDragOver: DragEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
if (!hasDraggedFiles(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.dataTransfer.dropEffect = canDropAttachments ? 'copy' : 'none';
|
||||||
|
if (canDropAttachments) setIsDraggingAttachments(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAttachmentDragLeave: DragEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
if (!hasDraggedFiles(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
||||||
|
if (dragDepthRef.current === 0) setIsDraggingAttachments(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAttachmentDrop: DragEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
if (!hasDraggedFiles(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
dragDepthRef.current = 0;
|
||||||
|
setIsDraggingAttachments(false);
|
||||||
|
if (!canDropAttachments || event.dataTransfer.files.length === 0) return;
|
||||||
|
void onDropAttachments(event.dataTransfer.files);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -209,7 +251,13 @@ export function DashboardChatComposer({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="ops-composer">
|
<div
|
||||||
|
className={`ops-composer ${isDraggingAttachments ? 'is-dragging-attachments' : ''}`}
|
||||||
|
onDragEnter={handleAttachmentDragEnter}
|
||||||
|
onDragOver={handleAttachmentDragOver}
|
||||||
|
onDragLeave={handleAttachmentDragLeave}
|
||||||
|
onDrop={handleAttachmentDrop}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
ref={filePickerRef}
|
ref={filePickerRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
|
@ -219,6 +267,11 @@ export function DashboardChatComposer({
|
||||||
className="ops-hidden-file-input"
|
className="ops-hidden-file-input"
|
||||||
/>
|
/>
|
||||||
<div className={`ops-composer-shell ${controlCommandPanelOpen ? 'is-command-open' : ''}`}>
|
<div className={`ops-composer-shell ${controlCommandPanelOpen ? 'is-command-open' : ''}`}>
|
||||||
|
{isDraggingAttachments ? (
|
||||||
|
<div className="ops-composer-drop-overlay" aria-hidden="true">
|
||||||
|
<Paperclip size={24} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="ops-composer-float-controls" ref={controlCommandPanelRef}>
|
<div className="ops-composer-float-controls" ref={controlCommandPanelRef}>
|
||||||
<div className={`ops-control-command-drawer ${controlCommandPanelOpen ? 'is-open' : ''}`}>
|
<div className={`ops-control-command-drawer ${controlCommandPanelOpen ? 'is-open' : ''}`}>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@
|
||||||
background: var(--panel-soft);
|
background: var(--panel-soft);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
|
transition: border-color 0.16s ease, background 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-composer.is-dragging-attachments {
|
||||||
|
border-color: color-mix(in oklab, var(--brand) 64%, var(--line) 36%);
|
||||||
|
background: color-mix(in oklab, var(--brand-soft) 18%, var(--panel-soft) 82%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-composer-shell {
|
.ops-composer-shell {
|
||||||
|
|
@ -57,6 +63,20 @@
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-composer-drop-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed color-mix(in oklab, var(--brand) 62%, var(--line) 38%);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in oklab, var(--panel) 70%, var(--brand-soft) 30%);
|
||||||
|
color: var(--brand);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ops-chat-top-context {
|
.ops-chat-top-context {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ interface DashboardChatPanelProps {
|
||||||
filePickerRef: RefObject<HTMLInputElement | null>;
|
filePickerRef: RefObject<HTMLInputElement | null>;
|
||||||
allowedAttachmentExtensions: string[];
|
allowedAttachmentExtensions: string[];
|
||||||
onPickAttachments: ChangeEventHandler<HTMLInputElement>;
|
onPickAttachments: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
onDropAttachments: (files: FileList) => Promise<void> | void;
|
||||||
controlCommandPanelOpen: boolean;
|
controlCommandPanelOpen: boolean;
|
||||||
controlCommandPanelRef: RefObject<HTMLDivElement | null>;
|
controlCommandPanelRef: RefObject<HTMLDivElement | null>;
|
||||||
onToggleControlCommandPanel: () => void;
|
onToggleControlCommandPanel: () => void;
|
||||||
|
|
@ -242,6 +243,7 @@ export function DashboardChatPanel({
|
||||||
filePickerRef,
|
filePickerRef,
|
||||||
allowedAttachmentExtensions,
|
allowedAttachmentExtensions,
|
||||||
onPickAttachments,
|
onPickAttachments,
|
||||||
|
onDropAttachments,
|
||||||
controlCommandPanelOpen,
|
controlCommandPanelOpen,
|
||||||
controlCommandPanelRef,
|
controlCommandPanelRef,
|
||||||
onToggleControlCommandPanel,
|
onToggleControlCommandPanel,
|
||||||
|
|
@ -370,6 +372,7 @@ export function DashboardChatPanel({
|
||||||
filePickerRef={filePickerRef}
|
filePickerRef={filePickerRef}
|
||||||
allowedAttachmentExtensions={allowedAttachmentExtensions}
|
allowedAttachmentExtensions={allowedAttachmentExtensions}
|
||||||
onPickAttachments={onPickAttachments}
|
onPickAttachments={onPickAttachments}
|
||||||
|
onDropAttachments={onDropAttachments}
|
||||||
controlCommandPanelOpen={controlCommandPanelOpen}
|
controlCommandPanelOpen={controlCommandPanelOpen}
|
||||||
controlCommandPanelRef={controlCommandPanelRef}
|
controlCommandPanelRef={controlCommandPanelRef}
|
||||||
onToggleControlCommandPanel={onToggleControlCommandPanel}
|
onToggleControlCommandPanel={onToggleControlCommandPanel}
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,7 @@ export function useBotDashboardModule({
|
||||||
resolveWorkspaceMediaSrc,
|
resolveWorkspaceMediaSrc,
|
||||||
saveWorkspacePreviewMarkdown,
|
saveWorkspacePreviewMarkdown,
|
||||||
setPendingAttachments,
|
setPendingAttachments,
|
||||||
|
uploadAttachmentFiles,
|
||||||
setWorkspaceAutoRefresh,
|
setWorkspaceAutoRefresh,
|
||||||
setWorkspacePreviewDraft,
|
setWorkspacePreviewDraft,
|
||||||
setWorkspacePreviewFullscreen,
|
setWorkspacePreviewFullscreen,
|
||||||
|
|
@ -664,6 +665,7 @@ export function useBotDashboardModule({
|
||||||
removeStagedSubmission,
|
removeStagedSubmission,
|
||||||
pendingAttachments,
|
pendingAttachments,
|
||||||
setPendingAttachments,
|
setPendingAttachments,
|
||||||
|
uploadAttachmentFiles,
|
||||||
attachmentUploadPercent,
|
attachmentUploadPercent,
|
||||||
isUploadingAttachments,
|
isUploadingAttachments,
|
||||||
filePickerRef,
|
filePickerRef,
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ export function useBotDashboardViewProps({
|
||||||
filePickerRef: dashboard.filePickerRef,
|
filePickerRef: dashboard.filePickerRef,
|
||||||
allowedAttachmentExtensions: dashboard.allowedAttachmentExtensions,
|
allowedAttachmentExtensions: dashboard.allowedAttachmentExtensions,
|
||||||
onPickAttachments: dashboard.onPickAttachments,
|
onPickAttachments: dashboard.onPickAttachments,
|
||||||
|
onDropAttachments: dashboard.uploadAttachmentFiles,
|
||||||
controlCommandPanelOpen: dashboard.controlCommandPanelOpen,
|
controlCommandPanelOpen: dashboard.controlCommandPanelOpen,
|
||||||
controlCommandPanelRef: dashboard.controlCommandPanelRef,
|
controlCommandPanelRef: dashboard.controlCommandPanelRef,
|
||||||
onToggleControlCommandPanel: () => {
|
onToggleControlCommandPanel: () => {
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,7 @@ export function useBotWorkspace({
|
||||||
pendingAttachments,
|
pendingAttachments,
|
||||||
resetPendingAttachments,
|
resetPendingAttachments,
|
||||||
setPendingAttachments,
|
setPendingAttachments,
|
||||||
|
uploadAttachmentFiles,
|
||||||
} = useWorkspaceAttachments({
|
} = useWorkspaceAttachments({
|
||||||
selectedBotId,
|
selectedBotId,
|
||||||
workspaceCurrentPath,
|
workspaceCurrentPath,
|
||||||
|
|
@ -365,6 +366,7 @@ export function useBotWorkspace({
|
||||||
resolveWorkspaceMediaSrc,
|
resolveWorkspaceMediaSrc,
|
||||||
saveWorkspacePreviewMarkdown,
|
saveWorkspacePreviewMarkdown,
|
||||||
setPendingAttachments,
|
setPendingAttachments,
|
||||||
|
uploadAttachmentFiles,
|
||||||
setWorkspaceAutoRefresh,
|
setWorkspaceAutoRefresh,
|
||||||
setWorkspacePreviewDraft,
|
setWorkspacePreviewDraft,
|
||||||
setWorkspacePreviewFullscreen,
|
setWorkspacePreviewFullscreen,
|
||||||
|
|
|
||||||
|
|
@ -84,9 +84,10 @@ export function useWorkspaceAttachments({
|
||||||
setAttachmentUploadPercent(null);
|
setAttachmentUploadPercent(null);
|
||||||
}, [selectedBotId]);
|
}, [selectedBotId]);
|
||||||
|
|
||||||
const onPickAttachments = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
|
const uploadAttachmentFiles = useCallback(async (inputFiles: File[] | FileList) => {
|
||||||
if (!selectedBotId || !event.target.files || event.target.files.length === 0) return;
|
if (!selectedBotId || isUploadingAttachments) return;
|
||||||
const files = Array.from(event.target.files);
|
const files = Array.from(inputFiles);
|
||||||
|
if (files.length === 0) return;
|
||||||
try {
|
try {
|
||||||
const latestAttachmentPolicy = await refreshAttachmentPolicy();
|
const latestAttachmentPolicy = await refreshAttachmentPolicy();
|
||||||
const effectiveUploadMaxMb = latestAttachmentPolicy.uploadMaxMb;
|
const effectiveUploadMaxMb = latestAttachmentPolicy.uploadMaxMb;
|
||||||
|
|
@ -103,7 +104,6 @@ export function useWorkspaceAttachments({
|
||||||
if (disallowed.length > 0) {
|
if (disallowed.length > 0) {
|
||||||
const names = disallowed.map((file) => String(file.name || '').trim() || 'unknown').slice(0, 3).join(', ');
|
const names = disallowed.map((file) => String(file.name || '').trim() || 'unknown').slice(0, 3).join(', ');
|
||||||
notify(t.uploadTypeNotAllowed(names, effectiveAllowedAttachmentExtensions.join(', ')), { tone: 'warning' });
|
notify(t.uploadTypeNotAllowed(names, effectiveAllowedAttachmentExtensions.join(', ')), { tone: 'warning' });
|
||||||
event.target.value = '';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +114,6 @@ export function useWorkspaceAttachments({
|
||||||
if (tooLarge.length > 0) {
|
if (tooLarge.length > 0) {
|
||||||
const names = tooLarge.map((file) => String(file.name || '').trim() || 'unknown').slice(0, 3).join(', ');
|
const names = tooLarge.map((file) => String(file.name || '').trim() || 'unknown').slice(0, 3).join(', ');
|
||||||
notify(t.uploadTooLarge(names, effectiveUploadMaxMb), { tone: 'warning' });
|
notify(t.uploadTooLarge(names, effectiveUploadMaxMb), { tone: 'warning' });
|
||||||
event.target.value = '';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,9 +177,26 @@ export function useWorkspaceAttachments({
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingAttachments(false);
|
setIsUploadingAttachments(false);
|
||||||
setAttachmentUploadPercent(null);
|
setAttachmentUploadPercent(null);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isUploadingAttachments,
|
||||||
|
loadWorkspaceTree,
|
||||||
|
notify,
|
||||||
|
refreshAttachmentPolicy,
|
||||||
|
selectedBotId,
|
||||||
|
setPendingAttachments,
|
||||||
|
t,
|
||||||
|
workspaceCurrentPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onPickAttachments = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
try {
|
||||||
|
if (!event.target.files || event.target.files.length === 0) return;
|
||||||
|
await uploadAttachmentFiles(event.target.files);
|
||||||
|
} finally {
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}
|
}
|
||||||
}, [loadWorkspaceTree, notify, refreshAttachmentPolicy, selectedBotId, setPendingAttachments, t, workspaceCurrentPath]);
|
}, [uploadAttachmentFiles]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachmentUploadPercent,
|
attachmentUploadPercent,
|
||||||
|
|
@ -189,5 +205,6 @@ export function useWorkspaceAttachments({
|
||||||
pendingAttachments,
|
pendingAttachments,
|
||||||
resetPendingAttachments,
|
resetPendingAttachments,
|
||||||
setPendingAttachments,
|
setPendingAttachments,
|
||||||
|
uploadAttachmentFiles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,7 @@ AUTH_TOKEN_MAX_ACTIVE_JSON="2"
|
||||||
UPLOAD_MAX_MB_JSON="$UPLOAD_MAX_MB"
|
UPLOAD_MAX_MB_JSON="$UPLOAD_MAX_MB"
|
||||||
ALLOWED_ATTACHMENT_EXTENSIONS_JSON="[]"
|
ALLOWED_ATTACHMENT_EXTENSIONS_JSON="[]"
|
||||||
WORKSPACE_DOWNLOAD_EXTENSIONS_JSON='[".pdf", ".doc", ".docx", ".xls", ".xlsx", ".xlsm", ".ppt", ".pptx", ".odt", ".ods", ".odp", ".wps"]'
|
WORKSPACE_DOWNLOAD_EXTENSIONS_JSON='[".pdf", ".doc", ".docx", ".xls", ".xlsx", ".xlsm", ".ppt", ".pptx", ".odt", ".ods", ".odp", ".wps"]'
|
||||||
|
WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS_SEED_JSON="3600"
|
||||||
if [[ "${STT_ENABLED,,}" =~ ^(1|true|yes|on)$ ]]; then
|
if [[ "${STT_ENABLED,,}" =~ ^(1|true|yes|on)$ ]]; then
|
||||||
SPEECH_ENABLED_JSON="true"
|
SPEECH_ENABLED_JSON="true"
|
||||||
else
|
else
|
||||||
|
|
@ -175,6 +176,7 @@ docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T \
|
||||||
-v upload_max_mb_json="$UPLOAD_MAX_MB_JSON" \
|
-v upload_max_mb_json="$UPLOAD_MAX_MB_JSON" \
|
||||||
-v allowed_attachment_extensions_json="$ALLOWED_ATTACHMENT_EXTENSIONS_JSON" \
|
-v allowed_attachment_extensions_json="$ALLOWED_ATTACHMENT_EXTENSIONS_JSON" \
|
||||||
-v workspace_download_extensions_json="$WORKSPACE_DOWNLOAD_EXTENSIONS_JSON" \
|
-v workspace_download_extensions_json="$WORKSPACE_DOWNLOAD_EXTENSIONS_JSON" \
|
||||||
|
-v workspace_preview_token_ttl_seconds_json="$WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS_SEED_JSON" \
|
||||||
-v speech_enabled_json="$SPEECH_ENABLED_JSON" \
|
-v speech_enabled_json="$SPEECH_ENABLED_JSON" \
|
||||||
-v activity_event_retention_days_json="$ACTIVITY_EVENT_RETENTION_DAYS_JSON" \
|
-v activity_event_retention_days_json="$ACTIVITY_EVENT_RETENTION_DAYS_JSON" \
|
||||||
-U "$POSTGRES_SUPERUSER" \
|
-U "$POSTGRES_SUPERUSER" \
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,44 @@
|
||||||
\set ON_ERROR_STOP on
|
\set ON_ERROR_STOP on
|
||||||
|
\if :{?page_size_json}
|
||||||
|
\else
|
||||||
|
\set page_size_json 10
|
||||||
|
\endif
|
||||||
|
\if :{?chat_pull_page_size_json}
|
||||||
|
\else
|
||||||
|
\set chat_pull_page_size_json 60
|
||||||
|
\endif
|
||||||
|
\if :{?auth_token_ttl_hours_json}
|
||||||
|
\else
|
||||||
|
\set auth_token_ttl_hours_json 24
|
||||||
|
\endif
|
||||||
|
\if :{?auth_token_max_active_json}
|
||||||
|
\else
|
||||||
|
\set auth_token_max_active_json 2
|
||||||
|
\endif
|
||||||
|
\if :{?upload_max_mb_json}
|
||||||
|
\else
|
||||||
|
\set upload_max_mb_json 100
|
||||||
|
\endif
|
||||||
|
\if :{?allowed_attachment_extensions_json}
|
||||||
|
\else
|
||||||
|
\set allowed_attachment_extensions_json []
|
||||||
|
\endif
|
||||||
|
\if :{?workspace_download_extensions_json}
|
||||||
|
\else
|
||||||
|
\set workspace_download_extensions_json '[".pdf", ".doc", ".docx", ".xls", ".xlsx", ".xlsm", ".ppt", ".pptx", ".odt", ".ods", ".odp", ".wps"]'
|
||||||
|
\endif
|
||||||
|
\if :{?workspace_preview_token_ttl_seconds_json}
|
||||||
|
\else
|
||||||
|
\set workspace_preview_token_ttl_seconds_json 3600
|
||||||
|
\endif
|
||||||
|
\if :{?speech_enabled_json}
|
||||||
|
\else
|
||||||
|
\set speech_enabled_json true
|
||||||
|
\endif
|
||||||
|
\if :{?activity_event_retention_days_json}
|
||||||
|
\else
|
||||||
|
\set activity_event_retention_days_json 7
|
||||||
|
\endif
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
|
|
@ -22,6 +62,7 @@ VALUES
|
||||||
('upload_max_mb', '上传大小限制', 'upload', '单文件上传大小限制,单位 MB。', 'integer', :'upload_max_mb_json', FALSE, 20, NOW(), NOW()),
|
('upload_max_mb', '上传大小限制', 'upload', '单文件上传大小限制,单位 MB。', 'integer', :'upload_max_mb_json', FALSE, 20, NOW(), NOW()),
|
||||||
('allowed_attachment_extensions', '允许附件后缀', 'upload', '允许上传的附件后缀列表,留空表示不限制。', 'json', :'allowed_attachment_extensions_json', FALSE, 20, NOW(), NOW()),
|
('allowed_attachment_extensions', '允许附件后缀', 'upload', '允许上传的附件后缀列表,留空表示不限制。', 'json', :'allowed_attachment_extensions_json', FALSE, 20, NOW(), NOW()),
|
||||||
('workspace_download_extensions', '工作区下载后缀', 'workspace', '命中后缀的工作区文件默认走下载模式。', 'json', :'workspace_download_extensions_json', FALSE, 30, NOW(), NOW()),
|
('workspace_download_extensions', '工作区下载后缀', 'workspace', '命中后缀的工作区文件默认走下载模式。', 'json', :'workspace_download_extensions_json', FALSE, 30, NOW(), NOW()),
|
||||||
|
('workspace_preview_token_ttl_seconds', '工作区预览 Token 过期秒数', 'workspace', 'HTML 预览地址中临时访问 Token 的默认有效时长,单位秒。', 'integer', :'workspace_preview_token_ttl_seconds_json', FALSE, 31, NOW(), NOW()),
|
||||||
('speech_enabled', '语音识别开关', 'speech', '控制 Bot 语音转写功能是否启用。', 'boolean', :'speech_enabled_json', TRUE, 32, NOW(), NOW()),
|
('speech_enabled', '语音识别开关', 'speech', '控制 Bot 语音转写功能是否启用。', 'boolean', :'speech_enabled_json', TRUE, 32, NOW(), NOW()),
|
||||||
('activity_event_retention_days', '活动事件保留天数', 'maintenance', 'bot_activity_event 运维事件的保留天数,超期记录会自动清理。', 'integer', :'activity_event_retention_days_json', FALSE, 34, NOW(), NOW())
|
('activity_event_retention_days', '活动事件保留天数', 'maintenance', 'bot_activity_event 运维事件的保留天数,超期记录会自动清理。', 'integer', :'activity_event_retention_days_json', FALSE, 34, NOW(), NOW())
|
||||||
ON CONFLICT (key) DO UPDATE
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
|
@ -34,8 +75,6 @@ SET
|
||||||
sort_order = EXCLUDED.sort_order,
|
sort_order = EXCLUDED.sort_order,
|
||||||
updated_at = NOW();
|
updated_at = NOW();
|
||||||
|
|
||||||
DELETE FROM sys_setting WHERE key = 'command_auto_unlock_seconds';
|
|
||||||
|
|
||||||
INSERT INTO skill_market_item (
|
INSERT INTO skill_market_item (
|
||||||
skill_key,
|
skill_key,
|
||||||
display_name,
|
display_name,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue