post = $post ?? []; $this->headers = $headers ?? []; } /** * 设置请求参数 * @param array $post */ public function setPost($post) { $this->post = array_merge($this->post, $post); } /** * 设置请求头 * @param array $headers */ public function setHeaders($headers) { $this->headers = array_merge($this->headers, $headers); } /** * 设置请求路径 * @param string $urlPath */ public function setUrlPath($urlPath) { $this->urlPath = $urlPath; } /** * 设置请求超时时间 * @param int $timeout */ public function setTimeout($timeout) { $this->timeout = $timeout; } /** * 请求 AI 接口 * @param bool $resRaw 是否返回原始数据 * @return array */ public function request($resRaw = false) { $aiSetting = Base::setting('aiSetting'); if (!Setting::AIOpen()) { return Base::retError("AI 助手未开启"); } $headers = [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $aiSetting['ai_api_key'], ]; if ($aiSetting['ai_proxy']) { $headers['CURLOPT_PROXY'] = $aiSetting['ai_proxy']; $headers['CURLOPT_PROXYTYPE'] = str_contains($aiSetting['ai_proxy'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP; } $headers = array_merge($headers, $this->headers); $url = $aiSetting['ai_api_url'] ?: 'https://api.openai.com/v1'; $url = $url . ($this->urlPath ?: '/chat/completions'); $result = Ihttp::ihttp_request($url, $this->post, $headers, $this->timeout); if (Base::isError($result)) { return Base::retError("AI 接口请求失败", $result); } $result = $result['data']; if (!$resRaw) { $resData = Base::json2array($result); if (empty($resData['choices'])) { return Base::retError("AI 接口返回数据格式错误", $resData); } $result = $resData['choices'][0]['message']['content']; $result = trim($result); if (empty($result)) { return Base::retError("AI 接口返回数据为空"); } } return Base::retSuccess("success", $result); } /** ******************************************************************************************** */ /** ******************************************************************************************** */ /** ******************************************************************************************** */ /** * 通过 openAI 语音转文字 * @param string $filePath 语音文件路径 * @param array $extParams 扩展参数 * @param bool $noCache 是否禁用缓存 * @return array */ public static function transcriptions($filePath, $extParams = [], $noCache = false) { if (!file_exists($filePath)) { return Base::retError("语音文件不存在"); } $systemSetting = Base::setting('system'); if ($systemSetting['voice2text'] !== 'open') { return Base::retError("语音转文字功能未开启"); } $cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extParams)); if ($noCache) { Cache::forget($cacheKey); } $result = Cache::remember($cacheKey, Carbon::now()->addDays(), function () use ($extParams, $filePath) { $post = array_merge($extParams, [ 'file' => new \CURLFile($filePath), 'model' => 'whisper-1', ]); $header = [ 'Content-Type' => 'multipart/form-data', ]; $ai = new self($post, $header); $ai->setUrlPath('/audio/transcriptions'); $ai->setTimeout(15); $res = $ai->request(true); if (Base::isError($res)) { return Base::retError("语音转文字失败", $res); } $resData = Base::json2array($res['data']); if (empty($resData['text'])) { return Base::retError("语音转文字失败", $resData); } return Base::retSuccess("success", [ 'file' => $filePath, 'text' => $resData['text'], ]); }); if (Base::isError($result)) { Cache::forget($cacheKey); } return $result; } /** * 通过 openAI 翻译 * @param string $text 需要翻译的文本内容 * @param string $targetLanguage 目标语言(如:English, 简体中文, 日本語等) * @param bool $noCache 是否禁用缓存 * @return array */ public static function translations($text, $targetLanguage, $noCache = false) { $systemSetting = Base::setting('system'); if ($systemSetting['translation'] !== 'open') { return Base::retError("翻译功能未开启"); } $cacheKey = "openAItranslations::" . md5($text . '_' . $targetLanguage); if ($noCache) { Cache::forget($cacheKey); } $result = Cache::remember($cacheKey, Carbon::now()->addDays(7), function () use ($text, $targetLanguage) { $post = json_encode([ "model" => "gpt-5-mini", "reasoning_effort" => "minimal", "messages" => [ [ "role" => "system", "content" => << "user", "content" => "请将以下内容翻译为 {$targetLanguage}:\n\n{$text}" ] ], ]); $ai = new self($post); $ai->setTimeout(60); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("翻译请求失败", $res); } $result = $res['data']; if (empty($result)) { return Base::retError("翻译结果为空"); } return Base::retSuccess("success", [ 'translated_text' => $result, 'target_language' => $targetLanguage, 'translated_at' => date('Y-m-d H:i:s') ]); }); if (Base::isError($result)) { Cache::forget($cacheKey); } return $result; } /** * 通过 openAI 生成标题 * @param string $text 需要生成标题的文本内容 * @param bool $noCache 是否禁用缓存 * @return array */ public static function generateTitle($text, $noCache = false) { $cacheKey = "openAIGenerateTitle::" . md5($text); if ($noCache) { Cache::forget($cacheKey); } $result = Cache::remember($cacheKey, Carbon::now()->addHours(24), function () use ($text) { $post = json_encode([ "model" => "gpt-5-mini", "reasoning_effort" => "minimal", "messages" => [ [ "role" => "system", "content" => << "user", "content" => "请为以下内容生成一个合适的标题:\n\n" . $text ] ], ]); $ai = new self($post); $ai->setTimeout(10); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("标题生成失败", $res); } $result = $res['data']; if (empty($result)) { return Base::retError("生成的标题为空"); } return Base::retSuccess("success", [ 'title' => $result, 'length' => mb_strlen($result), 'generated_at' => date('Y-m-d H:i:s') ]); }); if (Base::isError($result)) { Cache::forget($cacheKey); } return $result; } /** * 通过 openAI 生成任务标题和描述 * @param string $text 用户提供的提示词 * @param array $context 上下文信息 * @return array */ public static function generateTask($text, $context = []) { // 构建上下文提示信息 $contextPrompt = self::buildTaskContextPrompt($context); $post = json_encode([ "model" => "gpt-5-mini", "reasoning_effort" => "minimal", "messages" => [ [ "role" => "system", "content" => << "user", "content" => $contextPrompt . "\n\n请根据以上上下文并结合以下提示词生成一个完整的项目任务(包含标题和详细描述):\n\n" . $text ] ], ]); $ai = new self($post); $ai->setTimeout(60); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("任务生成失败", $res); } // 清理可能的markdown代码块标记 $content = $res['data']; $content = preg_replace('/^\s*```json\s*/', '', $content); $content = preg_replace('/\s*```\s*$/', '', $content); if (empty($content)) { return Base::retError("任务生成结果为空"); } // 解析JSON $parsedData = Base::json2array($content); if (!$parsedData || !isset($parsedData['title']) || !isset($parsedData['content'])) { return Base::retError("任务生成格式错误", $content); } $title = trim($parsedData['title']); $markdownContent = trim($parsedData['content']); $rawSubtasks = $parsedData['subtasks'] ?? []; if (empty($title) || empty($markdownContent)) { return Base::retError("生成的任务标题或内容为空", $parsedData); } $subtasks = []; if (is_array($rawSubtasks)) { foreach ($rawSubtasks as $raw) { if (is_array($raw)) { $name = trim($raw['title'] ?? $raw['name'] ?? ''); } else { $name = trim($raw); } if (!empty($name)) { $subtasks[] = $name; } if (count($subtasks) >= 8) { break; } } } return Base::retSuccess("success", [ 'title' => $title, 'content' => Base::markdown2html($markdownContent), // 将 Markdown 转换为 HTML 'subtasks' => $subtasks ]); } /** * 通过 openAI 生成项目名称与任务列表 * @param string $text 用户提供的提示词 * @param array $context 上下文信息 * @return array */ public static function generateProject($text, $context = []) { $text = trim((string)$text); if ($text === '') { return Base::retError("项目提示词不能为空"); } $context['current_name'] = trim($context['current_name'] ?? ''); $context['current_columns'] = self::normalizeProjectColumns($context['current_columns'] ?? []); if (!empty($context['template_examples']) && is_array($context['template_examples'])) { $examples = []; foreach ($context['template_examples'] as $item) { $name = trim($item['name'] ?? ''); $columns = self::normalizeProjectColumns($item['columns'] ?? []); if (empty($columns)) { continue; } $examples[] = [ 'name' => $name, 'columns' => $columns, ]; if (count($examples) >= 6) { break; } } $context['template_examples'] = $examples; } else { $context['template_examples'] = []; } $contextPrompt = self::buildProjectContextPrompt($context); $post = json_encode([ "model" => "gpt-5-mini", "reasoning_effort" => "minimal", "messages" => [ [ "role" => "system", "content" => << "user", "content" => ($contextPrompt ? $contextPrompt . "\n\n" : "") . "请根据以上信息,并结合以下提示词生成适合的项目名称和任务列表:\n\n" . $text ], ], ]); $ai = new self($post); $ai->setTimeout(45); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("项目生成失败", $res); } $content = $res['data']; $content = preg_replace('/^\s*```json\s*/', '', $content); $content = preg_replace('/\s*```\s*$/', '', $content); if (empty($content)) { return Base::retError("项目生成结果为空"); } $parsedData = Base::json2array($content); if (!$parsedData || !isset($parsedData['name'])) { return Base::retError("项目生成格式错误", $content); } $name = trim($parsedData['name']); $columns = self::normalizeProjectColumns($parsedData['columns'] ?? []); if ($name === '') { return Base::retError("生成的项目名称为空", $parsedData); } if (empty($columns)) { $columns = $context['current_columns']; } return Base::retSuccess("success", [ 'name' => $name, 'columns' => $columns, ]); } /** * 对工作汇报内容进行分析 * @param Report $report * @param array $context * @return array */ public static function analyzeReport(Report $report, array $context = []) { $prompt = self::buildReportAnalysisPrompt($report, $context); if ($prompt === '') { return Base::retError("报告内容为空,无法进行分析"); } $post = json_encode([ "model" => "gpt-5-mini", "reasoning_effort" => "minimal", "messages" => [ [ "role" => "system", "content" => << "user", "content" => $prompt, ], ], ]); $ai = new self($post); $ai->setTimeout(60); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("工作汇报分析失败", $res); } $content = trim($res['data']); $content = preg_replace('/^\s*```(?:markdown|md|text)?\s*/i', '', $content); $content = preg_replace('/\s*```\s*$/', '', $content); $content = trim($content); if ($content === '') { return Base::retError("工作汇报分析结果为空"); } return Base::retSuccess("success", [ 'text' => $content, ]); } /** * 整理优化工作汇报内容 * @param string $markdown 用户当前的工作汇报(Markdown) * @param array $context 上下文信息 * @return array */ public static function organizeReportContent(string $markdown, array $context = []) { $markdown = trim((string)$markdown); if ($markdown === '') { return Base::retError("工作汇报内容不能为空"); } $prompt = self::buildReportOrganizePrompt($markdown, $context); if ($prompt === '') { return Base::retError("整理内容为空"); } $post = json_encode([ "model" => "gpt-5-mini", "reasoning_effort" => "minimal", "messages" => [ [ "role" => "system", "content" => << "user", "content" => $prompt, ], ], ]); $ai = new self($post); $ai->setTimeout(60); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("汇报整理失败", $res); } $content = trim($res['data']); $content = preg_replace('/^\s*```(?:markdown|md|text)?\s*/i', '', $content); $content = preg_replace('/\s*```\s*$/', '', $content); $content = trim($content); if ($content === '') { return Base::retError("汇报整理结果为空"); } return Base::retSuccess("success", [ 'text' => $content, ]); } /** * 构建任务生成的上下文提示信息 * @param array $context 上下文信息 * @return string */ private static function buildTaskContextPrompt($context) { $prompts = []; // 当前任务信息 if (!empty($context['current_title']) || !empty($context['current_content'])) { $prompts[] = "## 当前任务信息"; if (!empty($context['current_title'])) { $prompts[] = "当前标题:" . $context['current_title']; } if (!empty($context['current_content'])) { $prompts[] = "当前内容:" . $context['current_content']; } $prompts[] = "请在此基础上优化改进,而不是完全重写。"; } // 任务模板信息 if (!empty($context['template_name']) || !empty($context['template_content'])) { $prompts[] = "## 任务模板要求"; if (!empty($context['template_name'])) { $prompts[] = "模板名称:" . $context['template_name']; } if (!empty($context['template_content'])) { $prompts[] = "模板内容结构:" . $context['template_content']; } $prompts[] = "请严格按照此模板的结构和格式要求生成内容。"; } // 项目状态信息 $statusInfo = []; if (!empty($context['has_owner'])) { $statusInfo[] = "已设置负责人"; } if (!empty($context['has_time_plan'])) { $statusInfo[] = "已设置计划时间"; } if (!empty($context['priority_level'])) { $statusInfo[] = "优先级:" . $context['priority_level']; } if (!empty($statusInfo)) { $prompts[] = "## 任务状态"; $prompts[] = implode(",", $statusInfo); $prompts[] = "请在任务描述中体现相应的要求和约束。"; } return empty($prompts) ? "" : implode("\n", $prompts); } private static function buildProjectContextPrompt($context) { $prompts = []; if (!empty($context['current_name']) || !empty($context['current_columns'])) { $prompts[] = "## 当前项目草稿"; if (!empty($context['current_name'])) { $prompts[] = "已有名称:" . $context['current_name']; } if (!empty($context['current_columns'])) { $prompts[] = "现有任务列表:" . implode("、", $context['current_columns']); } $prompts[] = "请在此基础上进行优化和补充。"; } if (!empty($context['template_examples'])) { $prompts[] = "## 常用模板示例"; foreach ($context['template_examples'] as $example) { $line = ''; if (!empty($example['name'])) { $line .= $example['name'] . ":"; } $line .= implode("、", $example['columns']); $prompts[] = "- " . $line; } $prompts[] = "可以借鉴以上结构,但要结合用户需求生成更贴合的方案。"; } return empty($prompts) ? "" : implode("\n", $prompts); } public static function messageSystemPrompt() { return << '群聊', 'user' => '单聊']; $prompts[] = "类型:" . ($typeMap[$context['dialog_type']] ?? $context['dialog_type']); } if (!empty($context['group_type'])) { $prompts[] = "分类:" . Base::cutStr($context['group_type'], 60); } } if (!empty($context['members']) && is_array($context['members'])) { $members = array_slice(array_filter($context['members']), 0, 10); if (!empty($members)) { $prompts[] = "## 会话成员"; $prompts[] = implode(",", array_map(fn($name) => Base::cutStr($name, 30), $members)); } } if (!empty($context['recent_messages']) && is_array($context['recent_messages'])) { $prompts[] = "## 最近消息"; foreach ($context['recent_messages'] as $item) { $sender = Base::cutStr(trim($item['sender'] ?? ''), 40) ?: '成员'; $summary = Base::cutStr(trim($item['summary'] ?? ''), 120); if ($summary !== '') { $prompts[] = "- {$sender}:{$summary}"; } } } if (!empty($context['quote_summary'])) { $prompts[] = "## 引用消息"; $quoteUser = Base::cutStr(trim($context['quote_user'] ?? ''), 40); $quoteText = Base::cutStr(trim($context['quote_summary']), 200); if ($quoteUser !== '') { $prompts[] = "{$quoteUser}:{$quoteText}"; } else { $prompts[] = $quoteText; } } if (!empty($context['current_draft'])) { $prompts[] = "## 当前草稿"; $prompts[] = Base::cutStr(trim($context['current_draft']), 200); } return empty($prompts) ? "" : implode("\n", $prompts); } private static function normalizeProjectColumns($columns) { if (is_string($columns)) { $columns = preg_split('/[\n\r,,;;|]/u', $columns); } $normalized = []; if (is_array($columns)) { foreach ($columns as $item) { if (is_array($item)) { $item = $item['name'] ?? $item['title'] ?? reset($item); } $item = trim((string)$item); if ($item === '') { continue; } $item = mb_substr($item, 0, 30); if (!in_array($item, $normalized)) { $normalized[] = $item; } if (count($normalized) >= 8) { break; } } } return $normalized; } /** * 通过 openAI 生成职场笑话、心灵鸡汤 * @param bool $noCache 是否禁用缓存 * @return array 返回20个笑话和20个心灵鸡汤 */ public static function generateJokeAndSoup($noCache = false) { $cacheKey = "openAIJokeAndSoup::" . md5(date('Y-m-d')); if ($noCache) { Cache::forget($cacheKey); } $result = Cache::remember($cacheKey, Carbon::now()->addHours(6), function () { $post = json_encode([ "model" => "gpt-5-mini", "reasoning_effort" => "minimal", "messages" => [ [ "role" => "system", "content" => << "user", "content" => "请生成20个职场笑话和20个心灵鸡汤" ] ], ]); $ai = new self($post); $ai->setTimeout(120); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("生成失败", $res); } // 清理可能的markdown代码块标记 $content = $res['data']; $content = preg_replace('/^\s*```json\s*/', '', $content); $content = preg_replace('/\s*```\s*$/', '', $content); if (empty($content)) { return Base::retError("翻译结果为空"); } // 解析JSON $parsedData = Base::json2array($content); if (!$parsedData || !isset($parsedData['jokes']) || !isset($parsedData['soups'])) { return Base::retError("生成内容格式错误", $content); } // 验证数据完整性 if (!is_array($parsedData['jokes']) || !is_array($parsedData['soups'])) { return Base::retError("生成内容格式错误", $parsedData); } // 过滤空内容并确保有内容 $jokes = array_filter(array_map('trim', $parsedData['jokes'])); $soups = array_filter(array_map('trim', $parsedData['soups'])); if (empty($jokes) || empty($soups)) { return Base::retError("生成内容为空", $parsedData); } return Base::retSuccess("success", [ 'jokes' => array_values($jokes), 'soups' => array_values($soups), 'total_jokes' => count($jokes), 'total_soups' => count($soups), 'generated_at' => date('Y-m-d H:i:s') ]); }); if (Base::isError($result)) { Cache::forget($cacheKey); } return $result; } /** * 构建工作汇报分析的提示词 * @param Report $report * @param array $context * @return string */ private static function buildReportAnalysisPrompt(Report $report, array $context = []): string { $sections = []; $metaLines = []; $metaLines[] = "标题:" . ($report->title ?: "(未填写)"); $metaLines[] = "类型:" . match ($report->type) { Report::WEEKLY => "周报", Report::DAILY => "日报", default => $report->type, }; if ($report->created_at) { $createdAt = $report->created_at instanceof Carbon ? $report->created_at->toDateTimeString() : (string)$report->created_at; $metaLines[] = "提交时间:" . $createdAt; } if (!empty($context['viewer_role'])) { $metaLines[] = "查看人角色:" . trim($context['viewer_role']); } if (!empty($context['viewer_name'])) { $metaLines[] = "查看人:" . trim($context['viewer_name']); } if (!empty($metaLines)) { $sections[] = "### 基础信息\n" . implode("\n", array_map(function ($line) { return "- " . $line; }, $metaLines)); } if (!empty($context['focus']) && is_array($context['focus'])) { $focusItems = array_filter(array_map('trim', $context['focus'])); if (!empty($focusItems)) { $sections[] = "### 关注重点\n" . implode("\n", array_map(function ($line) { return "- " . $line; }, $focusItems)); } } elseif (!empty($context['focus_note'])) { $sections[] = "### 关注重点\n- " . trim($context['focus_note']); } if (!empty($context['previous_feedback'])) { $sections[] = "### 历史反馈\n" . trim($context['previous_feedback']); } $contentMarkdown = trim(Base::html2markdown($report->content)); if ($contentMarkdown !== '') { $sections[] = "### 汇报正文\n" . $contentMarkdown; } return trim(implode("\n\n", array_filter($sections))); } /** * 构建工作汇报整理的提示词 * @param string $markdown * @param array $context * @return string */ private static function buildReportOrganizePrompt(string $markdown, array $context = []): string { $sections = []; $infoLines = []; if (!empty($context['title'])) { $infoLines[] = "汇报标题:" . trim($context['title']); } if (!empty($context['type'])) { $typeLabel = match ($context['type']) { Report::WEEKLY => "周报", Report::DAILY => "日报", default => $context['type'], }; $infoLines[] = "汇报类型:" . $typeLabel; } if (!empty($context['focus']) && is_array($context['focus'])) { $focusItems = array_filter(array_map('trim', $context['focus'])); if (!empty($focusItems)) { $sections[] = "### 需要重点整理的方向\n" . implode("\n", array_map(function ($line) { return "- " . $line; }, $focusItems)); } } if (!empty($infoLines)) { $sections[] = "### 汇报背景(仅供参考,请勿写入输出)\n" . implode("\n", array_map(function ($line) { return "- " . $line; }, $infoLines)); } $sections[] = "### 原始汇报草稿(请整理后仅输出正文内容)\n" . $markdown; return trim(implode("\n\n", array_filter($sections))); } /** * 获取 ollama 模型 * @param $baseUrl * @param $key * @param $agency * @return array */ public static function ollamaModels($baseUrl, $key = null, $agency = null) { $extra = [ 'Content-Type' => 'application/json', ]; if ($key) { $extra['Authorization'] = 'Bearer ' . $key; } if ($agency) { $extra['CURLOPT_PROXY'] = $agency; $extra['CURLOPT_PROXYTYPE'] = str_contains($agency, 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP; } $res = Ihttp::ihttp_request(rtrim($baseUrl, '/') . '/api/tags', [], $extra, 15); if (Base::isError($res)) { return Base::retError("获取失败", $res); } $resData = Base::json2array($res['data']); if (empty($resData['models'])) { return Base::retError("获取失败", $resData); } $models = []; foreach ($resData['models'] as $model) { if ($model['name'] !== $model['model']) { $models[] = "{$model['model']} | {$model['name']}"; } else { $models[] = $model['model']; } } return Base::retSuccess("success", [ 'models' => $models, 'original' => $resData['models'] ]); } }