更新
This commit is contained in:
@@ -14,6 +14,7 @@ use app\model\User;
|
||||
use app\model\UserDailyStat;
|
||||
use app\service\AdminScopeService;
|
||||
use app\service\ComfyUIService;
|
||||
use app\service\CosyVoiceService;
|
||||
use app\service\DepartmentService;
|
||||
use app\service\DifyService;
|
||||
use app\service\OpenAIService;
|
||||
@@ -666,11 +667,87 @@ class Admin extends BaseApi
|
||||
AdminScopeService::requireAny($this->authUser(), ['btn:settings:save', 'can_manage_settings']);
|
||||
$input = $this->request->put();
|
||||
foreach ($input as $key => $value) {
|
||||
if ($key === 'voice_persona') {
|
||||
if (!is_array($value)) {
|
||||
return $this->error('AI 客服人物配置格式无效', 422);
|
||||
}
|
||||
$value = CosyVoiceService::normalizePersona($value);
|
||||
}
|
||||
SettingsService::set($key, $value);
|
||||
}
|
||||
CosyVoiceService::clearFailure();
|
||||
return $this->success(null, '设置已更新');
|
||||
}
|
||||
|
||||
public function uploadVoiceReference()
|
||||
{
|
||||
AdminScopeService::requireAny($this->authUser(), ['btn:settings:save', 'can_manage_settings']);
|
||||
$file = $this->request->file('file');
|
||||
if (!$file) {
|
||||
return $this->error('请选择 WAV 参考音频', 422);
|
||||
}
|
||||
|
||||
$originalName = basename((string) $file->getOriginalName());
|
||||
$extension = strtolower($file->extension() ?: pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
if ($extension !== 'wav') {
|
||||
return $this->error('音色样本只支持 WAV 文件', 422);
|
||||
}
|
||||
if ((int) $file->getSize() > 15 * 1024 * 1024) {
|
||||
return $this->error('音色样本不能超过 15MB', 422);
|
||||
}
|
||||
|
||||
$header = @file_get_contents($file->getPathname(), false, null, 0, 12);
|
||||
if (!is_string($header) || strlen($header) < 12 || substr($header, 0, 4) !== 'RIFF' || substr($header, 8, 4) !== 'WAVE') {
|
||||
return $this->error('文件不是有效的 WAV 音频', 422);
|
||||
}
|
||||
|
||||
$targetDir = root_path() . 'storage' . DIRECTORY_SEPARATOR . 'cosyvoice';
|
||||
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
|
||||
return $this->error('无法创建音色样本目录', 500);
|
||||
}
|
||||
|
||||
$storedName = 'voice-' . date('Ymd-His') . '-' . bin2hex(random_bytes(4)) . '.wav';
|
||||
$moved = $file->move($targetDir, $storedName);
|
||||
if (!$moved) {
|
||||
return $this->error('音色样本保存失败', 500);
|
||||
}
|
||||
|
||||
$path = $targetDir . DIRECTORY_SEPARATOR . $storedName;
|
||||
$persona = CosyVoiceService::getPersona();
|
||||
$persona['prompt_wav'] = $path;
|
||||
$persona['prompt_wav_name'] = $originalName;
|
||||
$persona = CosyVoiceService::normalizePersona($persona, $persona);
|
||||
SettingsService::set('voice_persona', $persona);
|
||||
CosyVoiceService::clearFailure($persona);
|
||||
|
||||
return $this->success([
|
||||
'name' => $persona['prompt_wav_name'],
|
||||
'path' => $persona['prompt_wav'],
|
||||
], '音色样本上传成功');
|
||||
}
|
||||
|
||||
public function previewVoicePersona()
|
||||
{
|
||||
AdminScopeService::requireAny($this->authUser(), ['btn:settings:save', 'can_manage_settings']);
|
||||
$text = trim((string) ($this->request->post('text') ?: '您好,我是您的 AI 客服,很高兴为您服务。'));
|
||||
$text = mb_substr($text, 0, 160);
|
||||
|
||||
try {
|
||||
CosyVoiceService::clearFailure();
|
||||
$speech = CosyVoiceService::speech($text);
|
||||
} catch (\Throwable $error) {
|
||||
return $this->error($error->getMessage(), 502);
|
||||
}
|
||||
|
||||
return response($speech['audio'], 200, [
|
||||
'Content-Type' => $speech['content_type'],
|
||||
'Content-Length' => (string) strlen($speech['audio']),
|
||||
'Cache-Control' => 'no-store',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'X-TTS-Provider' => 'cosyvoice',
|
||||
]);
|
||||
}
|
||||
|
||||
public function models()
|
||||
{
|
||||
AdminScopeService::requireAny($this->authUser(), ['menu:models', 'can_manage_models']);
|
||||
@@ -872,6 +949,9 @@ class Admin extends BaseApi
|
||||
'inpaint_seed_node',
|
||||
'inpaint_image_node',
|
||||
'inpaint_mask_node',
|
||||
'tts_model',
|
||||
'tts_voice',
|
||||
'tts_instructions',
|
||||
] as $key) {
|
||||
if (!array_key_exists($key, $raw)) {
|
||||
continue;
|
||||
|
||||
@@ -9,6 +9,7 @@ use app\model\UploadFile;
|
||||
use app\service\AgentCatalog;
|
||||
use app\service\ComfyJobDeferredException;
|
||||
use app\service\ComfyUIService;
|
||||
use app\service\CosyVoiceService;
|
||||
use app\service\DifyService;
|
||||
use app\service\DocumentTextService;
|
||||
use app\service\OpenAIService;
|
||||
@@ -18,6 +19,133 @@ use think\facade\Log;
|
||||
|
||||
class Chat extends BaseApi
|
||||
{
|
||||
public function speech()
|
||||
{
|
||||
$this->authUser();
|
||||
$input = $this->request->post();
|
||||
$text = trim((string) ($input['text'] ?? ''));
|
||||
|
||||
if ($text === '') {
|
||||
return $this->error('语音内容不能为空', 422);
|
||||
}
|
||||
if (mb_strlen($text) > 600) {
|
||||
return $this->error('单次语音内容不能超过 600 个字符', 422);
|
||||
}
|
||||
|
||||
$modelId = isset($input['model_id']) && $input['model_id'] !== ''
|
||||
? (int) $input['model_id']
|
||||
: null;
|
||||
$persona = CosyVoiceService::getPersona();
|
||||
$voice = trim((string) ($persona['fallback_voice'] ?? $input['voice'] ?? 'marin'));
|
||||
$allowedVoices = [
|
||||
'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'nova',
|
||||
'onyx', 'sage', 'shimmer', 'verse', 'marin', 'cedar',
|
||||
];
|
||||
if (!in_array($voice, $allowedVoices, true)) {
|
||||
$voice = 'marin';
|
||||
}
|
||||
|
||||
$speech = null;
|
||||
if (CosyVoiceService::canAttempt()) {
|
||||
try {
|
||||
$speech = CosyVoiceService::speech($text);
|
||||
} catch (\Throwable $error) {
|
||||
Log::warning('CosyVoice speech fallback: ' . $error->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!$speech) {
|
||||
$model = OpenAIService::getSpeechModel($modelId);
|
||||
$speech = OpenAIService::speech($model, $text, $voice);
|
||||
$speech['provider'] = 'openai';
|
||||
}
|
||||
|
||||
return response($speech['audio'], 200, [
|
||||
'Content-Type' => $speech['content_type'],
|
||||
'Content-Length' => (string) strlen($speech['audio']),
|
||||
'Cache-Control' => 'no-store',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'X-TTS-Provider' => $speech['provider'] ?? 'unknown',
|
||||
]);
|
||||
}
|
||||
|
||||
public function speechStream(): never
|
||||
{
|
||||
$this->authUser();
|
||||
$input = $this->request->post();
|
||||
$text = trim((string) ($input['text'] ?? ''));
|
||||
$requestId = trim((string) ($input['request_id'] ?? ''));
|
||||
if (!preg_match('/^[A-Za-z0-9._:-]{8,128}$/', $requestId)) {
|
||||
$requestId = bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
if ($text === '' || mb_strlen($text) > 600 || !CosyVoiceService::canAttempt()) {
|
||||
http_response_code($text === '' || mb_strlen($text) > 600 ? 422 : 503);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'code' => 1,
|
||||
'message' => $text === ''
|
||||
? '语音内容不能为空'
|
||||
: (mb_strlen($text) > 600 ? '单次语音内容不能超过 600 个字符' : 'CosyVoice 暂时不可用'),
|
||||
'data' => null,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
@ini_set('zlib.output_compression', '0');
|
||||
ignore_user_abort(false);
|
||||
OpenAIService::sseHeaders();
|
||||
|
||||
$persona = CosyVoiceService::getPersona();
|
||||
OpenAIService::sseEvent('meta', [
|
||||
'provider' => 'cosyvoice',
|
||||
'request_id' => $requestId,
|
||||
'cancel_url' => rtrim((string) ($persona['base_url'] ?? ''), '/')
|
||||
. '/cancel/' . rawurlencode($requestId),
|
||||
'format' => 'pcm_s16le',
|
||||
'sample_rate' => (int) ($persona['sample_rate'] ?? 24000),
|
||||
'channels' => 1,
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = CosyVoiceService::streamSpeech($text, static function (string $pcm): void {
|
||||
OpenAIService::sseEvent('audio', [
|
||||
'audio' => base64_encode($pcm),
|
||||
]);
|
||||
}, $requestId);
|
||||
|
||||
if (empty($result['aborted'])) {
|
||||
OpenAIService::sseEvent('done', [
|
||||
'bytes' => (int) ($result['bytes'] ?? 0),
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $error) {
|
||||
Log::warning('CosyVoice stream failed: ' . $error->getMessage());
|
||||
OpenAIService::sseEvent('error', [
|
||||
'message' => $error->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
public function speechCancel()
|
||||
{
|
||||
$this->authUser();
|
||||
$requestId = trim((string) $this->request->post('request_id', ''));
|
||||
if (!preg_match('/^[A-Za-z0-9._:-]{8,128}$/', $requestId)) {
|
||||
return $this->error('语音请求标识无效', 422);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'cancelled' => CosyVoiceService::cancelSpeech($requestId),
|
||||
'request_id' => $requestId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function completions()
|
||||
{
|
||||
$user = $this->authUser();
|
||||
@@ -29,6 +157,7 @@ class Chat extends BaseApi
|
||||
$attachments = $input['attachments'] ?? [];
|
||||
$agentId = trim((string) ($input['agent_id'] ?? ''));
|
||||
$imageTool = trim((string) ($input['image_tool'] ?? ''));
|
||||
$voiceMode = !empty($input['voice_mode']);
|
||||
$stream = ($input['stream'] ?? true) !== false;
|
||||
|
||||
$allowedImageTools = ['enhance', 'erase', 'watermark', 'cutout', 'outpaint', 'replace', 'text', 'restore', 'creative', 'commit'];
|
||||
@@ -153,7 +282,7 @@ class Chat extends BaseApi
|
||||
$history = Message::where('conversation_id', $conversationId)
|
||||
->field('role,content,attachments')
|
||||
->order('id', 'desc')
|
||||
->limit(50)
|
||||
->limit($voiceMode ? 16 : 50)
|
||||
->select()
|
||||
->toArray();
|
||||
$history = array_reverse($history);
|
||||
@@ -167,6 +296,16 @@ class Chat extends BaseApi
|
||||
}
|
||||
|
||||
$apiMessages = $this->buildApiMessages($history, $model);
|
||||
if ($voiceMode) {
|
||||
$voicePersona = CosyVoiceService::getPersona();
|
||||
$personaName = trim((string) ($voicePersona['name'] ?? 'AI 客服')) ?: 'AI 客服';
|
||||
$personaPrompt = trim((string) ($voicePersona['role_prompt'] ?? ''));
|
||||
array_unshift($apiMessages, [
|
||||
'role' => 'system',
|
||||
'content' => '你是名为“' . $personaName . '”的 AI 客服。人物设定:' . $personaPrompt
|
||||
. ' 当前正在进行低延迟实时语音对话。像真人客服一样先回应用户的真实诉求,语气口语化、有耐心、有适度共情,不复述问题,不使用 Markdown 列表,不说“作为 AI”。先给结论,通常控制在 1 到 3 句;信息不足时每轮只追问一个最关键的问题,除非用户明确要求详细说明。',
|
||||
]);
|
||||
}
|
||||
$agentImageActionAllowed = false;
|
||||
if ($agent) {
|
||||
$agentSystemPrompt = $agent['system_prompt'];
|
||||
@@ -1590,6 +1729,12 @@ class Chat extends BaseApi
|
||||
},
|
||||
function (string $message) use (&$streamError) {
|
||||
$streamError = $message;
|
||||
},
|
||||
function () use ($conversationId, &$resolvedExternalConversationId) {
|
||||
$resolvedExternalConversationId = null;
|
||||
ConversationModel::where('id', $conversationId)->update([
|
||||
'external_conversation_id' => null,
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1723,6 +1868,10 @@ class Chat extends BaseApi
|
||||
ConversationModel::where('id', $conversationId)->update([
|
||||
'external_conversation_id' => $resolvedExternalConversationId,
|
||||
]);
|
||||
} elseif (!empty($result['conversation_reset'])) {
|
||||
ConversationModel::where('id', $conversationId)->update([
|
||||
'external_conversation_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$imageAction = $agent
|
||||
|
||||
@@ -93,6 +93,14 @@ class Conversation extends BaseApi
|
||||
} else {
|
||||
$data['model_id'] = (int) $mid;
|
||||
}
|
||||
|
||||
$currentModelId = $conversation->model_id === null
|
||||
? null
|
||||
: (int) $conversation->model_id;
|
||||
if ($data['model_id'] !== $currentModelId) {
|
||||
// Dify conversation_id 只属于创建它的应用/模型;切换模型后不可复用。
|
||||
$data['external_conversation_id'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace app\controller\api;
|
||||
|
||||
use app\model\AiModel;
|
||||
use app\service\AgentCatalog;
|
||||
use app\service\CosyVoiceService;
|
||||
use app\service\SettingsService;
|
||||
|
||||
class Settings extends BaseApi
|
||||
@@ -20,6 +21,7 @@ class Settings extends BaseApi
|
||||
'site_name' => SettingsService::get('site_name', 'AI Chat'),
|
||||
'allow_register' => $allow === true || $allow === 'true',
|
||||
'features' => SettingsService::getFeatures(),
|
||||
'voice_persona' => CosyVoiceService::publicPersona(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use think\facade\Cache;
|
||||
|
||||
class CosyVoiceService
|
||||
{
|
||||
private const MODES = ['sft', 'instruct', 'zero_shot', 'cross_lingual', 'instruct2'];
|
||||
private const FALLBACK_VOICES = [
|
||||
'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'nova',
|
||||
'onyx', 'sage', 'shimmer', 'verse', 'marin', 'cedar',
|
||||
];
|
||||
|
||||
public static function getPersona(): array
|
||||
{
|
||||
$defaults = [
|
||||
'enabled' => (bool) config('cosyvoice.enabled', true),
|
||||
'name' => '小暖',
|
||||
'greeting' => '您好,我是 AI 客服小暖,请问有什么可以帮您?',
|
||||
'role_prompt' => '温暖、专业、耐心,像经验丰富的真人客服一样理解用户的真实诉求。',
|
||||
'base_url' => (string) config('cosyvoice.base_url', 'http://127.0.0.1:50000'),
|
||||
'mode' => (string) config('cosyvoice.mode', 'sft'),
|
||||
'speaker' => (string) config('cosyvoice.speaker', '中文女'),
|
||||
'instruct_text' => (string) config('cosyvoice.instruct_text', ''),
|
||||
'prompt_text' => (string) config('cosyvoice.prompt_text', ''),
|
||||
'prompt_wav' => (string) config('cosyvoice.prompt_wav', ''),
|
||||
'prompt_wav_name' => '',
|
||||
'sample_rate' => (int) config('cosyvoice.sample_rate', 22050),
|
||||
'fallback_voice' => 'marin',
|
||||
'connect_timeout_ms' => (int) config('cosyvoice.connect_timeout_ms', 800),
|
||||
'timeout_seconds' => (int) config('cosyvoice.timeout_seconds', 8),
|
||||
'failure_ttl' => (int) config('cosyvoice.failure_ttl', 20),
|
||||
];
|
||||
$stored = SettingsService::get('voice_persona', []);
|
||||
|
||||
return array_merge($defaults, is_array($stored) ? $stored : []);
|
||||
}
|
||||
|
||||
public static function publicPersona(): array
|
||||
{
|
||||
$persona = self::getPersona();
|
||||
return [
|
||||
'name' => $persona['name'],
|
||||
'greeting' => $persona['greeting'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function normalizePersona(array $input, ?array $current = null): array
|
||||
{
|
||||
$current ??= self::getPersona();
|
||||
$mode = trim((string) ($input['mode'] ?? $current['mode'] ?? 'sft'));
|
||||
if (!in_array($mode, self::MODES, true)) {
|
||||
$mode = 'sft';
|
||||
}
|
||||
|
||||
$baseUrl = rtrim(trim((string) ($input['base_url'] ?? $current['base_url'] ?? '')), '/');
|
||||
if ($baseUrl !== '' && !preg_match('#^https?://#i', $baseUrl)) {
|
||||
$baseUrl = (string) ($current['base_url'] ?? '');
|
||||
}
|
||||
|
||||
$fallbackVoice = trim((string) ($input['fallback_voice'] ?? $current['fallback_voice'] ?? 'marin'));
|
||||
if (!in_array($fallbackVoice, self::FALLBACK_VOICES, true)) {
|
||||
$fallbackVoice = 'marin';
|
||||
}
|
||||
|
||||
$promptWav = self::safePromptPath((string) ($input['prompt_wav'] ?? $current['prompt_wav'] ?? ''));
|
||||
$sampleRate = (int) ($input['sample_rate'] ?? $current['sample_rate'] ?? 22050);
|
||||
if (!in_array($sampleRate, [16000, 22050, 24000, 44100, 48000], true)) {
|
||||
$sampleRate = 22050;
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => filter_var($input['enabled'] ?? $current['enabled'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
||||
'name' => self::limitedText($input['name'] ?? $current['name'] ?? '小暖', 40),
|
||||
'greeting' => self::limitedText($input['greeting'] ?? $current['greeting'] ?? '', 200),
|
||||
'role_prompt' => self::limitedText($input['role_prompt'] ?? $current['role_prompt'] ?? '', 2000),
|
||||
'base_url' => $baseUrl,
|
||||
'mode' => $mode,
|
||||
'speaker' => self::limitedText($input['speaker'] ?? $current['speaker'] ?? '中文女', 80),
|
||||
'instruct_text' => self::limitedText($input['instruct_text'] ?? $current['instruct_text'] ?? '', 1000),
|
||||
'prompt_text' => self::limitedText($input['prompt_text'] ?? $current['prompt_text'] ?? '', 1500),
|
||||
'prompt_wav' => $promptWav,
|
||||
'prompt_wav_name' => self::limitedText($input['prompt_wav_name'] ?? $current['prompt_wav_name'] ?? '', 180),
|
||||
'sample_rate' => $sampleRate,
|
||||
'fallback_voice' => $fallbackVoice,
|
||||
'connect_timeout_ms' => max(200, min(5000, (int) ($input['connect_timeout_ms'] ?? 800))),
|
||||
'timeout_seconds' => max(2, min(60, (int) ($input['timeout_seconds'] ?? 8))),
|
||||
'failure_ttl' => max(5, min(300, (int) ($input['failure_ttl'] ?? 20))),
|
||||
];
|
||||
}
|
||||
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
$persona = self::getPersona();
|
||||
return !empty($persona['enabled']) && trim((string) $persona['base_url']) !== '';
|
||||
}
|
||||
|
||||
public static function canAttempt(): bool
|
||||
{
|
||||
if (!self::isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !Cache::get(self::failureCacheKey(self::getPersona()), false);
|
||||
}
|
||||
|
||||
public static function clearFailure(?array $persona = null): void
|
||||
{
|
||||
Cache::delete(self::failureCacheKey($persona ?? self::getPersona()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用官方 FastAPI 服务。上游返回裸 PCM16 流,这里封装为浏览器可播放的 WAV。
|
||||
*
|
||||
* @return array{audio: string, content_type: string, provider: string}
|
||||
*/
|
||||
public static function speech(string $text): array
|
||||
{
|
||||
$persona = self::getPersona();
|
||||
if (empty($persona['enabled']) || trim((string) $persona['base_url']) === '') {
|
||||
throw new \RuntimeException('CosyVoice 未启用');
|
||||
}
|
||||
|
||||
$mode = trim((string) $persona['mode']);
|
||||
if (!in_array($mode, self::MODES, true)) {
|
||||
throw new \RuntimeException('CosyVoice 模式无效: ' . $mode);
|
||||
}
|
||||
|
||||
$fields = self::requestFields($mode, $text, $persona);
|
||||
$url = rtrim((string) $persona['base_url'], '/') . '/inference_' . $mode;
|
||||
$headers = ['Accept: application/octet-stream'];
|
||||
$apiKey = trim((string) config('cosyvoice.api_key', ''));
|
||||
if ($apiKey !== '') {
|
||||
$headers[] = 'Authorization: Bearer ' . $apiKey;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $fields,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT_MS => (int) $persona['connect_timeout_ms'],
|
||||
CURLOPT_TIMEOUT => (int) $persona['timeout_seconds'],
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
$pcm = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = strtolower((string) (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: ''));
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($pcm === false) {
|
||||
self::rememberFailure($persona);
|
||||
throw new \RuntimeException('CosyVoice 连接失败: ' . ($curlError ?: '网络不可达'));
|
||||
}
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
self::rememberFailure($persona);
|
||||
$detail = self::errorDetail($pcm);
|
||||
throw new \RuntimeException('CosyVoice 返回 HTTP ' . $httpCode . ($detail ? ': ' . $detail : ''));
|
||||
}
|
||||
if (str_contains($contentType, 'json')) {
|
||||
self::rememberFailure($persona);
|
||||
throw new \RuntimeException('CosyVoice 返回了错误响应: ' . (self::errorDetail($pcm) ?: '未知错误'));
|
||||
}
|
||||
if (strlen($pcm) < 2) {
|
||||
self::rememberFailure($persona);
|
||||
throw new \RuntimeException('CosyVoice 返回了空音频');
|
||||
}
|
||||
|
||||
// PCM16 每个采样占两个字节;丢弃异常的尾部半个采样。
|
||||
if (strlen($pcm) % 2 !== 0) {
|
||||
$pcm = substr($pcm, 0, -1);
|
||||
}
|
||||
|
||||
Cache::delete(self::failureCacheKey($persona));
|
||||
return [
|
||||
'audio' => self::pcm16ToWav($pcm, (int) $persona['sample_rate']),
|
||||
'content_type' => 'audio/wav',
|
||||
'provider' => 'cosyvoice',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 CosyVoice 上游产生的 PCM16 数据块原样向下游推送,避免等待整段音频生成完成。
|
||||
*
|
||||
* @return array{bytes: int, sample_rate: int, provider: string, aborted: bool}
|
||||
*/
|
||||
public static function streamSpeech(string $text, callable $onChunk, string $requestId = ''): array
|
||||
{
|
||||
$persona = self::getPersona();
|
||||
if (empty($persona['enabled']) || trim((string) $persona['base_url']) === '') {
|
||||
throw new \RuntimeException('CosyVoice 未启用');
|
||||
}
|
||||
|
||||
$mode = trim((string) $persona['mode']);
|
||||
if (!in_array($mode, self::MODES, true)) {
|
||||
throw new \RuntimeException('CosyVoice 模式无效: ' . $mode);
|
||||
}
|
||||
|
||||
$fields = self::requestFields($mode, $text, $persona);
|
||||
if ($requestId !== '') {
|
||||
$fields['request_id'] = $requestId;
|
||||
}
|
||||
$url = rtrim((string) $persona['base_url'], '/') . '/inference_' . $mode;
|
||||
$headers = ['Accept: application/octet-stream'];
|
||||
$apiKey = trim((string) config('cosyvoice.api_key', ''));
|
||||
if ($apiKey !== '') {
|
||||
$headers[] = 'Authorization: Bearer ' . $apiKey;
|
||||
}
|
||||
|
||||
$httpCode = 0;
|
||||
$contentType = '';
|
||||
$errorBody = '';
|
||||
$bytes = 0;
|
||||
$aborted = false;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $fields,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => false,
|
||||
CURLOPT_HEADERFUNCTION => function ($ch, string $header) use (&$httpCode, &$contentType): int {
|
||||
if (preg_match('/^HTTP\/\d+(?:\.\d+)?\s+(\d+)/i', trim($header), $matches)) {
|
||||
$httpCode = (int) $matches[1];
|
||||
} elseif (stripos($header, 'Content-Type:') === 0) {
|
||||
$contentType = strtolower(trim(substr($header, strlen('Content-Type:'))));
|
||||
}
|
||||
return strlen($header);
|
||||
},
|
||||
CURLOPT_WRITEFUNCTION => function ($ch, string $chunk) use (
|
||||
&$httpCode,
|
||||
&$contentType,
|
||||
&$errorBody,
|
||||
&$bytes,
|
||||
&$aborted,
|
||||
$onChunk
|
||||
): int {
|
||||
if ($httpCode < 200 || $httpCode >= 300 || str_contains($contentType, 'json')) {
|
||||
$errorBody .= $chunk;
|
||||
return strlen($chunk);
|
||||
}
|
||||
if (connection_aborted()) {
|
||||
$aborted = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$bytes += strlen($chunk);
|
||||
$onChunk($chunk);
|
||||
return strlen($chunk);
|
||||
},
|
||||
CURLOPT_CONNECTTIMEOUT_MS => (int) $persona['connect_timeout_ms'],
|
||||
CURLOPT_TIMEOUT => (int) $persona['timeout_seconds'],
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
$curlError = curl_error($ch);
|
||||
$curlErrno = curl_errno($ch);
|
||||
if ($httpCode === 0) {
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
if ($aborted || ($result === false && $curlErrno === CURLE_WRITE_ERROR && connection_aborted())) {
|
||||
return [
|
||||
'bytes' => $bytes,
|
||||
'sample_rate' => (int) $persona['sample_rate'],
|
||||
'provider' => 'cosyvoice',
|
||||
'aborted' => true,
|
||||
];
|
||||
}
|
||||
if ($result === false) {
|
||||
self::rememberFailure($persona);
|
||||
throw new \RuntimeException('CosyVoice 流连接失败: ' . ($curlError ?: '网络不可达'));
|
||||
}
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
self::rememberFailure($persona);
|
||||
$detail = self::errorDetail($errorBody);
|
||||
throw new \RuntimeException('CosyVoice 返回 HTTP ' . $httpCode . ($detail ? ': ' . $detail : ''));
|
||||
}
|
||||
if (str_contains($contentType, 'json')) {
|
||||
self::rememberFailure($persona);
|
||||
throw new \RuntimeException('CosyVoice 返回了错误响应: ' . (self::errorDetail($errorBody) ?: '未知错误'));
|
||||
}
|
||||
if ($bytes < 2) {
|
||||
self::rememberFailure($persona);
|
||||
throw new \RuntimeException('CosyVoice 返回了空音频');
|
||||
}
|
||||
|
||||
Cache::delete(self::failureCacheKey($persona));
|
||||
return [
|
||||
'bytes' => $bytes,
|
||||
'sample_rate' => (int) $persona['sample_rate'],
|
||||
'provider' => 'cosyvoice',
|
||||
'aborted' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public static function cancelSpeech(string $requestId): bool
|
||||
{
|
||||
$persona = self::getPersona();
|
||||
$requestId = trim($requestId);
|
||||
if ($requestId === '' || empty($persona['enabled']) || trim((string) $persona['base_url']) === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = rtrim((string) $persona['base_url'], '/') . '/cancel/' . rawurlencode($requestId);
|
||||
$headers = ['Accept: application/json'];
|
||||
$apiKey = trim((string) config('cosyvoice.api_key', ''));
|
||||
if ($apiKey !== '') {
|
||||
$headers[] = 'Authorization: Bearer ' . $apiKey;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => '',
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT_MS => min(800, (int) $persona['connect_timeout_ms']),
|
||||
CURLOPT_TIMEOUT_MS => 1500,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($body === false || $httpCode < 200 || $httpCode >= 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = json_decode($body, true);
|
||||
return is_array($payload) && !empty($payload['cancelled']);
|
||||
}
|
||||
|
||||
private static function requestFields(string $mode, string $text, array $persona): array
|
||||
{
|
||||
$fields = ['tts_text' => $text];
|
||||
$speaker = trim((string) $persona['speaker']);
|
||||
$instruct = trim((string) $persona['instruct_text']);
|
||||
|
||||
if (in_array($mode, ['sft', 'instruct'], true)) {
|
||||
$fields['spk_id'] = $speaker;
|
||||
}
|
||||
if (in_array($mode, ['instruct', 'instruct2'], true)) {
|
||||
$fields['instruct_text'] = $instruct;
|
||||
}
|
||||
if (in_array($mode, ['zero_shot', 'instruct2'], true)) {
|
||||
$fields['prompt_text'] = trim((string) $persona['prompt_text']);
|
||||
}
|
||||
if (in_array($mode, ['zero_shot', 'cross_lingual', 'instruct2'], true)) {
|
||||
$promptWav = trim((string) $persona['prompt_wav']);
|
||||
if ($promptWav === '' || !is_file($promptWav)) {
|
||||
throw new \RuntimeException('CosyVoice 音色样本不存在: ' . $promptWav);
|
||||
}
|
||||
$fields['prompt_wav'] = new \CURLFile($promptWav, 'audio/wav', basename($promptWav));
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private static function pcm16ToWav(string $pcm, int $sampleRate): string
|
||||
{
|
||||
$channels = 1;
|
||||
$bitsPerSample = 16;
|
||||
$dataSize = strlen($pcm);
|
||||
$blockAlign = (int) ($channels * $bitsPerSample / 8);
|
||||
$byteRate = $sampleRate * $blockAlign;
|
||||
|
||||
return 'RIFF'
|
||||
. pack('V', 36 + $dataSize)
|
||||
. 'WAVEfmt '
|
||||
. pack('VvvVVvv', 16, 1, $channels, $sampleRate, $byteRate, $blockAlign, $bitsPerSample)
|
||||
. 'data'
|
||||
. pack('V', $dataSize)
|
||||
. $pcm;
|
||||
}
|
||||
|
||||
private static function errorDetail(string $body): string
|
||||
{
|
||||
$data = json_decode($body, true);
|
||||
if (is_array($data)) {
|
||||
$detail = $data['detail'] ?? $data['message'] ?? $data['error'] ?? '';
|
||||
if (is_array($detail)) {
|
||||
return json_encode($detail, JSON_UNESCAPED_UNICODE) ?: '';
|
||||
}
|
||||
return trim((string) $detail);
|
||||
}
|
||||
|
||||
return mb_substr(trim(strip_tags($body)), 0, 240);
|
||||
}
|
||||
|
||||
private static function rememberFailure(array $persona): void
|
||||
{
|
||||
Cache::set(
|
||||
self::failureCacheKey($persona),
|
||||
true,
|
||||
(int) $persona['failure_ttl']
|
||||
);
|
||||
}
|
||||
|
||||
private static function failureCacheKey(array $persona): string
|
||||
{
|
||||
return 'cosyvoice_unavailable_' . md5((string) $persona['base_url']);
|
||||
}
|
||||
|
||||
private static function limitedText(mixed $value, int $maxLength): string
|
||||
{
|
||||
return mb_substr(trim((string) $value), 0, $maxLength);
|
||||
}
|
||||
|
||||
private static function safePromptPath(string $path): string
|
||||
{
|
||||
$path = trim($path);
|
||||
if ($path === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$realPath = realpath($path);
|
||||
$allowedRoot = realpath(root_path() . 'storage' . DIRECTORY_SEPARATOR . 'cosyvoice');
|
||||
if (!$realPath || !$allowedRoot || !is_file($realPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$normalizedPath = strtolower(str_replace('\\', '/', $realPath));
|
||||
$normalizedRoot = rtrim(strtolower(str_replace('\\', '/', $allowedRoot)), '/') . '/';
|
||||
return str_starts_with($normalizedPath, $normalizedRoot) ? $realPath : '';
|
||||
}
|
||||
}
|
||||
@@ -21,61 +21,147 @@ class DifyService
|
||||
public static function chat(AiModel $model, string $query, array $files, ?string $conversationId, string $userId): array
|
||||
{
|
||||
$url = rtrim($model->api_base_url, '/') . '/chat-messages';
|
||||
$payload = self::buildPayload($query, $files, $conversationId, $userId, false);
|
||||
$conversationReset = false;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $model->api_key,
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 120,
|
||||
CURLOPT_CONNECTTIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
for ($attempt = 0; $attempt < 2; $attempt++) {
|
||||
$requestConversationId = $attempt === 0 ? $conversationId : null;
|
||||
$payload = self::buildPayload($query, $files, $requestConversationId, $userId, false);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $model->api_key,
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 120,
|
||||
CURLOPT_CONNECTTIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
if ($response === false) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => 'Dify 请求失败: ' . ($curlError ?: '网络错误'),
|
||||
'data' => null,
|
||||
], 502));
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => 'Dify 请求失败: ' . ($curlError ?: '网络错误'),
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$detail = self::parseErrorBody($response) ?: ('HTTP ' . $httpCode);
|
||||
if (
|
||||
$attempt === 0 &&
|
||||
$conversationId &&
|
||||
self::isConversationNotFoundError($response . ' ' . $detail)
|
||||
) {
|
||||
$conversationReset = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => self::humanizeError('Dify 请求失败: ' . $detail . self::urlHint($httpCode)),
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (!$data) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => 'Dify 响应解析失败',
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
return [
|
||||
'answer' => $data['answer'] ?? '',
|
||||
'conversation_id' => $data['conversation_id'] ?? null,
|
||||
'conversation_reset' => $conversationReset,
|
||||
'tokens' => $data['metadata']['usage']['total_tokens'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$detail = self::parseErrorBody($response) ?: ('HTTP ' . $httpCode);
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => self::humanizeError('Dify 请求失败: ' . $detail . self::urlHint($httpCode)),
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (!$data) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => 'Dify 响应解析失败',
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
return [
|
||||
'answer' => $data['answer'] ?? '',
|
||||
'conversation_id' => $data['conversation_id'] ?? null,
|
||||
'tokens' => $data['metadata']['usage']['total_tokens'] ?? 0,
|
||||
];
|
||||
throw new \RuntimeException('Dify 会话恢复失败');
|
||||
}
|
||||
|
||||
public static function streamChat(
|
||||
AiModel $model,
|
||||
string $query,
|
||||
array $files,
|
||||
?string $conversationId,
|
||||
string $userId,
|
||||
callable $onChunk,
|
||||
callable $onDone,
|
||||
?callable $onError = null,
|
||||
?callable $onConversationReset = null
|
||||
): void {
|
||||
$emittedContent = false;
|
||||
$attemptError = null;
|
||||
$attemptDone = null;
|
||||
|
||||
$chunkProxy = function (string $delta) use (&$emittedContent, $onChunk) {
|
||||
$emittedContent = true;
|
||||
$onChunk($delta);
|
||||
};
|
||||
$doneProxy = function (?string $newConversationId, int $tokens) use (&$attemptDone) {
|
||||
$attemptDone = [$newConversationId, $tokens];
|
||||
};
|
||||
$errorProxy = function (string $message) use (&$attemptError) {
|
||||
$attemptError = $message;
|
||||
};
|
||||
|
||||
self::streamChatAttempt(
|
||||
$model,
|
||||
$query,
|
||||
$files,
|
||||
$conversationId,
|
||||
$userId,
|
||||
$chunkProxy,
|
||||
$doneProxy,
|
||||
$errorProxy
|
||||
);
|
||||
|
||||
if (
|
||||
$attemptDone === null &&
|
||||
!$emittedContent &&
|
||||
$conversationId &&
|
||||
self::isConversationNotFoundError((string) $attemptError)
|
||||
) {
|
||||
if ($onConversationReset) {
|
||||
$onConversationReset();
|
||||
}
|
||||
$attemptError = null;
|
||||
self::streamChatAttempt(
|
||||
$model,
|
||||
$query,
|
||||
$files,
|
||||
null,
|
||||
$userId,
|
||||
$chunkProxy,
|
||||
$doneProxy,
|
||||
$errorProxy
|
||||
);
|
||||
}
|
||||
|
||||
if ($attemptDone !== null) {
|
||||
$onDone($attemptDone[0], $attemptDone[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($onError && $attemptError !== null) {
|
||||
$onError($attemptError);
|
||||
}
|
||||
}
|
||||
|
||||
private static function streamChatAttempt(
|
||||
AiModel $model,
|
||||
string $query,
|
||||
array $files,
|
||||
@@ -373,6 +459,25 @@ class DifyService
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function isConversationNotFoundError(string $message): bool
|
||||
{
|
||||
$message = mb_strtolower($message);
|
||||
foreach ([
|
||||
'conversation not exists',
|
||||
'conversation does not exist',
|
||||
'conversation not found',
|
||||
'conversation_not_exists',
|
||||
'conversation_not_found',
|
||||
'dify 会话已失效',
|
||||
] as $needle) {
|
||||
if (str_contains($message, $needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function parseErrorBody(?string $body): ?string
|
||||
{
|
||||
if (!$body) {
|
||||
@@ -390,6 +495,10 @@ class DifyService
|
||||
*/
|
||||
public static function humanizeError(string $message): string
|
||||
{
|
||||
if (self::isConversationNotFoundError($message)) {
|
||||
return 'Dify 会话已失效,系统创建新会话后仍未恢复,请稍后重新发送。';
|
||||
}
|
||||
|
||||
if (str_contains($message, "Unsupported chat content part type: 'file'")
|
||||
|| str_contains($message, 'Unsupported chat content part type')) {
|
||||
return 'Dify 模型层仍不接受 file 类型。请确认 Dify 应用已开启文档上传,且 files.type 使用 document(不是 file)。'
|
||||
|
||||
@@ -55,6 +55,35 @@ class OpenAIService
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语音合成必须使用 OpenAI 兼容协议模型。Dify/ComfyUI 的 API 地址不提供
|
||||
* /audio/speech,因此所选对话模型不兼容时自动回落到已启用的 OpenAI 模型。
|
||||
*/
|
||||
public static function getSpeechModel(?int $preferredModelId = null): AiModel
|
||||
{
|
||||
$model = null;
|
||||
if ($preferredModelId) {
|
||||
$model = AiModel::where('id', $preferredModelId)
|
||||
->where('enabled', 1)
|
||||
->where('provider', 'openai')
|
||||
->find();
|
||||
}
|
||||
|
||||
if (!$model) {
|
||||
$model = AiModel::where('enabled', 1)
|
||||
->where('provider', 'openai')
|
||||
->order('is_default', 'desc')
|
||||
->order('sort_order')
|
||||
->find();
|
||||
}
|
||||
|
||||
if (!$model) {
|
||||
self::throwUnavailableModel('未配置支持语音合成的 OpenAI 兼容模型');
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public static function getImageModel(?int $preferredModelId = null): AiModel
|
||||
{
|
||||
$model = null;
|
||||
@@ -252,6 +281,106 @@ class OpenAIService
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用神经语音模型生成短句 WAV。短句由前端在文本流式输出期间提前提交,
|
||||
* WAV 则避免浏览器额外的解码启动开销。
|
||||
*
|
||||
* @return array{audio: string, content_type: string}
|
||||
*/
|
||||
public static function speech(AiModel $model, string $input, string $voice = 'marin'): array
|
||||
{
|
||||
$extra = is_array($model->extra_config ?? null) ? $model->extra_config : [];
|
||||
$ttsModel = trim((string) ($extra['tts_model'] ?? 'gpt-4o-mini-tts'));
|
||||
$ttsVoice = trim((string) ($extra['tts_voice'] ?? $voice));
|
||||
$instructions = trim((string) ($extra['tts_instructions'] ?? (
|
||||
'Speak in natural, warm, conversational Mandarin Chinese. '
|
||||
. 'Use relaxed pacing, subtle emotion, human-like phrasing and short natural pauses. '
|
||||
. 'Avoid an announcer, customer-service, robotic, or overly enthusiastic tone.'
|
||||
)));
|
||||
|
||||
$allowedVoices = [
|
||||
'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'nova',
|
||||
'onyx', 'sage', 'shimmer', 'verse', 'marin', 'cedar',
|
||||
];
|
||||
if (!in_array($ttsVoice, $allowedVoices, true)) {
|
||||
$ttsVoice = 'marin';
|
||||
}
|
||||
if ($ttsModel === '') {
|
||||
$ttsModel = 'gpt-4o-mini-tts';
|
||||
}
|
||||
|
||||
$legacyTts = in_array($ttsModel, ['tts-1', 'tts-1-hd'], true);
|
||||
if ($legacyTts && !in_array($ttsVoice, ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'], true)) {
|
||||
$ttsVoice = 'nova';
|
||||
}
|
||||
|
||||
$url = rtrim((string) $model->api_base_url, '/') . '/audio/speech';
|
||||
$payload = [
|
||||
'model' => $ttsModel,
|
||||
'input' => $input,
|
||||
'voice' => $ttsVoice,
|
||||
'response_format' => 'wav',
|
||||
];
|
||||
if (!$legacyTts && $instructions !== '') {
|
||||
$payload['instructions'] = $instructions;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Accept: audio/wav',
|
||||
'Authorization: Bearer ' . $model->api_key,
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = (string) (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'audio/wav');
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => '语音合成连接失败: ' . ($curlError ?: '网络不可达'),
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
$detail = self::parseErrorBody($response) ?: ('HTTP ' . $httpCode);
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => '自然语音生成失败: ' . $detail,
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
if ($response === '') {
|
||||
throw new HttpResponseException(json([
|
||||
'code' => 1,
|
||||
'message' => '语音服务返回了空音频',
|
||||
'data' => null,
|
||||
], 502));
|
||||
}
|
||||
|
||||
if (!str_starts_with(strtolower($contentType), 'audio/')) {
|
||||
$contentType = 'audio/wav';
|
||||
}
|
||||
|
||||
return [
|
||||
'audio' => $response,
|
||||
'content_type' => $contentType,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试模型连接是否正常
|
||||
* @return array{success: bool, latency_ms: int, reply: string, model: string}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// 开启后优先请求 CosyVoice;服务异常时会自动回落到 OpenAI / 浏览器语音。
|
||||
'enabled' => filter_var(env('COSYVOICE_ENABLED', true), FILTER_VALIDATE_BOOLEAN),
|
||||
'base_url' => rtrim((string) env('COSYVOICE_BASE_URL', 'http://127.0.0.1:50000'), '/'),
|
||||
// sft / instruct / zero_shot / cross_lingual / instruct2
|
||||
'mode' => (string) env('COSYVOICE_MODE', 'sft'),
|
||||
'speaker' => (string) env('COSYVOICE_SPEAKER', '中文女'),
|
||||
'instruct_text' => (string) env(
|
||||
'COSYVOICE_INSTRUCT',
|
||||
'请用温暖、自然、耐心的中文客服语气表达,语速适中,停顿真实,避免播音腔和夸张情绪。'
|
||||
),
|
||||
'prompt_text' => (string) env('COSYVOICE_PROMPT_TEXT', ''),
|
||||
'prompt_wav' => (string) env(
|
||||
'COSYVOICE_PROMPT_WAV',
|
||||
root_path() . 'storage/cosyvoice/customer-service.wav'
|
||||
),
|
||||
// CosyVoice-300M 常用 22050;CosyVoice2/3 通常应配置为 24000。
|
||||
'sample_rate' => max(8000, (int) env('COSYVOICE_SAMPLE_RATE', 22050)),
|
||||
'connect_timeout_ms' => max(200, (int) env('COSYVOICE_CONNECT_TIMEOUT_MS', 800)),
|
||||
'timeout_seconds' => max(2, (int) env('COSYVOICE_TIMEOUT_SECONDS', 8)),
|
||||
'failure_ttl' => max(5, (int) env('COSYVOICE_FAILURE_TTL', 20)),
|
||||
// 官方 FastAPI 默认无鉴权;经网关暴露时可使用 Bearer Token。
|
||||
'api_key' => (string) env('COSYVOICE_API_KEY', ''),
|
||||
];
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.conv-page[data-v-f8276717]{display:flex;flex-direction:column;height:calc(100vh - 48px);min-height:560px}.page-header[data-v-f8276717]{flex-shrink:0;margin-bottom:16px}.conv-layout[data-v-f8276717]{flex:1;min-height:0;display:grid;grid-template-columns:320px 1fr;gap:16px}.panel[data-v-f8276717]{display:flex;flex-direction:column;min-height:0;padding:0;overflow:hidden}.list-toolbar[data-v-f8276717]{padding:16px;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:8px}.dept-filter[data-v-f8276717],.search-input[data-v-f8276717]{font-size:13px}.conv-list[data-v-f8276717]{flex:1;overflow-y:auto;padding:8px}.conv-item[data-v-f8276717]{width:100%;text-align:left;padding:12px;border-radius:8px;margin-bottom:4px;transition:background .15s}.conv-item[data-v-f8276717]:hover{background:#ffffff0a}.conv-item.active[data-v-f8276717]{background:#6366f126;border:1px solid rgba(99,102,241,.35)}.conv-item-title[data-v-f8276717]{font-size:14px;font-weight:500;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.conv-item-meta[data-v-f8276717],.conv-item-time[data-v-f8276717]{font-size:12px;color:var(--text-muted)}.conv-item-meta[data-v-f8276717]{display:flex;justify-content:space-between;gap:8px;margin-bottom:2px}.list-pagination[data-v-f8276717]{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-top:1px solid var(--border);font-size:13px;color:var(--text-secondary)}.detail-header[data-v-f8276717]{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:16px 20px;border-bottom:1px solid var(--border)}.detail-header h3[data-v-f8276717]{font-size:16px;margin-bottom:4px}.detail-meta[data-v-f8276717]{font-size:13px;color:var(--text-secondary);display:flex;flex-wrap:wrap;gap:4px}.messages-scroll[data-v-f8276717]{flex:1;overflow-y:auto;padding:20px}.messages[data-v-f8276717]{display:flex;flex-direction:column;gap:16px}.message[data-v-f8276717]{display:flex;gap:10px;max-width:85%}.message.user[data-v-f8276717]{flex-direction:row-reverse;align-self:flex-end}.message.assistant[data-v-f8276717]{align-self:flex-start}.message-avatar[data-v-f8276717]{flex-shrink:0;width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;background:var(--bg-tertiary);color:var(--text-secondary)}.message.user .message-avatar[data-v-f8276717]{background:var(--accent);color:#fff}.message-body[data-v-f8276717]{min-width:0}.message-content[data-v-f8276717]{padding:10px 14px;border-radius:12px;background:var(--bg-tertiary);font-size:14px;line-height:1.6;white-space:pre-wrap;word-break:break-word}.message.user .message-content[data-v-f8276717]{background:#6366f133;border:1px solid rgba(99,102,241,.3)}.message-time[data-v-f8276717]{display:block;margin-top:4px;font-size:11px;color:var(--text-muted)}.message.user .message-time[data-v-f8276717]{text-align:right}.attachments[data-v-f8276717]{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.att-image[data-v-f8276717]{max-width:240px;max-height:180px;border-radius:8px;cursor:pointer;border:1px solid var(--border)}.att-link[data-v-f8276717]{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;font-size:13px;color:var(--text-primary)}.att-link[data-v-f8276717]:hover{border-color:var(--accent)}.detail-empty[data-v-f8276717],.empty[data-v-f8276717]{text-align:center;padding:48px 24px;color:var(--text-muted);font-size:14px}.detail-empty[data-v-f8276717]{flex:1;display:flex;align-items:center;justify-content:center}@media(max-width:900px){.conv-layout[data-v-f8276717]{grid-template-columns:1fr;grid-template-rows:280px 1fr}.conv-page[data-v-f8276717]{height:auto;min-height:calc(100vh - 48px)}}
|
||||
@@ -0,0 +1 @@
|
||||
.conv-page[data-v-f98449d7]{display:flex;flex-direction:column;height:calc(100vh - 48px);min-height:560px}.page-header[data-v-f98449d7]{flex-shrink:0;margin-bottom:16px}.conv-layout[data-v-f98449d7]{flex:1;min-height:0;display:grid;grid-template-columns:320px 1fr;gap:16px}.panel[data-v-f98449d7]{display:flex;flex-direction:column;min-height:0;padding:0;overflow:hidden}.list-toolbar[data-v-f98449d7]{padding:16px;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:8px}.dept-filter[data-v-f98449d7],.search-input[data-v-f98449d7]{font-size:13px}.conv-list[data-v-f98449d7]{flex:1;overflow-y:auto;padding:8px}.conv-item[data-v-f98449d7]{width:100%;text-align:left;padding:12px;border-radius:8px;margin-bottom:4px;transition:background .15s}.conv-item[data-v-f98449d7]:hover{background:#ffffff0a}.conv-item.active[data-v-f98449d7]{background:var(--accent-soft);border:1px solid rgba(183,243,107,.28)}.conv-item-title[data-v-f98449d7]{font-size:14px;font-weight:500;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.conv-item-meta[data-v-f98449d7],.conv-item-time[data-v-f98449d7]{font-size:12px;color:var(--text-muted)}.conv-item-meta[data-v-f98449d7]{display:flex;justify-content:space-between;gap:8px;margin-bottom:2px}.list-pagination[data-v-f98449d7]{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-top:1px solid var(--border);font-size:13px;color:var(--text-secondary)}.detail-header[data-v-f98449d7]{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:16px 20px;border-bottom:1px solid var(--border)}.detail-header h3[data-v-f98449d7]{font-size:16px;margin-bottom:4px}.detail-meta[data-v-f98449d7]{font-size:13px;color:var(--text-secondary);display:flex;flex-wrap:wrap;gap:4px}.messages-scroll[data-v-f98449d7]{flex:1;overflow-y:auto;padding:20px}.messages[data-v-f98449d7]{display:flex;flex-direction:column;gap:16px}.message[data-v-f98449d7]{display:flex;gap:10px;max-width:85%}.message.user[data-v-f98449d7]{flex-direction:row-reverse;align-self:flex-end}.message.assistant[data-v-f98449d7]{align-self:flex-start}.message-avatar[data-v-f98449d7]{flex-shrink:0;width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;background:var(--bg-tertiary);color:var(--text-secondary)}.message.user .message-avatar[data-v-f98449d7]{background:var(--accent);color:#fff}.message-body[data-v-f98449d7]{min-width:0}.message-content[data-v-f98449d7]{padding:10px 14px;border-radius:12px;background:var(--bg-tertiary);font-size:14px;line-height:1.6;white-space:pre-wrap;word-break:break-word}.message.user .message-content[data-v-f98449d7]{background:var(--accent-soft);border:1px solid rgba(183,243,107,.24)}.message-time[data-v-f98449d7]{display:block;margin-top:4px;font-size:11px;color:var(--text-muted)}.message.user .message-time[data-v-f98449d7]{text-align:right}.attachments[data-v-f98449d7]{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.att-image[data-v-f98449d7]{max-width:240px;max-height:180px;border-radius:8px;cursor:pointer;border:1px solid var(--border)}.att-link[data-v-f98449d7]{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;font-size:13px;color:var(--text-primary)}.att-link[data-v-f98449d7]:hover{border-color:var(--accent)}.detail-empty[data-v-f98449d7],.empty[data-v-f98449d7]{text-align:center;padding:48px 24px;color:var(--text-muted);font-size:14px}.detail-empty[data-v-f98449d7]{flex:1;display:flex;align-items:center;justify-content:center}@media(max-width:900px){.conv-layout[data-v-f98449d7]{grid-template-columns:1fr;grid-template-rows:280px 1fr}.conv-page[data-v-f98449d7]{height:auto;min-height:calc(100vh - 48px)}}
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as l,g as e,h as d,c as i,a as s,t as o,r,o as p}from"./index-z4tF8s-R.js";const u={class:"stats-grid"},v={class:"stat-card"},c={class:"stat-value"},_={class:"stat-card"},m={class:"stat-value"},g={class:"stat-card"},b={class:"stat-value"},f={class:"stat-card"},y={class:"stat-value"},w={__name:"DashboardView",setup(x){const t=r({users:0,conversations:0,messages:0,today_messages:0});return e(async()=>{const n=await d.get("/admin/stats");t.value=n.data.data}),(n,a)=>(p(),i("div",null,[a[8]||(a[8]=s("div",{class:"page-header"},[s("h2",null,"数据概览"),s("p",null,"系统运行统计数据")],-1)),s("div",u,[s("div",v,[a[0]||(a[0]=s("span",{class:"stat-icon"},"👥",-1)),s("span",c,o(t.value.users),1),a[1]||(a[1]=s("span",{class:"stat-label"},"用户总数",-1))]),s("div",_,[a[2]||(a[2]=s("span",{class:"stat-icon"},"💬",-1)),s("span",m,o(t.value.conversations),1),a[3]||(a[3]=s("span",{class:"stat-label"},"会话总数",-1))]),s("div",g,[a[4]||(a[4]=s("span",{class:"stat-icon"},"📝",-1)),s("span",b,o(t.value.messages),1),a[5]||(a[5]=s("span",{class:"stat-label"},"消息总数",-1))]),s("div",f,[a[6]||(a[6]=s("span",{class:"stat-icon"},"📈",-1)),s("span",y,o(t.value.today_messages),1),a[7]||(a[7]=s("span",{class:"stat-label"},"今日消息",-1))])])]))}},D=l(w,[["__scopeId","data-v-5214a2c8"]]);export{D as default};
|
||||
@@ -0,0 +1 @@
|
||||
.stats-grid[data-v-5a83d2b7]{display:grid;grid-template-columns:repeat(4,minmax(180px,1fr));gap:14px}.stat-card[data-v-5a83d2b7]{display:grid;min-height:178px;grid-template-columns:1fr auto;grid-template-rows:auto 1fr auto;padding:19px;transition:transform .22s var(--ease-spring),border-color .18s ease,box-shadow .18s ease}.stat-card[data-v-5a83d2b7]:hover{border-color:#b7f36b38;box-shadow:inset 0 1px #ffffff0b,0 22px 52px #00000057;transform:translateY(-3px)}.stat-icon[data-v-5a83d2b7]{z-index:1;display:grid;width:42px;height:42px;place-items:center;border:1px solid rgba(183,243,107,.24);border-radius:12px;background:var(--accent-soft);color:var(--accent);box-shadow:inset 0 1px #ffffff0e,0 0 22px #b7f36b0f}.stat-value[data-v-5a83d2b7]{z-index:1;align-self:end;color:var(--text-primary);font-family:Cascadia Code,Consolas,monospace;font-size:clamp(34px,4vw,48px);font-weight:760;font-variant-numeric:tabular-nums;letter-spacing:-.065em;line-height:1}.stat-label[data-v-5a83d2b7]{z-index:1;align-self:end;margin-top:9px;color:var(--text-secondary);font-size:12px;font-weight:600}.stat-index[data-v-5a83d2b7]{z-index:1;grid-column:2;grid-row:1;color:var(--text-muted);font-family:Cascadia Code,Consolas,monospace;font-size:10px;letter-spacing:.08em}@media(max-width:1050px){.stats-grid[data-v-5a83d2b7]{grid-template-columns:repeat(2,minmax(180px,1fr))}}@media(max-width:560px){.stats-grid[data-v-5a83d2b7]{grid-template-columns:1fr}.stat-card[data-v-5a83d2b7]{min-height:150px}}
|
||||
@@ -1 +0,0 @@
|
||||
.stats-grid[data-v-5214a2c8]{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}.stat-card[data-v-5214a2c8]{background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:24px;text-align:center}.stat-icon[data-v-5214a2c8]{font-size:28px;display:block;margin-bottom:8px}.stat-value[data-v-5214a2c8]{display:block;font-size:36px;font-weight:700;color:var(--accent)}.stat-label[data-v-5214a2c8]{font-size:14px;color:var(--text-secondary);margin-top:4px}
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as i,i as d,j as r,c,b as s,a as e,t as n,r as p,o as _,U as o}from"./index-BZEF5hZc.js";const u={class:"stats-grid"},v={class:"stat-card"},m={class:"stat-icon"},g={class:"stat-value"},b={class:"stat-card"},x={class:"stat-icon"},f={class:"stat-value"},y={class:"stat-card"},z={class:"stat-icon"},V={class:"stat-value"},w={class:"stat-card"},B={class:"stat-icon"},D={class:"stat-value"},k={__name:"DashboardView",setup(I){const t=p({users:0,conversations:0,messages:0,today_messages:0});return d(async()=>{const l=await r.get("/admin/stats");t.value=l.data.data}),(l,a)=>(_(),c("div",null,[a[8]||(a[8]=s("div",{class:"page-header"},[s("h2",null,"数据概览"),s("p",null,"系统运行统计数据")],-1)),s("div",u,[s("article",v,[s("span",m,[e(o,{name:"users",size:21})]),s("span",g,n(t.value.users),1),a[0]||(a[0]=s("span",{class:"stat-label"},"用户总数",-1)),a[1]||(a[1]=s("span",{class:"stat-index"},"01",-1))]),s("article",b,[s("span",x,[e(o,{name:"conversations",size:21})]),s("span",f,n(t.value.conversations),1),a[2]||(a[2]=s("span",{class:"stat-label"},"会话总数",-1)),a[3]||(a[3]=s("span",{class:"stat-index"},"02",-1))]),s("article",y,[s("span",z,[e(o,{name:"messages",size:21})]),s("span",V,n(t.value.messages),1),a[4]||(a[4]=s("span",{class:"stat-label"},"消息总数",-1)),a[5]||(a[5]=s("span",{class:"stat-index"},"03",-1))]),s("article",w,[s("span",B,[e(o,{name:"activity",size:21})]),s("span",D,n(t.value.today_messages),1),a[6]||(a[6]=s("span",{class:"stat-label"},"今日消息",-1)),a[7]||(a[7]=s("span",{class:"stat-index"},"04",-1))])])]))}},U=i(k,[["__scopeId","data-v-5a83d2b7"]]);export{U as default};
|
||||
@@ -1 +0,0 @@
|
||||
.toolbar[data-v-1b3fb9c5]{margin-bottom:16px}.empty[data-v-1b3fb9c5]{text-align:center;padding:32px;color:var(--text-muted)}.danger[data-v-1b3fb9c5]{color:#ef4444}
|
||||
@@ -0,0 +1 @@
|
||||
.toolbar[data-v-b11f0247]{margin-bottom:16px}.empty[data-v-b11f0247]{text-align:center;padding:32px;color:var(--text-muted)}.danger[data-v-b11f0247]{color:var(--danger)}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{_ as L,u as N,g as U,h as b,c as s,a as t,i as f,d as u,F as C,j as B,w as E,t as d,b as h,v as $,k as F,r as m,m as j,l as z,o,B as A}from"./index-z4tF8s-R.js";const I={class:"toolbar"},O={class:"panel"},R={class:"data-table"},T=["onClick"],q=["onClick"],G=["onClick"],H={key:0,class:"empty"},J={class:"modal"},K={class:"modal-header"},P={class:"modal-body"},Q={class:"form-group"},W={class:"form-group"},X=["value","disabled"],Y={class:"form-group"},Z={key:0,class:"form-error"},tt={__name:"DepartmentsView",setup(et){const c=N(),v=m([]),k=m([]),p=m(!1),r=m(null),i=m(""),l=j({name:"",parent_id:null,sort_order:0}),D=z(()=>v.value.filter(a=>a.id!==r.value));U(y);async function y(){const e=(await b.get("/admin/departments")).data.data||{};v.value=e.tree||[],k.value=e.list||[]}function V(a){var e;return a&&((e=k.value.find(n=>n.id===a))==null?void 0:e.name)||"-"}function g(a=null){r.value=null,l.name="",l.parent_id=a,l.sort_order=0,i.value="",p.value=!0}function M(a){r.value=a.id,l.name=a.name,l.parent_id=a.parent_id||null,l.sort_order=a.sort_order??0,i.value="",p.value=!0}function _(){p.value=!1}async function x(){if(i.value="",!l.name.trim()){i.value="请填写部门名称";return}const a={name:l.name.trim(),parent_id:l.parent_id,sort_order:l.sort_order};try{r.value?await b.put(`/admin/departments/${r.value}`,a):await b.post("/admin/departments",a),_(),await y()}catch(e){i.value=e.message||"保存失败"}}async function S(a){if(confirm(`确定删除部门「${a.name}」?`))try{await b.delete(`/admin/departments/${a.id}`),await y()}catch(e){alert(e.message||"删除失败")}}return(a,e)=>(o(),s("div",null,[e[9]||(e[9]=t("div",{class:"page-header"},[t("h2",null,"部门管理"),t("p",null,"维护组织部门层级,上级部门可查看下级部门员工聊天记录")],-1)),t("div",I,[f(c).hasButton("btn:dept:create")?(o(),s("button",{key:0,class:"btn btn-primary",onClick:e[0]||(e[0]=n=>g())},"新增部门")):u("",!0)]),t("div",O,[t("table",R,[e[4]||(e[4]=t("thead",null,[t("tr",null,[t("th",null,"部门名称"),t("th",null,"上级部门"),t("th",null,"排序"),t("th",null,"操作")])],-1)),t("tbody",null,[(o(!0),s(C,null,B(v.value,n=>(o(),s("tr",{key:n.id},[t("td",null,[t("span",{style:A({paddingLeft:`${n.depth*16}px`})},d(n.label||n.name),5)]),t("td",null,d(V(n.parent_id)),1),t("td",null,d(n.sort_order??0),1),t("td",null,[f(c).hasButton("btn:dept:create")?(o(),s("button",{key:0,class:"btn btn-ghost",onClick:w=>g(n.id)},"添加下级",8,T)):u("",!0),f(c).hasButton("btn:dept:edit")?(o(),s("button",{key:1,class:"btn btn-ghost",onClick:w=>M(n)},"编辑",8,q)):u("",!0),f(c).hasButton("btn:dept:delete")?(o(),s("button",{key:2,class:"btn btn-ghost danger",onClick:w=>S(n)},"删除",8,G)):u("",!0)])]))),128))])]),v.value.length?u("",!0):(o(),s("p",H,"暂无部门"))]),p.value?(o(),s("div",{key:0,class:"modal-overlay",onClick:E(_,["self"])},[t("div",J,[t("div",K,[t("h3",null,d(r.value?"编辑部门":"新增部门"),1),t("button",{onClick:_},"×")]),t("div",P,[t("div",Q,[e[5]||(e[5]=t("label",null,"部门名称",-1)),h(t("input",{"onUpdate:modelValue":e[1]||(e[1]=n=>l.name=n),class:"form-input"},null,512),[[$,l.name]])]),t("div",W,[e[7]||(e[7]=t("label",null,"上级部门",-1)),h(t("select",{"onUpdate:modelValue":e[2]||(e[2]=n=>l.parent_id=n),class:"form-select"},[e[6]||(e[6]=t("option",{value:null},"无(顶级部门)",-1)),(o(!0),s(C,null,B(D.value,n=>(o(),s("option",{key:n.id,value:n.id,disabled:r.value===n.id},d(n.label||n.name),9,X))),128))],512),[[F,l.parent_id]])]),t("div",Y,[e[8]||(e[8]=t("label",null,"排序",-1)),h(t("input",{"onUpdate:modelValue":e[3]||(e[3]=n=>l.sort_order=n),type:"number",class:"form-input"},null,512),[[$,l.sort_order,void 0,{number:!0}]])]),i.value?(o(),s("p",Z,d(i.value),1)):u("",!0)]),t("div",{class:"modal-footer"},[t("button",{class:"btn btn-ghost",onClick:_},"取消"),t("button",{class:"btn btn-primary",onClick:x},"保存")])])])):u("",!0)]))}},at=L(tt,[["__scopeId","data-v-1b3fb9c5"]]);export{at as default};
|
||||
import{_ as L,u as N,i as U,j as b,c as s,b as t,k as f,f as u,F as w,l as $,w as E,t as d,e as k,v as B,m as F,r as m,p as j,n as z,o,C as A}from"./index-BZEF5hZc.js";const I={class:"toolbar"},O={class:"panel"},R={class:"data-table"},T=["onClick"],q=["onClick"],G=["onClick"],H={key:0,class:"empty"},J={class:"modal"},K={class:"modal-header"},P={class:"modal-body"},Q={class:"form-group"},W={class:"form-group"},X=["value","disabled"],Y={class:"form-group"},Z={key:0,class:"form-error"},tt={__name:"DepartmentsView",setup(et){const c=N(),v=m([]),h=m([]),p=m(!1),r=m(null),i=m(""),l=j({name:"",parent_id:null,sort_order:0}),D=z(()=>v.value.filter(a=>a.id!==r.value));U(y);async function y(){const e=(await b.get("/admin/departments")).data.data||{};v.value=e.tree||[],h.value=e.list||[]}function V(a){var e;return a&&((e=h.value.find(n=>n.id===a))==null?void 0:e.name)||"-"}function g(a=null){r.value=null,l.name="",l.parent_id=a,l.sort_order=0,i.value="",p.value=!0}function M(a){r.value=a.id,l.name=a.name,l.parent_id=a.parent_id||null,l.sort_order=a.sort_order??0,i.value="",p.value=!0}function _(){p.value=!1}async function x(){if(i.value="",!l.name.trim()){i.value="请填写部门名称";return}const a={name:l.name.trim(),parent_id:l.parent_id,sort_order:l.sort_order};try{r.value?await b.put(`/admin/departments/${r.value}`,a):await b.post("/admin/departments",a),_(),await y()}catch(e){i.value=e.message||"保存失败"}}async function S(a){if(confirm(`确定删除部门「${a.name}」?`))try{await b.delete(`/admin/departments/${a.id}`),await y()}catch(e){alert(e.message||"删除失败")}}return(a,e)=>(o(),s("div",null,[e[9]||(e[9]=t("div",{class:"page-header"},[t("h2",null,"部门管理"),t("p",null,"维护组织部门层级,上级部门可查看下级部门员工聊天记录")],-1)),t("div",I,[f(c).hasButton("btn:dept:create")?(o(),s("button",{key:0,class:"btn btn-primary",onClick:e[0]||(e[0]=n=>g())},"新增部门")):u("",!0)]),t("div",O,[t("table",R,[e[4]||(e[4]=t("thead",null,[t("tr",null,[t("th",null,"部门名称"),t("th",null,"上级部门"),t("th",null,"排序"),t("th",null,"操作")])],-1)),t("tbody",null,[(o(!0),s(w,null,$(v.value,n=>(o(),s("tr",{key:n.id},[t("td",null,[t("span",{style:A({paddingLeft:`${n.depth*16}px`})},d(n.label||n.name),5)]),t("td",null,d(V(n.parent_id)),1),t("td",null,d(n.sort_order??0),1),t("td",null,[f(c).hasButton("btn:dept:create")?(o(),s("button",{key:0,class:"btn btn-ghost",onClick:C=>g(n.id)},"添加下级",8,T)):u("",!0),f(c).hasButton("btn:dept:edit")?(o(),s("button",{key:1,class:"btn btn-ghost",onClick:C=>M(n)},"编辑",8,q)):u("",!0),f(c).hasButton("btn:dept:delete")?(o(),s("button",{key:2,class:"btn btn-ghost danger",onClick:C=>S(n)},"删除",8,G)):u("",!0)])]))),128))])]),v.value.length?u("",!0):(o(),s("p",H,"暂无部门"))]),p.value?(o(),s("div",{key:0,class:"modal-overlay",onClick:E(_,["self"])},[t("div",J,[t("div",K,[t("h3",null,d(r.value?"编辑部门":"新增部门"),1),t("button",{onClick:_},"×")]),t("div",P,[t("div",Q,[e[5]||(e[5]=t("label",null,"部门名称",-1)),k(t("input",{"onUpdate:modelValue":e[1]||(e[1]=n=>l.name=n),class:"form-input"},null,512),[[B,l.name]])]),t("div",W,[e[7]||(e[7]=t("label",null,"上级部门",-1)),k(t("select",{"onUpdate:modelValue":e[2]||(e[2]=n=>l.parent_id=n),class:"form-select"},[e[6]||(e[6]=t("option",{value:null},"无(顶级部门)",-1)),(o(!0),s(w,null,$(D.value,n=>(o(),s("option",{key:n.id,value:n.id,disabled:r.value===n.id},d(n.label||n.name),9,X))),128))],512),[[F,l.parent_id]])]),t("div",Y,[e[8]||(e[8]=t("label",null,"排序",-1)),k(t("input",{"onUpdate:modelValue":e[3]||(e[3]=n=>l.sort_order=n),type:"number",class:"form-input"},null,512),[[B,l.sort_order,void 0,{number:!0}]])]),i.value?(o(),s("p",Z,d(i.value),1)):u("",!0)]),t("div",{class:"modal-footer"},[t("button",{class:"btn btn-ghost",onClick:_},"取消"),t("button",{class:"btn btn-primary",onClick:x},"保存")])])])):u("",!0)]))}},at=L(tt,[["__scopeId","data-v-b11f0247"]]);export{at as default};
|
||||
@@ -0,0 +1 @@
|
||||
.login-page[data-v-d42d96d7]{position:relative;min-height:100dvh;display:grid;place-items:center;padding:28px;overflow:hidden;background:radial-gradient(circle at 70% 20%,rgba(183,243,107,.08),transparent 24%),var(--bg-primary)}.login-theme-toggle[data-v-d42d96d7]{position:absolute;top:18px;right:18px;z-index:3}.login-shell[data-v-d42d96d7]{position:relative;display:grid;width:min(880px,100%);grid-template-columns:1.08fr .92fr;overflow:hidden;border:1px solid var(--border-strong);border-radius:22px;background:var(--bg-secondary);box-shadow:inset 0 1px #ffffff0d,var(--shadow)}.login-shell[data-v-d42d96d7]:after{position:absolute;top:0;left:18%;width:36%;height:1px;background:linear-gradient(90deg,transparent,var(--accent),transparent);box-shadow:0 0 17px #b7f36b70;content:""}.login-aside[data-v-d42d96d7]{position:relative;display:flex;min-height:530px;flex-direction:column;justify-content:center;padding:52px;overflow:hidden;border-right:1px solid var(--border);background:linear-gradient(135deg,rgba(183,243,107,.07),transparent 45%),repeating-linear-gradient(135deg,rgba(255,255,255,.018) 0 1px,transparent 1px 14px),#0c0f13}.login-mark[data-v-d42d96d7]{display:grid;width:46px;height:46px;place-items:center;margin-bottom:44px;border:1px solid rgba(183,243,107,.48);border-radius:14px;background:var(--accent);color:#11150e;box-shadow:inset 0 1px #ffffff80,0 5px #55782f,0 14px 28px #6ea6362b}.login-eyebrow[data-v-d42d96d7],.login-version[data-v-d42d96d7]{color:var(--accent);font-family:Cascadia Code,Consolas,monospace;font-size:10px;letter-spacing:.16em}.login-aside h1[data-v-d42d96d7]{margin:15px 0 18px;font-size:clamp(34px,4.6vw,52px);font-weight:760;letter-spacing:-.055em;line-height:1.04;text-wrap:balance}.login-aside p[data-v-d42d96d7]{max-width:32ch;color:var(--text-secondary);font-size:14px;line-height:1.75}.login-version[data-v-d42d96d7]{position:absolute;bottom:28px;left:52px;color:var(--text-muted);font-size:9px}.login-card[data-v-d42d96d7]{display:flex;flex-direction:column;justify-content:center;padding:48px 42px;background:radial-gradient(circle at 100% 0%,rgba(183,243,107,.055),transparent 26%),var(--bg-secondary)}.login-header[data-v-d42d96d7]{margin-bottom:30px}.login-header>span[data-v-d42d96d7]:not(.status-dot){color:var(--text-muted);font-size:11px;font-weight:650;letter-spacing:.08em}.status-dot[data-v-d42d96d7]{display:inline-block;width:7px;height:7px;margin-right:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px #b7f36b99}.login-header h2[data-v-d42d96d7]{margin:12px 0 7px;font-size:26px;letter-spacing:-.04em}.login-header p[data-v-d42d96d7]{color:var(--text-secondary);font-size:13px}.login-btn[data-v-d42d96d7]{width:100%;min-height:46px;margin-top:7px}@media(max-width:720px){.login-page[data-v-d42d96d7]{padding:14px}.login-shell[data-v-d42d96d7]{grid-template-columns:1fr}.login-aside[data-v-d42d96d7]{display:none}.login-card[data-v-d42d96d7]{min-height:520px;padding:38px 25px}}
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as y,u as w,c as u,a as d,b as s,d as p,w as V,e as v,v as g,t as c,f as T,r as t,g as N,h as x,o as m,T as A,U}from"./index-BZEF5hZc.js";const k={class:"login-page"},C={class:"login-shell"},I={class:"login-aside"},L={class:"login-mark"},R={class:"login-card"},S={class:"form-group"},q={class:"form-group"},B={key:0,class:"form-error"},D=["disabled"],M={__name:"LoginView",setup(E){const f=N(),b=x(),_=w(),a=t(""),n=t(""),o=t(""),l=t(!1);async function h(){o.value="",l.value=!0;try{await _.login(a.value,n.value),f.push(b.query.redirect||"/dashboard")}catch(r){o.value=r.message}finally{l.value=!1}}return(r,e)=>(m(),u("div",k,[d(A,{class:"login-theme-toggle"}),s("div",C,[s("div",I,[s("span",L,[d(U,{name:"bolt",size:22})]),e[2]||(e[2]=s("span",{class:"login-eyebrow"},"AI CHAT / ADMIN",-1)),e[3]||(e[3]=s("h1",null,[p("让系统配置"),s("br"),p("保持清晰可控")],-1)),e[4]||(e[4]=s("p",null,"统一管理用户、模型、权限与会话数据。",-1)),e[5]||(e[5]=s("span",{class:"login-version"},"CONTROL SURFACE · V2",-1))]),s("div",R,[e[8]||(e[8]=s("div",{class:"login-header"},[s("span",{class:"status-dot","aria-hidden":"true"}),s("span",null,"安全入口"),s("h2",null,"登录管理后台"),s("p",null,"请使用管理员账户继续")],-1)),s("form",{onSubmit:V(h,["prevent"])},[s("div",S,[e[6]||(e[6]=s("label",null,"账号",-1)),v(s("input",{"onUpdate:modelValue":e[0]||(e[0]=i=>a.value=i),class:"form-input",placeholder:"管理员用户名或邮箱",required:""},null,512),[[g,a.value]])]),s("div",q,[e[7]||(e[7]=s("label",null,"密码",-1)),v(s("input",{"onUpdate:modelValue":e[1]||(e[1]=i=>n.value=i),type:"password",class:"form-input",placeholder:"请输入密码",required:""},null,512),[[g,n.value]])]),o.value?(m(),u("p",B,c(o.value),1)):T("",!0),s("button",{type:"submit",class:"btn btn-primary login-btn",disabled:l.value},c(l.value?"登录中...":"进入控制台"),9,D)],32)])])]))}},z=y(M,[["__scopeId","data-v-d42d96d7"]]);export{z as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as b,u as h,c as i,a as e,w as y,b as d,v as p,t as c,d as w,r as a,e as V,f as x,o as v}from"./index-z4tF8s-R.js";const k={class:"login-page"},q={class:"login-card"},B={class:"form-group"},L={class:"form-group"},S={key:0,class:"form-error"},A=["disabled"],C={__name:"LoginView",setup(D){const f=V(),m=x(),g=h(),l=a(""),n=a(""),o=a(""),t=a(!1);async function _(){o.value="",t.value=!0;try{await g.login(l.value,n.value),f.push(m.query.redirect||"/dashboard")}catch(u){o.value=u.message}finally{t.value=!1}}return(u,s)=>(v(),i("div",k,[e("div",q,[s[4]||(s[4]=e("div",{class:"login-header"},[e("div",{class:"logo"},"⚙️"),e("h1",null,"AI Chat 管理后台"),e("p",null,"请使用管理员账户登录")],-1)),e("form",{onSubmit:y(_,["prevent"])},[e("div",B,[s[2]||(s[2]=e("label",null,"账号",-1)),d(e("input",{"onUpdate:modelValue":s[0]||(s[0]=r=>l.value=r),class:"form-input",placeholder:"管理员用户名或邮箱",required:""},null,512),[[p,l.value]])]),e("div",L,[s[3]||(s[3]=e("label",null,"密码",-1)),d(e("input",{"onUpdate:modelValue":s[1]||(s[1]=r=>n.value=r),type:"password",class:"form-input",placeholder:"请输入密码",required:""},null,512),[[p,n.value]])]),o.value?(v(),i("p",S,c(o.value),1)):w("",!0),e("button",{type:"submit",class:"btn btn-primary login-btn",disabled:t.value},c(t.value?"登录中...":"登录"),9,A)],32)])]))}},M=b(C,[["__scopeId","data-v-7f135f83"]]);export{M as default};
|
||||
@@ -1 +0,0 @@
|
||||
.login-page[data-v-7f135f83]{min-height:100%;display:flex;align-items:center;justify-content:center;padding:24px;background:linear-gradient(135deg,#0f172a,#1e1b4b)}.login-card[data-v-7f135f83]{width:100%;max-width:400px;padding:40px 32px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:16px}.login-header[data-v-7f135f83]{text-align:center;margin-bottom:32px}.logo[data-v-7f135f83]{font-size:48px;margin-bottom:12px}.login-header h1[data-v-7f135f83]{font-size:22px;margin-bottom:8px}.login-header p[data-v-7f135f83]{color:var(--text-secondary);font-size:14px}.login-btn[data-v-7f135f83]{width:100%;padding:12px;margin-top:8px}
|
||||
@@ -1 +0,0 @@
|
||||
.toolbar[data-v-b7a460a8]{margin-bottom:16px}.membership-grid[data-v-b7a460a8]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.membership-card[data-v-b7a460a8]{background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:20px}.card-header[data-v-b7a460a8]{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.card-header h3[data-v-b7a460a8]{font-size:18px}.slug[data-v-b7a460a8]{font-size:12px;color:var(--text-muted);background:var(--bg-tertiary);padding:2px 8px;border-radius:4px}.info-row[data-v-b7a460a8]{display:flex;justify-content:space-between;padding:8px 0;font-size:14px;border-bottom:1px solid var(--border)}.info-row span[data-v-b7a460a8]{color:var(--text-secondary)}.permissions[data-v-b7a460a8]{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px;min-height:24px}.perm-tag[data-v-b7a460a8]{font-size:12px;padding:2px 8px;background:#6366f126;color:var(--accent);border-radius:4px}.field-hint[data-v-b7a460a8]{font-size:12px;color:var(--text-muted)}.card-actions[data-v-b7a460a8]{display:flex;gap:8px;margin-top:16px}.card-actions .btn[data-v-b7a460a8]{flex:1}.check-item[data-v-b7a460a8]{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:14px;cursor:pointer}.danger[data-v-b7a460a8]{color:#ef4444}
|
||||
@@ -0,0 +1 @@
|
||||
.toolbar[data-v-cc64fdb8]{margin-bottom:16px}.membership-grid[data-v-cc64fdb8]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.membership-card[data-v-cc64fdb8]{position:relative;overflow:hidden;background:radial-gradient(circle at 100% 0%,rgba(183,243,107,.05),transparent 30%),var(--bg-secondary);border:1px solid var(--border);border-radius:16px;padding:20px;box-shadow:inset 0 1px #ffffff09,var(--shadow-soft);transition:transform .2s var(--ease-spring),border-color .18s ease,box-shadow .18s ease}.membership-card[data-v-cc64fdb8]:hover{border-color:#b7f36b38;transform:translateY(-3px);box-shadow:inset 0 1px #ffffff0b,0 22px 52px #00000057}.card-header[data-v-cc64fdb8]{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.card-header h3[data-v-cc64fdb8]{font-size:18px}.slug[data-v-cc64fdb8]{font-size:12px;color:var(--text-muted);background:var(--bg-tertiary);padding:2px 8px;border-radius:4px}.info-row[data-v-cc64fdb8]{display:flex;justify-content:space-between;padding:8px 0;font-size:14px;border-bottom:1px solid var(--border)}.info-row span[data-v-cc64fdb8]{color:var(--text-secondary)}.permissions[data-v-cc64fdb8]{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px;min-height:24px}.perm-tag[data-v-cc64fdb8]{font-size:12px;padding:2px 8px;border:1px solid rgba(183,243,107,.22);background:var(--accent-soft);color:var(--accent);border-radius:4px}.field-hint[data-v-cc64fdb8]{font-size:12px;color:var(--text-muted)}.card-actions[data-v-cc64fdb8]{display:flex;gap:8px;margin-top:16px}.card-actions .btn[data-v-cc64fdb8]{flex:1}.check-item[data-v-cc64fdb8]{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:14px;cursor:pointer}.danger[data-v-cc64fdb8]{color:var(--danger)}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.toolbar[data-v-f8ed284c]{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}.empty[data-v-f8ed284c]{text-align:center;padding:32px;color:var(--text-muted)}.type-badge[data-v-f8ed284c]{font-size:11px;padding:2px 6px;border-radius:4px}.type-badge.dir[data-v-f8ed284c]{background:#0ea5e926;color:#0ea5e9}.type-badge.menu[data-v-f8ed284c]{background:#6366f126;color:var(--accent)}.type-badge.btn[data-v-f8ed284c]{background:#22c55e26;color:#22c55e}.field-hint[data-v-f8ed284c]{margin-top:6px;font-size:12px;color:var(--text-muted)}.danger[data-v-f8ed284c]{color:#ef4444}
|
||||
@@ -0,0 +1 @@
|
||||
.toolbar[data-v-0259ca3f]{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}.empty[data-v-0259ca3f]{text-align:center;padding:32px;color:var(--text-muted)}.type-badge[data-v-0259ca3f]{font-size:11px;padding:2px 6px;border-radius:4px}.type-badge.dir[data-v-0259ca3f]{background:#ffffff0b;color:var(--text-secondary)}.type-badge.menu[data-v-0259ca3f]{background:var(--accent-soft);color:var(--accent)}.type-badge.btn[data-v-0259ca3f]{background:#8fe06a14;color:var(--success)}.path-cell[data-v-0259ca3f]{display:flex;align-items:center;gap:8px;color:var(--text-secondary);font-family:Cascadia Code,Consolas,monospace;font-size:12px}.path-cell[data-v-0259ca3f] svg{color:var(--accent)}.field-hint[data-v-0259ca3f]{margin-top:6px;font-size:12px;color:var(--text-muted)}.danger[data-v-0259ca3f]{color:var(--danger)}
|
||||
@@ -0,0 +1 @@
|
||||
.toolbar[data-v-b204ae6d]{margin-bottom:16px}.perm-tags[data-v-b204ae6d]{display:flex;flex-wrap:wrap;gap:4px}.perm-tag[data-v-b204ae6d]{font-size:12px;padding:2px 8px;background:var(--accent-soft);color:var(--accent);border-radius:4px}.field-hint[data-v-b204ae6d]{font-size:12px;color:var(--text-muted)}.modal-wide[data-v-b204ae6d]{max-width:640px;max-height:90vh;display:flex;flex-direction:column}.modal-wide .modal-body[data-v-b204ae6d]{overflow-y:auto}.top-check[data-v-b204ae6d]{font-weight:500}.perm-section[data-v-b204ae6d]{border:1px solid var(--border);border-radius:10px;padding:12px;background:var(--bg-tertiary)}.perm-section-header[data-v-b204ae6d]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:13px;font-weight:500}.perm-actions[data-v-b204ae6d]{display:flex;gap:4px}.perm-dir[data-v-b204ae6d]{margin-bottom:14px;padding-bottom:10px;border-bottom:1px dashed var(--border)}.perm-dir[data-v-b204ae6d]:last-child{border-bottom:none;margin-bottom:0}.dir-check[data-v-b204ae6d]{font-weight:600;margin-bottom:8px}.perm-menu[data-v-b204ae6d]{margin-left:22px;margin-bottom:8px}.perm-btns[data-v-b204ae6d]{margin-left:24px;display:flex;flex-direction:column;gap:4px}.btn-check[data-v-b204ae6d]{font-size:13px;color:var(--text-secondary)}.check-item[data-v-b204ae6d]{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:14px;cursor:pointer}.type-badge[data-v-b204ae6d]{font-size:11px;padding:1px 6px;border-radius:4px;font-weight:500}.type-badge.dir[data-v-b204ae6d]{background:#b7f36b12;color:var(--text-secondary)}.type-badge.menu[data-v-b204ae6d]{background:var(--accent-soft);color:var(--accent)}.type-badge.btn[data-v-b204ae6d]{background:#8fe06a14;color:var(--success)}.danger[data-v-b204ae6d]{color:var(--danger)}
|
||||
@@ -1 +0,0 @@
|
||||
.toolbar[data-v-fc345f7d]{margin-bottom:16px}.perm-tags[data-v-fc345f7d]{display:flex;flex-wrap:wrap;gap:4px}.perm-tag[data-v-fc345f7d]{font-size:12px;padding:2px 8px;background:#6366f126;color:var(--accent);border-radius:4px}.field-hint[data-v-fc345f7d]{font-size:12px;color:var(--text-muted)}.modal-wide[data-v-fc345f7d]{max-width:640px;max-height:90vh;display:flex;flex-direction:column}.modal-wide .modal-body[data-v-fc345f7d]{overflow-y:auto}.top-check[data-v-fc345f7d]{font-weight:500}.perm-section[data-v-fc345f7d]{border:1px solid var(--border);border-radius:10px;padding:12px;background:var(--bg-tertiary)}.perm-section-header[data-v-fc345f7d]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:13px;font-weight:500}.perm-actions[data-v-fc345f7d]{display:flex;gap:4px}.perm-dir[data-v-fc345f7d]{margin-bottom:14px;padding-bottom:10px;border-bottom:1px dashed var(--border)}.perm-dir[data-v-fc345f7d]:last-child{border-bottom:none;margin-bottom:0}.dir-check[data-v-fc345f7d]{font-weight:600;margin-bottom:8px}.perm-menu[data-v-fc345f7d]{margin-left:22px;margin-bottom:8px}.perm-btns[data-v-fc345f7d]{margin-left:24px;display:flex;flex-direction:column;gap:4px}.btn-check[data-v-fc345f7d]{font-size:13px;color:var(--text-secondary)}.check-item[data-v-fc345f7d]{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:14px;cursor:pointer}.type-badge[data-v-fc345f7d]{font-size:11px;padding:1px 6px;border-radius:4px;font-weight:500}.type-badge.dir[data-v-fc345f7d]{background:#0ea5e926;color:#0ea5e9}.type-badge.menu[data-v-fc345f7d]{background:#6366f126;color:var(--accent)}.type-badge.btn[data-v-fc345f7d]{background:#22c55e26;color:#22c55e}.danger[data-v-fc345f7d]{color:#ef4444}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{_ as V,u as h,g as y,h as c,c as o,a as t,b as p,v as j,x as m,s as B,F as C,j as M,i as N,t as _,d as f,r as v,m as S,o as u}from"./index-z4tF8s-R.js";const U={class:"panel"},A={class:"form-group"},T={class:"form-group"},D={class:"check-item"},F={class:"panel",style:{"margin-top":"16px"}},I={class:"feature-grid"},L=["onUpdate:modelValue"],E=["disabled"],O={key:1,class:"success-msg"},R={__name:"SettingsView",setup(q){const g=h(),i=v("AI Chat"),n=v(!0),r=v(!1),d=v(!1),a=S({markdown:!0,image:!0,video:!0,voice:!0,document:!0,emoji:!0,upload_image:!0,upload_video:!0,upload_file:!0,paste_image:!0}),b={markdown:"Markdown 解析",image:"图片解析",video:"视频解析",voice:"语音解析",document:"文档解析",emoji:"表情",upload_image:"上传图片",upload_video:"上传视频",upload_file:"上传文件",paste_image:"粘贴图片"};y(async()=>{var s;const e=(await c.get("/admin/settings")).data.data;e.site_name&&(i.value=e.site_name.value),e.allow_register&&(n.value=e.allow_register.value===!0||e.allow_register.value==="true"),(s=e.features)!=null&&s.value&&Object.assign(a,e.features.value)});async function w(){r.value=!0,d.value=!1;try{await c.put("/admin/settings",{site_name:i.value,allow_register:n.value?"true":"false",features:{...a}}),d.value=!0,setTimeout(()=>{d.value=!1},3e3)}finally{r.value=!1}}return(x,e)=>(u(),o("div",null,[e[7]||(e[7]=t("div",{class:"page-header"},[t("h2",null,"系统设置"),t("p",null,"控制前端功能开关与站点配置")],-1)),t("div",U,[e[4]||(e[4]=t("h3",{class:"section-title"},"站点配置",-1)),t("div",A,[e[2]||(e[2]=t("label",null,"站点名称",-1)),p(t("input",{"onUpdate:modelValue":e[0]||(e[0]=s=>i.value=s),class:"form-input"},null,512),[[j,i.value]])]),t("div",T,[t("label",D,[p(t("input",{type:"checkbox","onUpdate:modelValue":e[1]||(e[1]=s=>n.value=s)},null,512),[[m,n.value]]),e[3]||(e[3]=B(" 允许用户注册 ",-1))])])]),t("div",F,[e[5]||(e[5]=t("h3",{class:"section-title"},"功能开关(会员端)",-1)),e[6]||(e[6]=t("p",{class:"section-desc"},"关闭后,会员端对应功能将不可用",-1)),t("div",I,[(u(!0),o(C,null,M(a,(s,l)=>(u(),o("label",{key:l,class:"feature-item"},[p(t("input",{type:"checkbox","onUpdate:modelValue":k=>a[l]=k},null,8,L),[[m,a[l]]]),t("span",null,_(b[l]||l),1)]))),128))]),N(g).hasButton("btn:settings:save")?(u(),o("button",{key:0,class:"btn btn-primary",onClick:w,disabled:r.value},_(r.value?"保存中...":"保存全部设置"),9,E)):f("",!0),d.value?(u(),o("p",O,"保存成功")):f("",!0)])]))}},G=V(R,[["__scopeId","data-v-75cd9393"]]);export{G as default};
|
||||
@@ -1 +0,0 @@
|
||||
.section-title[data-v-75cd9393]{font-size:16px;margin-bottom:16px}.section-desc[data-v-75cd9393]{font-size:13px;color:var(--text-secondary);margin-bottom:16px}.feature-grid[data-v-75cd9393]{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:20px}.feature-item[data-v-75cd9393]{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.feature-item input[data-v-75cd9393]{accent-color:var(--accent)}.check-item[data-v-75cd9393]{display:flex;align-items:center;gap:8px;cursor:pointer}.success-msg[data-v-75cd9393]{color:var(--success);font-size:14px;margin-top:12px}
|
||||
@@ -0,0 +1 @@
|
||||
.section-title[data-v-0509a01f]{margin-bottom:16px;font-size:16px}.section-desc[data-v-0509a01f],.field-hint[data-v-0509a01f]{color:var(--text-secondary);font-size:12px}.section-desc[data-v-0509a01f]{margin-bottom:16px;font-size:13px}.voice-persona-panel[data-v-0509a01f],.feature-panel[data-v-0509a01f]{margin-top:16px}.persona-heading[data-v-0509a01f]{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;padding-bottom:20px;border-bottom:1px solid var(--border)}.persona-preview[data-v-0509a01f]{display:flex;align-items:center;gap:14px;min-width:0}.persona-avatar[data-v-0509a01f]{width:54px;height:54px;flex:0 0 54px;border-radius:18px;display:grid;place-items:center;border:1px solid rgba(183,243,107,.42);background:linear-gradient(145deg,var(--accent-hover),var(--accent));box-shadow:inset 0 1px #ffffff73,0 5px #577c2f,0 13px 26px #6ea63629;color:#11150e;font-size:22px;font-weight:700}.persona-kicker[data-v-0509a01f]{color:var(--accent);font-size:10px;font-weight:700;letter-spacing:.13em}.persona-preview h3[data-v-0509a01f]{margin-top:3px;font-size:18px}.persona-preview p[data-v-0509a01f]{margin-top:4px;color:var(--text-secondary);font-size:13px;line-height:1.5}.enable-switch[data-v-0509a01f]{display:inline-flex;align-items:center;gap:8px;padding:8px 11px;border:1px solid var(--border);border-radius:999px;color:var(--text-secondary);font-size:12px;white-space:nowrap}.enable-switch input[data-v-0509a01f],.feature-item input[data-v-0509a01f],.check-item input[data-v-0509a01f]{accent-color:var(--accent)}.preset-row[data-v-0509a01f]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:18px 0}.preset-row>span[data-v-0509a01f]{margin-right:4px;color:var(--text-secondary);font-size:12px}.preset-btn[data-v-0509a01f]{padding:7px 11px;border:1px solid var(--border);border-radius:8px;background:#ffffff05;color:var(--text-secondary);font-size:12px}.preset-btn[data-v-0509a01f]:hover{border-color:#b7f36b66;background:var(--accent-soft);color:var(--accent)}.form-grid[data-v-0509a01f]{display:grid;grid-template-columns:1fr 1fr;gap:14px}.form-grid-3[data-v-0509a01f]{grid-template-columns:1fr 1fr .7fr}.persona-textarea[data-v-0509a01f]{min-height:86px;resize:vertical;line-height:1.55}.field-hint[data-v-0509a01f]{margin-top:6px;line-height:1.5}.input-suffix[data-v-0509a01f]{position:relative}.input-suffix input[data-v-0509a01f]{padding-right:38px}.input-suffix span[data-v-0509a01f]{position:absolute;top:50%;right:12px;color:var(--text-muted);font-size:12px;transform:translateY(-50%)}.reference-box[data-v-0509a01f]{margin-bottom:16px;padding:14px;border:1px dashed var(--border-strong);border-radius:10px;display:flex;align-items:center;justify-content:space-between;gap:16px;background:#b7f36b09}.reference-box strong[data-v-0509a01f]{font-size:13px}.reference-box p[data-v-0509a01f]{margin-top:4px;color:var(--text-secondary);font-size:12px;line-height:1.5}.reference-btn[data-v-0509a01f]{flex:0 0 auto;border:1px solid var(--border)}.persona-actions[data-v-0509a01f]{min-height:38px;display:flex;align-items:center;flex-wrap:wrap;gap:12px}.preview-btn[data-v-0509a01f]{border:1px solid var(--border-strong)}.preview-audio[data-v-0509a01f]{width:min(320px,100%);height:36px}.voice-status[data-v-0509a01f]{color:var(--success);font-size:12px}.voice-status.error[data-v-0509a01f]{color:var(--danger)}.feature-grid[data-v-0509a01f]{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:20px}.feature-item[data-v-0509a01f],.check-item[data-v-0509a01f]{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px}.success-msg[data-v-0509a01f]{margin-top:12px;color:var(--success);font-size:14px}@media(max-width:760px){.persona-heading[data-v-0509a01f],.reference-box[data-v-0509a01f]{align-items:stretch;flex-direction:column}.enable-switch[data-v-0509a01f]{align-self:flex-start}.form-grid[data-v-0509a01f],.form-grid-3[data-v-0509a01f]{grid-template-columns:1fr}}
|
||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.toolbar[data-v-855121f7]{margin-bottom:16px}.empty[data-v-855121f7]{text-align:center;padding:32px;color:var(--text-muted)}.pagination[data-v-855121f7]{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:16px;font-size:13px;color:var(--text-secondary)}.modal-wide[data-v-855121f7]{max-width:560px}.user-meta[data-v-855121f7]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:16px;padding:10px 12px;background:var(--bg-tertiary);border-radius:8px;font-size:13px;color:var(--text-secondary)}.form-divider[data-v-855121f7]{margin:20px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border);font-size:13px;font-weight:500;color:var(--text-secondary)}.membership-preview[data-v-855121f7]{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.perm-tag[data-v-855121f7]{font-size:12px;padding:2px 8px;background:var(--accent-soft);color:var(--accent);border-radius:4px}.field-hint[data-v-855121f7]{font-size:12px;color:var(--text-muted)}.danger[data-v-855121f7]{color:var(--danger)}
|
||||
@@ -1 +0,0 @@
|
||||
.toolbar[data-v-921bbcc9]{margin-bottom:16px}.empty[data-v-921bbcc9]{text-align:center;padding:32px;color:var(--text-muted)}.pagination[data-v-921bbcc9]{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:16px;font-size:13px;color:var(--text-secondary)}.modal-wide[data-v-921bbcc9]{max-width:560px}.user-meta[data-v-921bbcc9]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:16px;padding:10px 12px;background:var(--bg-tertiary);border-radius:8px;font-size:13px;color:var(--text-secondary)}.form-divider[data-v-921bbcc9]{margin:20px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border);font-size:13px;font-weight:500;color:var(--text-secondary)}.membership-preview[data-v-921bbcc9]{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.perm-tag[data-v-921bbcc9]{font-size:12px;padding:2px 8px;background:#6366f126;color:var(--accent);border-radius:4px}.field-hint[data-v-921bbcc9]{font-size:12px;color:var(--text-muted)}.danger[data-v-921bbcc9]{color:#ef4444}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI Chat 管理后台</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
|
||||
<script type="module" crossorigin src="/admin/assets/index-z4tF8s-R.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-DPq65Hqk.css">
|
||||
<script type="module" crossorigin src="/admin/assets/index-BZEF5hZc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-B4BiP-qK.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{_ as V,u as k,o as E,c as v,a as i,b as t,d as f,e,t as d,w as S,f as c,v as g,g as x,h as A,r as l,i as I,j as N,k as C,l as h}from"./index-sdqi2xzF.js";import{u as M,I as q,T as B}from"./settings-R2Yjxl-Z.js";const L={class:"auth-page"},R={class:"auth-shell"},D={class:"auth-story"},G={class:"auth-logo"},P={class:"auth-card"},U={class:"auth-header"},j={class:"form-group"},z={class:"form-group"},X={key:0,class:"form-error"},F=["disabled"],H={class:"auth-footer"},J={__name:"LoginView",setup(K){const _=N(),b=C(),w=k(),p=M(),n=l(""),u=l(""),o=l(""),a=l(!1);E(()=>p.loadPublic());async function y(){o.value="",a.value=!0;try{await w.login(n.value,u.value),_.push(b.query.redirect||"/")}catch(m){o.value=m.message}finally{a.value=!1}}return(m,s)=>{const T=I("router-link");return h(),v("div",L,[i(B,{class:"auth-theme-toggle"}),t("section",R,[t("aside",D,[t("span",G,[i(f(q),{size:22,"stroke-width":1.8})]),s[2]||(s[2]=t("span",{class:"auth-eyebrow"},"AI CREATIVE SPACE",-1)),s[3]||(s[3]=t("h1",null,[e("把想法放进来,"),t("br"),e("让创作自然发生。")],-1)),s[4]||(s[4]=t("p",null,"在同一个工作区完成对话、图像生成与内容整理。",-1)),s[5]||(s[5]=t("span",{class:"auth-note"},"TEXT · IMAGE · AGENT",-1))]),t("div",P,[t("div",U,[s[6]||(s[6]=t("span",{class:"auth-status"},[t("i"),e(" 账户登录")],-1)),t("h2",null,d(f(p).siteName),1),s[7]||(s[7]=t("p",null,"欢迎回来,请登录您的账户",-1))]),t("form",{onSubmit:S(y,["prevent"])},[t("div",j,[s[8]||(s[8]=t("label",null,"账号",-1)),c(t("input",{"onUpdate:modelValue":s[0]||(s[0]=r=>n.value=r),class:"form-input",placeholder:"用户名或邮箱",required:""},null,512),[[g,n.value]])]),t("div",z,[s[9]||(s[9]=t("label",null,"密码",-1)),c(t("input",{"onUpdate:modelValue":s[1]||(s[1]=r=>u.value=r),type:"password",class:"form-input",placeholder:"请输入密码",required:""},null,512),[[g,u.value]])]),o.value?(h(),v("p",X,d(o.value),1)):x("",!0),t("button",{type:"submit",class:"btn btn-primary auth-btn",disabled:a.value},d(a.value?"登录中...":"进入创作空间"),9,F)],32),t("p",H,[s[11]||(s[11]=e(" 还没有账户? ",-1)),i(T,{to:"/register"},{default:A(()=>[...s[10]||(s[10]=[e("立即注册",-1)])]),_:1})])])])])}}},W=V(J,[["__scopeId","data-v-036d1210"]]);export{W as default};
|
||||
@@ -0,0 +1 @@
|
||||
.auth-page[data-v-036d1210]{position:relative;min-height:100dvh;display:grid;place-items:center;padding:28px;overflow:auto;background:radial-gradient(circle at 72% 16%,rgba(183,243,107,.08),transparent 26%),var(--bg-primary)}.auth-theme-toggle[data-v-036d1210]{position:absolute;top:18px;right:18px;z-index:3}.auth-shell[data-v-036d1210]{position:relative;display:grid;width:min(900px,100%);grid-template-columns:1.08fr .92fr;overflow:hidden;border:1px solid var(--border-strong);border-radius:22px;background:var(--bg-secondary);box-shadow:inset 0 1px #ffffff0b,var(--shadow)}.auth-shell[data-v-036d1210]:after{position:absolute;top:0;left:16%;width:36%;height:1px;background:linear-gradient(90deg,transparent,var(--accent),transparent);box-shadow:0 0 17px #b7f36b70;content:""}.auth-story[data-v-036d1210]{position:relative;display:flex;min-height:550px;flex-direction:column;justify-content:center;padding:54px;overflow:hidden;border-right:1px solid var(--border);background:linear-gradient(135deg,rgba(183,243,107,.07),transparent 45%),repeating-linear-gradient(135deg,rgba(255,255,255,.018) 0 1px,transparent 1px 14px),#0c0f13}.auth-logo[data-v-036d1210]{display:grid;width:46px;height:46px;place-items:center;margin-bottom:46px;border:1px solid rgba(183,243,107,.5);border-radius:14px;background:var(--accent);color:#11150e;box-shadow:inset 0 1px #ffffff85,0 5px #55782f,0 14px 28px #6ea6362b}.auth-eyebrow[data-v-036d1210],.auth-note[data-v-036d1210]{color:var(--accent);font-family:Cascadia Code,Consolas,monospace;font-size:10px;letter-spacing:.16em}.auth-story h1[data-v-036d1210]{margin:15px 0 19px;font-size:clamp(36px,4.7vw,54px);font-weight:760;letter-spacing:-.06em;line-height:1.04;text-wrap:balance}.auth-story p[data-v-036d1210]{max-width:31ch;color:var(--text-secondary);font-size:14px;line-height:1.75}.auth-note[data-v-036d1210]{position:absolute;bottom:30px;left:54px;color:var(--text-muted);font-size:9px}.auth-card[data-v-036d1210]{display:flex;flex-direction:column;justify-content:center;padding:48px 42px;background:radial-gradient(circle at 100% 0%,rgba(183,243,107,.05),transparent 28%),var(--bg-secondary)}.auth-header[data-v-036d1210]{margin-bottom:30px}.auth-status[data-v-036d1210]{display:inline-flex;align-items:center;gap:7px;color:var(--text-muted);font-size:11px;font-weight:650;letter-spacing:.06em}.auth-status i[data-v-036d1210]{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px #b7f36b99}.auth-header h2[data-v-036d1210]{margin:12px 0 7px;font-size:27px;letter-spacing:-.045em}.auth-header p[data-v-036d1210],.auth-footer[data-v-036d1210]{color:var(--text-secondary);font-size:13px}.auth-btn[data-v-036d1210]{width:100%;min-height:47px;margin-top:7px}.auth-footer[data-v-036d1210]{margin-top:25px;text-align:center}@media(max-width:720px){.auth-page[data-v-036d1210]{padding:14px}.auth-shell[data-v-036d1210]{grid-template-columns:1fr}.auth-story[data-v-036d1210]{display:none}.auth-card[data-v-036d1210]{min-height:540px;padding:40px 25px}}
|
||||
@@ -1 +0,0 @@
|
||||
.auth-page[data-v-a92a8550]{min-height:100%;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--bg-secondary)}.auth-card[data-v-a92a8550]{width:100%;max-width:400px;padding:40px 32px;background:var(--bg-primary);border-radius:16px;border:1px solid var(--border);box-shadow:var(--shadow)}.auth-header[data-v-a92a8550]{text-align:center;margin-bottom:32px}.logo[data-v-a92a8550]{font-size:48px;margin-bottom:12px}.auth-header h1[data-v-a92a8550]{font-size:24px;margin-bottom:8px}.auth-header p[data-v-a92a8550]{color:var(--text-secondary);font-size:14px}.auth-btn[data-v-a92a8550]{width:100%;margin-top:8px;padding:14px}.auth-footer[data-v-a92a8550]{text-align:center;margin-top:24px;font-size:14px;color:var(--text-secondary)}
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as y,u as V,o as k,c as p,a as t,t as u,b as x,w as N,d as v,v as c,e as S,f as m,g as q,h as B,r as a,i as C,j as L,k as M,l as f}from"./index-k46zOoYG.js";import{u as D}from"./settings-1-hPceiv.js";const R={class:"auth-page"},T={class:"auth-card"},U={class:"auth-header"},j={class:"form-group"},A={class:"form-group"},E={key:0,class:"form-error"},I=["disabled"],P={class:"auth-footer"},z={__name:"LoginView",setup(F){const _=L(),g=M(),b=V(),i=D(),l=a(""),r=a(""),s=a(""),o=a(!1);k(()=>i.loadPublic());async function h(){s.value="",o.value=!0;try{await b.login(l.value,r.value),_.push(g.query.redirect||"/")}catch(d){s.value=d.message}finally{o.value=!1}}return(d,e)=>{const w=C("router-link");return f(),p("div",R,[t("div",T,[t("div",U,[e[2]||(e[2]=t("div",{class:"logo"},"💬",-1)),t("h1",null,u(x(i).siteName),1),e[3]||(e[3]=t("p",null,"登录您的账户",-1))]),t("form",{onSubmit:N(h,["prevent"])},[t("div",j,[e[4]||(e[4]=t("label",null,"账号",-1)),v(t("input",{"onUpdate:modelValue":e[0]||(e[0]=n=>l.value=n),class:"form-input",placeholder:"用户名或邮箱",required:""},null,512),[[c,l.value]])]),t("div",A,[e[5]||(e[5]=t("label",null,"密码",-1)),v(t("input",{"onUpdate:modelValue":e[1]||(e[1]=n=>r.value=n),type:"password",class:"form-input",placeholder:"请输入密码",required:""},null,512),[[c,r.value]])]),s.value?(f(),p("p",E,u(s.value),1)):S("",!0),t("button",{type:"submit",class:"btn btn-primary auth-btn",disabled:o.value},u(o.value?"登录中...":"登录"),9,I)],32),t("p",P,[e[7]||(e[7]=m(" 还没有账户? ",-1)),q(w,{to:"/register"},{default:B(()=>[...e[6]||(e[6]=[m("立即注册",-1)])]),_:1})])])])}}},J=y(z,[["__scopeId","data-v-a92a8550"]]);export{J as default};
|
||||
@@ -0,0 +1 @@
|
||||
.auth-page[data-v-09353b56]{position:relative;min-height:100dvh;display:grid;place-items:center;padding:28px;overflow:auto;background:radial-gradient(circle at 72% 16%,rgba(183,243,107,.08),transparent 26%),var(--bg-primary)}.auth-theme-toggle[data-v-09353b56]{position:absolute;top:18px;right:18px;z-index:3}.auth-shell[data-v-09353b56]{position:relative;display:grid;width:min(900px,100%);grid-template-columns:1.08fr .92fr;overflow:hidden;border:1px solid var(--border-strong);border-radius:22px;background:var(--bg-secondary);box-shadow:inset 0 1px #ffffff0b,var(--shadow)}.auth-shell[data-v-09353b56]:after{position:absolute;top:0;left:16%;width:36%;height:1px;background:linear-gradient(90deg,transparent,var(--accent),transparent);box-shadow:0 0 17px #b7f36b70;content:""}.auth-story[data-v-09353b56]{position:relative;display:flex;min-height:610px;flex-direction:column;justify-content:center;padding:54px;overflow:hidden;border-right:1px solid var(--border);background:linear-gradient(135deg,rgba(183,243,107,.07),transparent 45%),repeating-linear-gradient(135deg,rgba(255,255,255,.018) 0 1px,transparent 1px 14px),#0c0f13}.auth-logo[data-v-09353b56]{display:grid;width:46px;height:46px;place-items:center;margin-bottom:46px;border:1px solid rgba(183,243,107,.5);border-radius:14px;background:var(--accent);color:#11150e;box-shadow:inset 0 1px #ffffff85,0 5px #55782f,0 14px 28px #6ea6362b}.auth-eyebrow[data-v-09353b56],.auth-note[data-v-09353b56]{color:var(--accent);font-family:Cascadia Code,Consolas,monospace;font-size:10px;letter-spacing:.16em}.auth-story h1[data-v-09353b56]{margin:15px 0 19px;font-size:clamp(36px,4.7vw,54px);font-weight:760;letter-spacing:-.06em;line-height:1.04;text-wrap:balance}.auth-story p[data-v-09353b56]{max-width:31ch;color:var(--text-secondary);font-size:14px;line-height:1.75}.auth-note[data-v-09353b56]{position:absolute;bottom:30px;left:54px;color:var(--text-muted);font-size:9px}.auth-card[data-v-09353b56]{display:flex;flex-direction:column;justify-content:center;padding:42px;background:radial-gradient(circle at 100% 0%,rgba(183,243,107,.05),transparent 28%),var(--bg-secondary)}.auth-header[data-v-09353b56]{margin-bottom:27px}.auth-status[data-v-09353b56]{display:inline-flex;align-items:center;gap:7px;color:var(--text-muted);font-size:11px;font-weight:650;letter-spacing:.06em}.auth-status i[data-v-09353b56]{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px #b7f36b99}.auth-header h2[data-v-09353b56]{margin:12px 0 7px;font-size:27px;letter-spacing:-.045em}.auth-header p[data-v-09353b56],.auth-footer[data-v-09353b56]{color:var(--text-secondary);font-size:13px}.auth-btn[data-v-09353b56]{width:100%;min-height:47px;margin-top:6px}.auth-footer[data-v-09353b56]{margin-top:23px;text-align:center}@media(max-width:720px){.auth-page[data-v-09353b56]{padding:14px}.auth-shell[data-v-09353b56]{grid-template-columns:1fr}.auth-story[data-v-09353b56]{display:none}.auth-card[data-v-09353b56]{min-height:640px;padding:36px 25px}}
|
||||
@@ -1 +0,0 @@
|
||||
.auth-page[data-v-ab6798e7]{min-height:100%;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--bg-secondary)}.auth-card[data-v-ab6798e7]{width:100%;max-width:400px;padding:40px 32px;background:var(--bg-primary);border-radius:16px;border:1px solid var(--border);box-shadow:var(--shadow)}.auth-header[data-v-ab6798e7]{text-align:center;margin-bottom:32px}.logo[data-v-ab6798e7]{font-size:48px;margin-bottom:12px}.auth-header h1[data-v-ab6798e7]{font-size:24px;margin-bottom:8px}.auth-header p[data-v-ab6798e7]{color:var(--text-secondary);font-size:14px}.auth-btn[data-v-ab6798e7]{width:100%;margin-top:8px;padding:14px}.auth-footer[data-v-ab6798e7]{text-align:center;margin-top:24px;font-size:14px;color:var(--text-secondary)}
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as h,u as V,o as x,c as f,a as t,t as d,b as k,w as N,d as p,v as m,e as R,f as c,g as S,h as q,r as l,i as B,j as C,l as g}from"./index-k46zOoYG.js";import{u as M}from"./settings-1-hPceiv.js";const U={class:"auth-page"},D={class:"auth-card"},T={class:"auth-header"},j={class:"form-group"},A={class:"form-group"},E={class:"form-group"},I={key:0,class:"form-error"},P=["disabled"],z={class:"auth-footer"},F={__name:"RegisterView",setup(G){const _=C(),b=V(),r=M(),u=l(""),n=l(""),i=l(""),s=l(""),a=l(!1);x(()=>r.loadPublic());async function w(){if(!r.allowRegister){s.value="当前不允许注册";return}s.value="",a.value=!0;try{await b.register(u.value,n.value,i.value),_.push("/")}catch(v){s.value=v.message}finally{a.value=!1}}return(v,e)=>{const y=B("router-link");return g(),f("div",U,[t("div",D,[t("div",T,[e[3]||(e[3]=t("div",{class:"logo"},"💬",-1)),t("h1",null,d(k(r).siteName),1),e[4]||(e[4]=t("p",null,"创建新账户",-1))]),t("form",{onSubmit:N(w,["prevent"])},[t("div",j,[e[5]||(e[5]=t("label",null,"用户名",-1)),p(t("input",{"onUpdate:modelValue":e[0]||(e[0]=o=>u.value=o),class:"form-input",placeholder:"3-50 个字符",required:""},null,512),[[m,u.value]])]),t("div",A,[e[6]||(e[6]=t("label",null,"邮箱",-1)),p(t("input",{"onUpdate:modelValue":e[1]||(e[1]=o=>n.value=o),type:"email",class:"form-input",placeholder:"your@email.com",required:""},null,512),[[m,n.value]])]),t("div",E,[e[7]||(e[7]=t("label",null,"密码",-1)),p(t("input",{"onUpdate:modelValue":e[2]||(e[2]=o=>i.value=o),type:"password",class:"form-input",placeholder:"至少 6 位",required:""},null,512),[[m,i.value]])]),s.value?(g(),f("p",I,d(s.value),1)):R("",!0),t("button",{type:"submit",class:"btn btn-primary auth-btn",disabled:a.value},d(a.value?"注册中...":"注册"),9,P)],32),t("p",z,[e[9]||(e[9]=c(" 已有账户? ",-1)),S(y,{to:"/login"},{default:q(()=>[...e[8]||(e[8]=[c("立即登录",-1)])]),_:1})])])])}}},K=h(F,[["__scopeId","data-v-ab6798e7"]]);export{K as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as V,u as k,o as N,c as b,a as p,b as e,d as h,e as l,t as m,w as R,f as v,v as f,g as S,h as x,r as a,i as A,j as E,l as c}from"./index-sdqi2xzF.js";import{u as I,I as C,T as M}from"./settings-R2Yjxl-Z.js";const q={class:"auth-page"},B={class:"auth-shell"},G={class:"auth-story"},U={class:"auth-logo"},D={class:"auth-card"},j={class:"auth-header"},z={class:"form-group"},P={class:"form-group"},X={class:"form-group"},F={key:0,class:"form-error"},H=["disabled"],J={class:"auth-footer"},K={__name:"RegisterView",setup(L){const _=E(),w=k(),n=I(),r=a(""),i=a(""),d=a(""),t=a(""),u=a(!1);N(()=>n.loadPublic());async function y(){if(!n.allowRegister){t.value="当前不允许注册";return}t.value="",u.value=!0;try{await w.register(r.value,i.value,d.value),_.push("/")}catch(g){t.value=g.message}finally{u.value=!1}}return(g,s)=>{const T=A("router-link");return c(),b("div",q,[p(M,{class:"auth-theme-toggle"}),e("section",B,[e("aside",G,[e("span",U,[p(h(C),{size:22,"stroke-width":1.8})]),s[3]||(s[3]=e("span",{class:"auth-eyebrow"},"START CREATING",-1)),s[4]||(s[4]=e("h1",null,[l("一个账户,"),e("br"),l("连接全部创作工具。")],-1)),s[5]||(s[5]=e("p",null,"创建账户后即可保存会话,并使用可用的模型与 Agent。",-1)),s[6]||(s[6]=e("span",{class:"auth-note"},"TEXT · IMAGE · AGENT",-1))]),e("div",D,[e("div",j,[s[7]||(s[7]=e("span",{class:"auth-status"},[e("i"),l(" 创建账户")],-1)),e("h2",null,m(h(n).siteName),1),s[8]||(s[8]=e("p",null,"填写信息,开始新的创作会话",-1))]),e("form",{onSubmit:R(y,["prevent"])},[e("div",z,[s[9]||(s[9]=e("label",null,"用户名",-1)),v(e("input",{"onUpdate:modelValue":s[0]||(s[0]=o=>r.value=o),class:"form-input",placeholder:"3-50 个字符",required:""},null,512),[[f,r.value]])]),e("div",P,[s[10]||(s[10]=e("label",null,"邮箱",-1)),v(e("input",{"onUpdate:modelValue":s[1]||(s[1]=o=>i.value=o),type:"email",class:"form-input",placeholder:"your@email.com",required:""},null,512),[[f,i.value]])]),e("div",X,[s[11]||(s[11]=e("label",null,"密码",-1)),v(e("input",{"onUpdate:modelValue":s[2]||(s[2]=o=>d.value=o),type:"password",class:"form-input",placeholder:"至少 6 位",required:""},null,512),[[f,d.value]])]),t.value?(c(),b("p",F,m(t.value),1)):S("",!0),e("button",{type:"submit",class:"btn btn-primary auth-btn",disabled:u.value},m(u.value?"注册中...":"创建并进入"),9,H)],32),e("p",J,[s[13]||(s[13]=l(" 已有账户? ",-1)),p(T,{to:"/login"},{default:x(()=>[...s[12]||(s[12]=[l("立即登录",-1)])]),_:1})])])])])}}},W=V(K,[["__scopeId","data-v-09353b56"]]);export{W as default};
|
||||
@@ -1,10 +0,0 @@
|
||||
.notification-region[data-v-a3b13931]{position:fixed;top:16px;right:16px;z-index:120;display:flex;width:min(420px,calc(100vw - 32px));flex-direction:column;gap:10px;pointer-events:none}.notification-card[data-v-a3b13931]{position:relative;display:grid;grid-template-columns:38px minmax(0,1fr);gap:12px;overflow:hidden;padding:14px;background:#fffffffa;border:1px solid var(--border-strong);border-radius:16px;box-shadow:0 18px 48px #263b5c29;pointer-events:auto}.notification-card[data-v-a3b13931]:before{content:"";position:absolute;inset:0 auto 0 0;width:3px;background:var(--danger)}.notification-icon[data-v-a3b13931]{width:38px;height:38px;border-radius:12px;display:flex;align-items:center;justify-content:center;background:#fff0f0;color:var(--danger)}.notification-content[data-v-a3b13931]{min-width:0}.notification-heading[data-v-a3b13931]{display:flex;min-height:28px;align-items:flex-start;justify-content:space-between;gap:10px}.notification-heading strong[data-v-a3b13931]{padding-top:2px;color:var(--text-primary);font-size:15px;font-weight:650;line-height:1.4}.notification-close[data-v-a3b13931]{width:28px;height:28px;margin:-3px -4px 0 0;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--text-muted);flex-shrink:0;transition:background .16s ease,color .16s ease,transform .16s ease}.notification-close[data-v-a3b13931]:hover{background:var(--bg-soft);color:var(--text-primary)}.notification-close[data-v-a3b13931]:active,.copy-detail[data-v-a3b13931]:active{transform:scale(.96)}.notification-content>p[data-v-a3b13931]{max-width:36em;color:var(--text-secondary);font-size:13px;line-height:1.65;text-wrap:pretty}.notification-detail[data-v-a3b13931]{margin-top:10px;border-top:1px solid var(--border)}.notification-detail summary[data-v-a3b13931]{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 0 1px;color:var(--text-secondary);font-size:12px;font-weight:550;cursor:pointer;list-style:none;-webkit-user-select:none;user-select:none}.notification-detail summary[data-v-a3b13931]::-webkit-details-marker{display:none}.detail-chevron[data-v-a3b13931]{transition:transform .18s ease}.notification-detail[open] .detail-chevron[data-v-a3b13931]{transform:rotate(180deg)}.detail-body[data-v-a3b13931]{margin-top:8px;padding:10px;background:#f6f8fb;border:1px solid var(--border);border-radius:10px}.detail-body pre[data-v-a3b13931]{max-height:190px;overflow:auto;color:#354258;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:11px;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere}.copy-detail[data-v-a3b13931]{display:inline-flex;align-items:center;gap:5px;min-height:30px;margin-top:9px;padding:0 9px;background:#fff;border:1px solid var(--border-strong);border-radius:8px;color:var(--text-secondary);font-size:11px;transition:background .16s ease,color .16s ease,transform .16s ease}.copy-detail[data-v-a3b13931]:hover{background:var(--bg-soft);color:var(--text-primary)}.notification-enter-active[data-v-a3b13931],.notification-leave-active[data-v-a3b13931]{transition:opacity .22s ease,transform .22s cubic-bezier(.16,1,.3,1)}.notification-enter-from[data-v-a3b13931],.notification-leave-to[data-v-a3b13931]{opacity:0;transform:translateY(-8px) scale(.98)}@media(max-width:768px){.notification-region[data-v-a3b13931]{top:calc(10px + env(safe-area-inset-top));right:10px;left:10px;width:auto}.notification-card[data-v-a3b13931]{grid-template-columns:34px minmax(0,1fr);gap:10px;padding:12px;border-radius:14px}.notification-icon[data-v-a3b13931]{width:34px;height:34px;border-radius:10px}.detail-body pre[data-v-a3b13931]{max-height:min(220px,36dvh)}}@media(prefers-reduced-motion:reduce){.notification-enter-active[data-v-a3b13931],.notification-leave-active[data-v-a3b13931],.notification-close[data-v-a3b13931],.copy-detail[data-v-a3b13931],.detail-chevron[data-v-a3b13931]{transition:none}}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-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-comment,.hljs-code,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{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}:root{--bg-primary: #f5f7fb;--bg-secondary: #ffffff;--bg-tertiary: #f8fafc;--bg-hover: #eef3f9;--bg-soft: #f2f5f8;--text-primary: #152033;--text-secondary: #526077;--text-muted: #7d899b;--accent: #2d66da;--accent-hover: #2458c4;--accent-soft: #edf4ff;--border: #e1e7ef;--border-strong: #d4dce7;--danger: #dc4c4c;--sidebar-width: 268px;--header-height: 66px;--input-max-width: 920px;--content-max-width: 1040px;--radius: 18px;--radius-sm: 12px;--shadow: 0 18px 50px rgba(38, 59, 92, .08);--shadow-soft: 0 8px 24px rgba(38, 59, 92, .06);--shadow-composer: 0 14px 38px rgba(38, 59, 92, .09)}*{margin:0;padding:0;box-sizing:border-box}html,body,#app{height:100%;min-height:100dvh;font-family:Microsoft YaHei UI,PingFang SC,Segoe UI,sans-serif;background:var(--bg-primary);color:var(--text-primary);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}body{overflow:hidden}a{color:var(--accent);text-decoration:none}button{cursor:pointer;border:none;background:none;color:inherit;font:inherit}button:focus-visible,a:focus-visible,select:focus-visible,textarea:focus-visible,input:focus-visible{outline:2px solid rgba(45,102,218,.72);outline-offset:2px}input,textarea,select{font:inherit;color:inherit;background:transparent;border:none;outline:none}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#ced7e3;border-radius:999px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 20px;border-radius:999px;font-size:14px;font-weight:500;transition:color .16s ease,background .16s ease,border-color .16s ease,transform .16s ease}.btn:active{transform:scale(.97)}.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover{background:var(--accent-hover)}.btn-primary:disabled{opacity:.5;cursor:not-allowed}.btn-ghost{background:#fff;color:var(--text-secondary);border:1px solid var(--border)}.btn-ghost:hover{background:var(--bg-hover);color:var(--text-primary)}.btn-danger{color:var(--danger)}.btn-danger:hover{background:#ef44441a}.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;color:var(--text-secondary)}.form-input{width:100%;padding:12px 16px;background:#fff;border:1px solid var(--border);border-radius:14px;font-size:15px;transition:border-color .2s,box-shadow .2s}.form-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px #2d66da1c}.form-error{color:var(--danger);font-size:13px;margin-top:12px}.markdown-body{line-height:1.7;font-size:15px;word-break:break-word}.markdown-body p{margin-bottom:12px}.markdown-body p:last-child{margin-bottom:0}.markdown-body br{content:"";display:block;margin-top:10px}.markdown-body pre{background:#f8fafc;border:1px solid var(--border);border-radius:14px;padding:16px;overflow-x:auto;margin:12px 0}.markdown-body code{font-family:SF Mono,Monaco,Consolas,monospace;font-size:13px}.markdown-body :not(pre)>code{background:#eef2ff;padding:2px 6px;border-radius:6px}.markdown-body ul,.markdown-body ol{padding-left:24px;margin-bottom:12px}.markdown-body blockquote{border-left:3px solid var(--accent);padding-left:16px;color:var(--text-secondary);margin:12px 0}.markdown-body table{border-collapse:collapse;width:100%;margin:12px 0}.markdown-body th,.markdown-body td{border:1px solid var(--border);padding:8px 12px;text-align:left}.markdown-body th{background:var(--bg-soft)}.markdown-body a{color:var(--accent)}.markdown-body img{max-width:100%;border-radius:8px}.sidebar-overlay{display:none;position:fixed;top:0;right:0;bottom:0;left:0;background:#15203357;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:90}@media(max-width:768px){.sidebar-overlay.active{display:block}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{m,r as e,n as s}from"./index-k46zOoYG.js";const v=m("settings",()=>{const o=e({markdown:!0,image:!0,video:!0,voice:!0,document:!0,emoji:!0,upload_image:!0,upload_video:!0,upload_file:!0,paste_image:!0}),n=e("AI Chat"),u=e(!0),r=e([]),i=e([]),l=e(!1);async function d(){const a=(await s.get("/settings/public")).data.data;o.value=a.features,n.value=a.site_name,u.value=a.allow_register,l.value=!0}async function c(){const t=await s.get("/models");r.value=t.data.data}async function g(){const t=await s.get("/agents");i.value=t.data.data||[]}return{features:o,siteName:n,allowRegister:u,models:r,agents:i,loaded:l,loadPublic:d,loadModels:c,loadAgents:g}});export{v as u};
|
||||
@@ -0,0 +1,6 @@
|
||||
import{p as f,_ as k,l as b,c as h,b as u,d as s,y as g,P as y,m as T,r as t,n as c}from"./index-sdqi2xzF.js";/**
|
||||
* @license @tabler/icons-vue v3.45.0 - MIT
|
||||
*
|
||||
* This source code is licensed under the MIT license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/var A=f("outline","sparkles","Sparkles",[["path",{d:"M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2m0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2m-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6",key:"svg-0"}]]);const w={class:"theme-toggle",role:"group","aria-label":"界面主题"},C=["aria-pressed"],I=["aria-pressed"],S={__name:"ThemeToggle",setup(d){const{theme:a,setTheme:o}=y();return(n,e)=>(b(),h("div",w,[u("button",{type:"button",class:g({active:s(a)==="light"}),"aria-pressed":s(a)==="light","aria-label":"使用浅色主题",title:"浅色主题",onClick:e[0]||(e[0]=r=>s(o)("light"))},[...e[2]||(e[2]=[u("span",null,"浅色",-1)])],10,C),u("button",{type:"button",class:g({active:s(a)==="dark"}),"aria-pressed":s(a)==="dark","aria-label":"使用深色主题",title:"深色主题",onClick:e[1]||(e[1]=r=>s(o)("dark"))},[...e[3]||(e[3]=[u("span",null,"深色",-1)])],10,I)]))}},B=k(S,[["__scopeId","data-v-6644b9d7"]]),P=T("settings",()=>{const d=t({markdown:!0,image:!0,video:!0,voice:!0,document:!0,emoji:!0,upload_image:!0,upload_video:!0,upload_file:!0,paste_image:!0}),a=t("AI Chat"),o=t(!0),n=t({name:"AI 客服",greeting:"您好,请问有什么可以帮您?"}),e=t([]),r=t([]),p=t(!1);async function m(){const i=(await c.get("/settings/public")).data.data;d.value=i.features,a.value=i.site_name,o.value=i.allow_register,n.value=i.voice_persona||n.value,p.value=!0}async function v(){const l=await c.get("/models");e.value=l.data.data}async function _(){const l=await c.get("/agents");r.value=l.data.data||[]}return{features:d,siteName:a,allowRegister:o,voicePersona:n,models:e,agents:r,loaded:p,loadPublic:m,loadModels:v,loadAgents:_}});export{A as I,B as T,P as u};
|
||||
@@ -0,0 +1 @@
|
||||
.theme-toggle[data-v-6644b9d7]{display:inline-grid;grid-template-columns:repeat(2,42px);gap:0;padding:2px;border:1px solid var(--border);border-radius:6px;background:var(--bg-tertiary)}button[data-v-6644b9d7]{display:grid;width:42px;height:30px;place-items:center;border:1px solid transparent;border-radius:4px;color:var(--text-muted);font-size:11px;font-weight:600;transition:color .16s ease,background .16s ease,transform .16s ease}button[data-v-6644b9d7]:hover{color:var(--text-primary);background:var(--bg-hover)}button[data-v-6644b9d7]:active{transform:translateY(1px) scale(.96)}button.active[data-v-6644b9d7]{border-color:transparent;background:var(--button-primary-bg);color:var(--button-primary-text)}
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>AI Chat</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<script type="module" crossorigin src="/assets/index-k46zOoYG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-3dTEmASf.css">
|
||||
<script type="module" crossorigin src="/assets/index-sdqi2xzF.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-De9xW-9E.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -43,6 +43,9 @@ Route::group('api', function () {
|
||||
Route::get('conversations/:id/messages', 'api.Conversation/messages');
|
||||
|
||||
Route::post('chat/completions', 'api.Chat/completions');
|
||||
Route::post('chat/speech', 'api.Chat/speech');
|
||||
Route::post('chat/speech/stream', 'api.Chat/speechStream');
|
||||
Route::post('chat/speech/cancel', 'api.Chat/speechCancel');
|
||||
Route::post('upload', 'api.Upload/upload');
|
||||
Route::get('models', 'api.Settings/models');
|
||||
Route::get('agents', 'api.Settings/agents');
|
||||
@@ -71,6 +74,8 @@ Route::group('api', function () {
|
||||
Route::get('stats', 'api.Admin/stats');
|
||||
Route::get('settings', 'api.Admin/settings');
|
||||
Route::put('settings', 'api.Admin/updateSettings');
|
||||
Route::post('voice/reference', 'api.Admin/uploadVoiceReference');
|
||||
Route::post('voice/preview', 'api.Admin/previewVoicePersona');
|
||||
Route::get('models', 'api.Admin/models');
|
||||
Route::post('models/test', 'api.Admin/testModel');
|
||||
Route::post('models/:id/test', 'api.Admin/testModel');
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Reference in New Issue
Block a user