mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-10 18:02:55 +00:00
feat: 重构报告分析功能,更新API接口,移除冗余代码,优化分析逻辑
This commit is contained in:
parent
0b6c478b4f
commit
a5adbf80a9
@ -11,7 +11,6 @@ use App\Models\ReportLink;
|
||||
use App\Models\ReportReceive;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\AI;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Tasks\PushTask;
|
||||
@ -523,33 +522,38 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/report/ai_analyze 生成工作汇报 AI 分析
|
||||
* @api {post} api/report/analysave 保存工作汇报 AI 分析
|
||||
*
|
||||
* @apiDescription 需要token身份,仅支持报告提交人或接收人发起分析
|
||||
* @apiDescription 需要token身份,仅支持报告提交人或接收人保存分析
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName ai_analyze
|
||||
* @apiName analysave
|
||||
*
|
||||
* @apiParam {Number} id 报告ID
|
||||
* @apiParam {Array|String} [focus] 分析关注点(可选)
|
||||
* @apiParam {Number} id 报告ID
|
||||
* @apiParam {String} text 分析内容(Markdown)
|
||||
* @apiParam {String} [model] 分析使用的模型标识(可选)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
* @apiSuccess {Number} data.id 分析记录ID
|
||||
* @apiSuccess {String} data.text 分析内容(Markdown)
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
* @apiSuccess {Number} data.id 分析记录ID
|
||||
* @apiSuccess {String} data.text 分析内容(Markdown)
|
||||
* @apiSuccess {String} data.updated_at 最近更新时间
|
||||
*/
|
||||
public function ai_analyze(): array
|
||||
public function analysave(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
$id = intval(Request::input("id"));
|
||||
if ($id <= 0) {
|
||||
return Base::retError("缺少ID参数");
|
||||
}
|
||||
$text = trim((string)Request::input('text', ''));
|
||||
if ($text === '') {
|
||||
return Base::retError("分析内容不能为空");
|
||||
}
|
||||
$model = trim((string)Request::input('model', ''));
|
||||
|
||||
$report = Report::getOne($id);
|
||||
|
||||
if (!$this->userCanAccessReport($report, $user)) {
|
||||
return Base::retError("无权访问该工作汇报");
|
||||
}
|
||||
@ -559,31 +563,6 @@ class ReportController extends AbstractController
|
||||
->whereUserid($user->userid)
|
||||
->first();
|
||||
|
||||
$context = [
|
||||
'viewer_name' => $user->nickname ?? ('用户' . $user->userid),
|
||||
];
|
||||
if (!empty($user->profession)) {
|
||||
$context['viewer_role'] = $user->profession;
|
||||
} elseif (is_array($user->identity) && !empty($user->identity)) {
|
||||
$context['viewer_role'] = implode('/', $user->identity);
|
||||
}
|
||||
if ($analysis && $analysis->analysis_text) {
|
||||
$context['previous_feedback'] = $analysis->analysis_text;
|
||||
}
|
||||
|
||||
$focus = Request::input('focus');
|
||||
if (is_array($focus)) {
|
||||
$context['focus'] = $focus;
|
||||
} elseif (is_string($focus) && trim($focus) !== '') {
|
||||
$context['focus_note'] = trim($focus);
|
||||
}
|
||||
|
||||
$result = AI::analyzeReport($report, $context);
|
||||
if (Base::isError($result)) {
|
||||
return Base::retError("生成AI分析失败", $result);
|
||||
}
|
||||
$data = $result['data'];
|
||||
|
||||
if (!$analysis) {
|
||||
$analysis = ReportAnalysis::fillInstance([
|
||||
'rid' => $report->id,
|
||||
@ -591,10 +570,19 @@ class ReportController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
$viewerRole = $user->profession ?: (is_array($user->identity) && !empty($user->identity) ? implode('/', $user->identity) : null);
|
||||
$focusMeta = null;
|
||||
$focus = Request::input('focus');
|
||||
if (is_array($focus)) {
|
||||
$focusMeta = array_filter(array_map('trim', $focus));
|
||||
} elseif (is_string($focus) && trim($focus) !== '') {
|
||||
$focusMeta = [trim($focus)];
|
||||
}
|
||||
|
||||
$meta = array_filter([
|
||||
'viewer_role' => $context['viewer_role'] ?? null,
|
||||
'viewer_name' => $context['viewer_name'] ?? null,
|
||||
'focus' => $context['focus'] ?? null,
|
||||
'viewer_role' => $viewerRole,
|
||||
'viewer_name' => $user->nickname ?? null,
|
||||
'focus' => $focusMeta,
|
||||
], function ($value) {
|
||||
if (is_array($value)) {
|
||||
return !empty($value);
|
||||
@ -603,8 +591,8 @@ class ReportController extends AbstractController
|
||||
});
|
||||
|
||||
$analysis->updateInstance([
|
||||
'model' => $data['model'] ?? '',
|
||||
'analysis_text' => $data['text'],
|
||||
'model' => $model,
|
||||
'analysis_text' => $text,
|
||||
'meta' => $meta,
|
||||
]);
|
||||
$analysis->save();
|
||||
@ -614,72 +602,11 @@ class ReportController extends AbstractController
|
||||
return Base::retSuccess("success", [
|
||||
'id' => $analysis->id,
|
||||
'text' => $analysis->analysis_text,
|
||||
'model' => $analysis->model,
|
||||
'updated_at' => $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/report/ai_organize 整理工作汇报内容
|
||||
*
|
||||
* @apiDescription 需要token身份,根据当前草稿重新整理工作汇报结构
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName ai_organize
|
||||
*
|
||||
* @apiParam {String} content 汇报内容(HTML)
|
||||
* @apiParam {String} [title] 汇报标题
|
||||
* @apiParam {String} [type] 汇报类型(weekly/daily)
|
||||
* @apiParam {Array|String} [focus] 整理关注点(可选)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
* @apiSuccess {String} data.html 整理后的内容(HTML)
|
||||
*/
|
||||
public function ai_organize(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
$content = trim((string)Request::input("content", ""));
|
||||
if ($content === '') {
|
||||
return Base::retError("汇报内容不能为空");
|
||||
}
|
||||
|
||||
$title = trim((string)Request::input("title", ""));
|
||||
$type = trim((string)Request::input("type", ""));
|
||||
|
||||
$markdown = Base::html2markdown($content);
|
||||
if ($markdown === '') {
|
||||
return Base::retError("汇报内容解析失败");
|
||||
}
|
||||
|
||||
$context = array_filter([
|
||||
'title' => $title,
|
||||
'type' => $type,
|
||||
]);
|
||||
|
||||
$focus = Request::input('focus');
|
||||
if (is_array($focus)) {
|
||||
$context['focus'] = $focus;
|
||||
} elseif (is_string($focus) && trim($focus) !== '') {
|
||||
$context['focus'] = [trim($focus)];
|
||||
}
|
||||
|
||||
$result = AI::organizeReportContent($markdown, $context);
|
||||
if (Base::isError($result)) {
|
||||
return Base::retError("整理汇报失败", $result);
|
||||
}
|
||||
|
||||
$data = $result['data'];
|
||||
$html = Base::markdown2html($data['text']);
|
||||
if (trim($html) === '') {
|
||||
return Base::retError("整理后的内容为空");
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", [
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/mark 标记已读/未读
|
||||
*
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use App\Models\Report;
|
||||
use App\Models\Setting;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
@ -318,135 +317,6 @@ class AI
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对工作汇报内容进行分析
|
||||
* @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" => <<<EOF
|
||||
你是一名经验丰富的团队管理顾问,擅长阅读和分析员工提交的工作汇报,能够快速提炼重点并给出可执行建议。
|
||||
|
||||
输出要求:
|
||||
1. 使用简洁的 Markdown 结构(标题、无序列表、引用等),不要使用代码块或 JSON
|
||||
2. 先给出整体概览,再列出具体亮点、风险或问题,以及明确的改进建议
|
||||
3. 如有数据或目标,应评估其完成情况和后续跟进要点
|
||||
4. 语气保持专业、客观,中立,不过度夸赞或批评
|
||||
5. 控制在 200-400 字之间,必要时可略微增减,但保持紧凑
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "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" => <<<EOF
|
||||
你是一名资深的职场写作顾问,擅长根据已有的工作汇报草稿进行整理、结构化和措辞优化。
|
||||
|
||||
工作任务:
|
||||
1. 保留草稿中的事实、数据和结论,确保信息准确无误
|
||||
2. 重新组织结构,让内容清晰分段(如「重点进展」「成果亮点」「问题与风险」「后续计划」等),并按照草稿中的时间范围或类型进行表达
|
||||
3. 用简洁、专业且积极的语气描述,并突出可复用的要点
|
||||
4. 支持使用 Markdown 标题、列表、引用、表格等语法增强可读性,但不要返回 HTML 或代码块
|
||||
5. 若草稿信息不完整,可合理推测缺失项并以占位符提示(如「待补充」),不要臆造细节
|
||||
|
||||
输出要求:
|
||||
- 仅返回整理后的 Markdown 正文内容,并用于直接替换原草稿
|
||||
- 不得输出任何汇报名称、汇报对象、汇报类型或其他元信息,即便草稿或上下文中存在这些字段
|
||||
- 不加额外说明、指引、总结或前缀后缀
|
||||
- 内容保持精炼、结构清晰
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 生成职场笑话、心灵鸡汤
|
||||
* @param bool $noCache 是否禁用缓存
|
||||
@ -546,106 +416,6 @@ class AI
|
||||
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
|
||||
|
||||
@ -2218,7 +2218,6 @@ MCP 服务器已启动成功!
|
||||
AI 分析
|
||||
重新分析
|
||||
生成分析
|
||||
AI 正在生成分析...
|
||||
最后更新:
|
||||
暂无 AI 分析,点击右侧按钮生成。
|
||||
AI 整理汇报
|
||||
|
||||
@ -31811,18 +31811,6 @@
|
||||
"id": "Hasilkan analisis",
|
||||
"ru": "Сгенерировать анализ"
|
||||
},
|
||||
{
|
||||
"key": "AI 正在生成分析...",
|
||||
"zh": "",
|
||||
"zh-CHT": "AI 正在生成分析...",
|
||||
"en": "AI is generating analysis...",
|
||||
"ko": "AI가 분석을 생성하는 중...",
|
||||
"ja": "AIが分析を生成しています...",
|
||||
"de": "KI erstellt die Analyse...",
|
||||
"fr": "L’IA génère l’analyse...",
|
||||
"id": "AI sedang menghasilkan analisis...",
|
||||
"ru": "ИИ формирует анализ..."
|
||||
},
|
||||
{
|
||||
"key": "最后更新:",
|
||||
"zh": "",
|
||||
|
||||
@ -49,16 +49,12 @@
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="aiLoading"
|
||||
:loading="analysisSaving"
|
||||
@click="onAnalyze">
|
||||
{{ aiAnalysis ? $L("重新分析") : $L("生成分析") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="aiLoading" class="analysis-loading">
|
||||
<Icon type="ios-loading" class="icon-loading"/>
|
||||
<span>{{ $L("AI 正在生成分析...") }}</span>
|
||||
</div>
|
||||
<div v-else-if="aiAnalysis" class="analysis-content">
|
||||
<div v-if="aiAnalysis" class="analysis-content">
|
||||
<div v-if="aiAnalysis.updated_at" class="analysis-meta">
|
||||
{{ $L("最后更新:") }}{{ aiAnalysis.updated_at }}
|
||||
</div>
|
||||
@ -77,6 +73,9 @@
|
||||
<script>
|
||||
const VMPreview = () => import('../../../components/VMEditor/preview');
|
||||
import {mapState} from "vuex";
|
||||
import emitter from "../../../store/events";
|
||||
import {extractPlainText} from "../../../utils/text";
|
||||
import {REPORT_ANALYSIS_SYSTEM_PROMPT} from "../../../utils/ai";
|
||||
|
||||
export default {
|
||||
name: "ReportDetail",
|
||||
@ -92,13 +91,13 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loadIng: 0,
|
||||
aiLoading: false,
|
||||
analysisSaving: false,
|
||||
aiAnalysis: null,
|
||||
detail: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['formOptions']),
|
||||
...mapState(['formOptions', 'userInfo']),
|
||||
currentDetail() {
|
||||
return this.detail || this.data || {};
|
||||
}
|
||||
@ -107,6 +106,7 @@ export default {
|
||||
'data.id': {
|
||||
handler(id) {
|
||||
if (id > 0) {
|
||||
this.analysisSaving = false;
|
||||
this.aiAnalysis = this.data?.ai_analysis || null;
|
||||
this.detail = null;
|
||||
if (this.type === 'view') {
|
||||
@ -114,6 +114,7 @@ export default {
|
||||
this.fetchDetail();
|
||||
}
|
||||
} else {
|
||||
this.analysisSaving = false;
|
||||
this.aiAnalysis = null;
|
||||
this.detail = null;
|
||||
}
|
||||
@ -160,28 +161,188 @@ export default {
|
||||
});
|
||||
},
|
||||
onAnalyze() {
|
||||
if (!this.currentDetail.id || this.aiLoading) {
|
||||
if (this.analysisSaving) {
|
||||
return;
|
||||
}
|
||||
this.aiLoading = true;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'report/ai_analyze',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: this.currentDetail.id,
|
||||
},
|
||||
timeout: 60 * 1000,
|
||||
}).then(({data}) => {
|
||||
this.aiAnalysis = data;
|
||||
if (this.detail) {
|
||||
this.detail.ai_analysis = data;
|
||||
}
|
||||
}).catch(({msg}) => {
|
||||
$A.messageError(msg);
|
||||
}).finally(() => {
|
||||
this.aiLoading = false;
|
||||
if (!this.currentDetail.id) {
|
||||
$A.messageWarning("当前没有可分析的汇报");
|
||||
return;
|
||||
}
|
||||
const plain = extractPlainText(this.currentDetail.content || '');
|
||||
if (!plain) {
|
||||
$A.messageWarning("汇报内容为空,无法分析");
|
||||
return;
|
||||
}
|
||||
emitter.emit('openAIAssistant', {
|
||||
placeholder: this.$L('补充你想聚焦的风险、成果或建议,留空直接生成分析'),
|
||||
onBeforeSend: this.handleReportAnalysisBeforeSend,
|
||||
onApply: this.handleReportAnalysisApply,
|
||||
autoSubmit: true,
|
||||
});
|
||||
},
|
||||
|
||||
handleReportAnalysisBeforeSend(context = []) {
|
||||
const prepared = [
|
||||
['system', REPORT_ANALYSIS_SYSTEM_PROMPT]
|
||||
];
|
||||
const contextPrompt = this.buildReportAnalysisContextData();
|
||||
if (contextPrompt) {
|
||||
let assistantContext = [
|
||||
'以下是工作汇报详情,请据此输出结构化的分析:',
|
||||
contextPrompt,
|
||||
].join('\n');
|
||||
if ($A.getObject(context, [0,0]) === 'human') {
|
||||
assistantContext += "\n----\n请结合以上背景和以下补充说明完成分析:++++";
|
||||
}
|
||||
prepared.push(['human', assistantContext]);
|
||||
}
|
||||
if (context.length > 0) {
|
||||
prepared.push(...context);
|
||||
}
|
||||
return prepared;
|
||||
},
|
||||
|
||||
handleReportAnalysisApply({rawOutput, model}) {
|
||||
const text = (rawOutput || '').trim();
|
||||
if (!text) {
|
||||
$A.messageWarning("AI 未生成内容");
|
||||
return;
|
||||
}
|
||||
if (!this.currentDetail.id) {
|
||||
$A.messageWarning("当前没有可分析的汇报");
|
||||
return;
|
||||
}
|
||||
this.analysisSaving = true;
|
||||
const payload = {
|
||||
id: this.currentDetail.id,
|
||||
text,
|
||||
model: model || '',
|
||||
};
|
||||
return this.$store.dispatch("call", {
|
||||
url: 'report/analysave',
|
||||
method: 'post',
|
||||
data: payload,
|
||||
}).then(({data}) => {
|
||||
const analysis = data || {
|
||||
text,
|
||||
updated_at: $A.dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
this.aiAnalysis = analysis;
|
||||
if (this.detail) {
|
||||
this.$set(this.detail, 'ai_analysis', analysis);
|
||||
}
|
||||
$A.messageSuccess("AI 分析已更新");
|
||||
}).catch(({msg}) => {
|
||||
$A.messageError(msg || '保存 AI 分析失败');
|
||||
return Promise.reject(msg);
|
||||
}).finally(() => {
|
||||
this.analysisSaving = false;
|
||||
});
|
||||
},
|
||||
|
||||
buildReportAnalysisContextData() {
|
||||
const detail = this.currentDetail || {};
|
||||
if (!detail.id) {
|
||||
return '';
|
||||
}
|
||||
const sections = [];
|
||||
const meta = [];
|
||||
const title = (detail.title || '').trim();
|
||||
if (title) {
|
||||
meta.push(`标题:${title}`);
|
||||
}
|
||||
const typeLabel = this.resolveReportTypeLabel(detail.type || detail.type_val);
|
||||
if (typeLabel) {
|
||||
meta.push(`类型:${typeLabel}`);
|
||||
}
|
||||
if (detail.sign) {
|
||||
meta.push(`周期:${detail.sign}`);
|
||||
}
|
||||
if (detail.created_at) {
|
||||
meta.push(`提交时间:${detail.created_at}`);
|
||||
}
|
||||
const submitter = this.resolveUserName(detail.user || detail);
|
||||
if (submitter) {
|
||||
meta.push(`汇报人:${submitter}`);
|
||||
}
|
||||
const receivers = Array.isArray(detail.receives_user)
|
||||
? detail.receives_user
|
||||
.map(item => this.resolveUserName(item))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
if (receivers.length > 0) {
|
||||
meta.push(`接收人:${receivers.join('、')}`);
|
||||
}
|
||||
if (meta.length > 0) {
|
||||
sections.push('## 汇报信息');
|
||||
meta.forEach(line => sections.push(`- ${line}`));
|
||||
}
|
||||
|
||||
const viewerMeta = [];
|
||||
const viewerName = this.resolveUserName(this.userInfo);
|
||||
if (viewerName) {
|
||||
viewerMeta.push(`查看人:${viewerName}`);
|
||||
}
|
||||
const viewerRole = this.resolveViewerRole();
|
||||
if (viewerRole) {
|
||||
viewerMeta.push(`角色:${viewerRole}`);
|
||||
}
|
||||
if (viewerMeta.length > 0) {
|
||||
sections.push('## 查看上下文');
|
||||
viewerMeta.forEach(line => sections.push(`- ${line}`));
|
||||
}
|
||||
|
||||
const bodyText = extractPlainText(detail.content || '');
|
||||
if (bodyText) {
|
||||
const limit = 5000;
|
||||
const trimmed = bodyText.length > limit
|
||||
? `${bodyText.slice(0, limit)}...`
|
||||
: bodyText;
|
||||
sections.push('## 汇报正文');
|
||||
sections.push(trimmed);
|
||||
}
|
||||
|
||||
const previous = this.aiAnalysis?.text || detail.ai_analysis?.text;
|
||||
if (previous) {
|
||||
sections.push('## 历史分析供参考');
|
||||
sections.push(previous);
|
||||
}
|
||||
|
||||
return sections.join('\n').trim();
|
||||
},
|
||||
|
||||
resolveReportTypeLabel(type) {
|
||||
const map = {
|
||||
weekly: this.$L('周报'),
|
||||
daily: this.$L('日报'),
|
||||
};
|
||||
return map[type] || (typeof type === 'string' ? type : '');
|
||||
},
|
||||
|
||||
resolveUserName(user) {
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
if (typeof user === 'string') {
|
||||
return user;
|
||||
}
|
||||
const name = user.nickname || user.realname || user.name || user.username || '';
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (user.userid) {
|
||||
return `${this.$L('用户')} ${user.userid}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
resolveViewerRole() {
|
||||
const info = this.userInfo || {};
|
||||
if (Array.isArray(info.identity) && info.identity.length > 0) {
|
||||
return info.identity.join('/');
|
||||
}
|
||||
return info.profession || info.job || info.position || '';
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
10
resources/assets/js/utils/ai.js
vendored
10
resources/assets/js/utils/ai.js
vendored
@ -336,6 +336,15 @@ const REPORT_AI_SYSTEM_PROMPT = `你是一名资深团队管理教练,需要
|
||||
- 若原文包含数据或里程碑,保留并突出这些数字
|
||||
- 若某一章节没有信息,请输出“暂无”而非留空`;
|
||||
|
||||
const REPORT_ANALYSIS_SYSTEM_PROMPT = `你是一名经验丰富的团队管理顾问,擅长阅读和分析员工提交的工作汇报,能够快速提炼重点并给出可执行建议。
|
||||
|
||||
输出要求:
|
||||
1. 使用简洁的 Markdown 结构(标题、无序列表、引用等),不要使用代码块或 JSON
|
||||
2. 先给出整体概览,再列出具体亮点、风险或问题,以及明确的改进建议
|
||||
3. 如有数据或目标,应评估其完成情况和后续跟进要点
|
||||
4. 语气保持专业、客观、中立,不过度夸赞或批评
|
||||
5. 控制在 200-400 字之间,可视内容复杂度略微增减,但保持紧凑`;
|
||||
|
||||
export {
|
||||
AIModelNames,
|
||||
AINormalizeJsonContent,
|
||||
@ -345,4 +354,5 @@ export {
|
||||
TASK_AI_SYSTEM_PROMPT,
|
||||
PROJECT_AI_SYSTEM_PROMPT,
|
||||
REPORT_AI_SYSTEM_PROMPT,
|
||||
REPORT_ANALYSIS_SYSTEM_PROMPT,
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user