mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-24 17:21:40 +08:00
feat(pipeline): 策略页独立 strategy_config 与仅规则稿选项
新增 PipelineJob.strategy_config(默认 use_llm_default);GET /api/strategy-config-defaults/;PATCH 任务可保存策略偏好。策略生成页:本次仅规则稿复选框(默认关)、保存策略偏好、任务下拉去掉目录;第九章对齐仍由服务端默认处理。迁移 0021。 Made-with: Cursor
This commit is contained in:
parent
6e113bbdfe
commit
7d219feb3c
@ -191,6 +191,14 @@ def get_default_report_config() -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_strategy_config() -> dict[str, Any]:
|
||||||
|
"""策略生成页独立默认(与 ``report_config`` 无关),供前端回填与 PATCH 合并。"""
|
||||||
|
return {
|
||||||
|
# 打开页面时是否默认勾选「使用大模型生成」
|
||||||
|
"use_llm_default": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def write_competitor_analysis_for_run_dir(
|
def write_competitor_analysis_for_run_dir(
|
||||||
run_dir: Path,
|
run_dir: Path,
|
||||||
keyword: str,
|
keyword: str,
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2026-04-17 08:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pipeline', '0020_search_merged_volume_sort'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pipelinejob',
|
||||||
|
name='strategy_config',
|
||||||
|
field=models.JSONField(blank=True, default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -37,6 +37,8 @@ class PipelineJob(models.Model):
|
|||||||
scenario_filter_enabled = models.BooleanField(null=True, blank=True)
|
scenario_filter_enabled = models.BooleanField(null=True, blank=True)
|
||||||
# 竞品报告 / competitor-brief:关注词、场景词组、外部市场表等(JSON,空对象=用爬虫脚本默认)
|
# 竞品报告 / competitor-brief:关注词、场景词组、外部市场表等(JSON,空对象=用爬虫脚本默认)
|
||||||
report_config = models.JSONField(default=dict, blank=True)
|
report_config = models.JSONField(default=dict, blank=True)
|
||||||
|
# 策略生成页独立配置(与 report_config 无关),如默认是否使用大模型润色等
|
||||||
|
strategy_config = models.JSONField(default=dict, blank=True)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=16,
|
max_length=16,
|
||||||
choices=JobStatus.choices,
|
choices=JobStatus.choices,
|
||||||
|
|||||||
@ -86,6 +86,33 @@ def validate_report_config_body(value: dict) -> dict:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
_STRATEGY_CONFIG_ALLOWED_KEYS = frozenset({"use_llm_default"})
|
||||||
|
_DEFAULT_STRATEGY_CONFIG: dict[str, bool] = {"use_llm_default": False}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_strategy_config_body(value: dict) -> dict:
|
||||||
|
"""策略生成页独立 JSON(与 ``report_config`` 无关)。"""
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise serializers.ValidationError("须为 JSON 对象")
|
||||||
|
value = dict(value)
|
||||||
|
extra = set(value.keys()) - _STRATEGY_CONFIG_ALLOWED_KEYS
|
||||||
|
if extra:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"未知字段:{', '.join(sorted(extra))}"
|
||||||
|
)
|
||||||
|
if "use_llm_default" in value and value["use_llm_default"] is not None:
|
||||||
|
if not isinstance(value["use_llm_default"], bool):
|
||||||
|
raise serializers.ValidationError("use_llm_default 须为 true 或 false")
|
||||||
|
merged = {
|
||||||
|
**_DEFAULT_STRATEGY_CONFIG,
|
||||||
|
**{k: value[k] for k in _STRATEGY_CONFIG_ALLOWED_KEYS if k in value},
|
||||||
|
}
|
||||||
|
raw = json.dumps(merged, ensure_ascii=False)
|
||||||
|
if len(raw) > 32_000:
|
||||||
|
raise serializers.ValidationError("策略配置体积过大")
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
_ARTIFACT_FILES: tuple[tuple[str, str], ...] = (
|
_ARTIFACT_FILES: tuple[tuple[str, str], ...] = (
|
||||||
("merged", "keyword_pipeline_merged.csv"),
|
("merged", "keyword_pipeline_merged.csv"),
|
||||||
("pc_search", "pc_search_export.csv"),
|
("pc_search", "pc_search_export.csv"),
|
||||||
@ -119,6 +146,7 @@ class PipelineJobSerializer(serializers.ModelSerializer):
|
|||||||
"list_pages",
|
"list_pages",
|
||||||
"scenario_filter_enabled",
|
"scenario_filter_enabled",
|
||||||
"report_config",
|
"report_config",
|
||||||
|
"strategy_config",
|
||||||
"status",
|
"status",
|
||||||
"cancellation_requested",
|
"cancellation_requested",
|
||||||
"resume_from_checkpoint",
|
"resume_from_checkpoint",
|
||||||
@ -142,6 +170,7 @@ class PipelineJobSerializer(serializers.ModelSerializer):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"report_config",
|
"report_config",
|
||||||
|
"strategy_config",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_inline_cookie_used(self, obj: PipelineJob) -> bool:
|
def get_inline_cookie_used(self, obj: PipelineJob) -> bool:
|
||||||
@ -343,6 +372,15 @@ class JobReportConfigPatchSerializer(serializers.Serializer):
|
|||||||
return validate_report_config_body(value)
|
return validate_report_config_body(value)
|
||||||
|
|
||||||
|
|
||||||
|
class JobStrategyConfigPatchSerializer(serializers.Serializer):
|
||||||
|
strategy_config = serializers.JSONField()
|
||||||
|
|
||||||
|
def validate_strategy_config(self, value):
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise serializers.ValidationError("须为 JSON 对象")
|
||||||
|
return validate_strategy_config_body(value)
|
||||||
|
|
||||||
|
|
||||||
class RegenerateReportRequestSerializer(serializers.Serializer):
|
class RegenerateReportRequestSerializer(serializers.Serializer):
|
||||||
"""重新生成竞品报告:规则引擎或大模型(与 ``AI_crawler.chat_completion_text`` 同一网关)。"""
|
"""重新生成竞品报告:规则引擎或大模型(与 ``AI_crawler.chat_completion_text`` 同一网关)。"""
|
||||||
|
|
||||||
|
|||||||
22
backend/pipeline/tests/test_strategy_config.py
Normal file
22
backend/pipeline/tests/test_strategy_config.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""策略页独立 ``strategy_config`` 校验。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pipeline.serializers import validate_strategy_config_body
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_strategy_config_merges_default() -> None:
|
||||||
|
out = validate_strategy_config_body({})
|
||||||
|
assert out["use_llm_default"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_strategy_config_unknown_key() -> None:
|
||||||
|
with pytest.raises(serializers.ValidationError):
|
||||||
|
validate_strategy_config_body({"foo": 1})
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_strategy_config_use_llm_type() -> None:
|
||||||
|
with pytest.raises(serializers.ValidationError):
|
||||||
|
validate_strategy_config_body({"use_llm_default": "yes"})
|
||||||
@ -8,6 +8,11 @@ urlpatterns = [
|
|||||||
views.ReportConfigDefaultsView.as_view(),
|
views.ReportConfigDefaultsView.as_view(),
|
||||||
name="report-config-defaults",
|
name="report-config-defaults",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"strategy-config-defaults/",
|
||||||
|
views.StrategyConfigDefaultsView.as_view(),
|
||||||
|
name="strategy-config-defaults",
|
||||||
|
),
|
||||||
path("jobs/", views.JobListCreateView.as_view(), name="job-list-create"),
|
path("jobs/", views.JobListCreateView.as_view(), name="job-list-create"),
|
||||||
path("jobs/<int:pk>/", views.JobDetailView.as_view(), name="job-detail"),
|
path("jobs/<int:pk>/", views.JobDetailView.as_view(), name="job-detail"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@ -26,6 +26,7 @@ from .job_views import (
|
|||||||
JobRegenerateReportView,
|
JobRegenerateReportView,
|
||||||
JobResumeView,
|
JobResumeView,
|
||||||
ReportConfigDefaultsView,
|
ReportConfigDefaultsView,
|
||||||
|
StrategyConfigDefaultsView,
|
||||||
)
|
)
|
||||||
from .product_views import (
|
from .product_views import (
|
||||||
JdProductDetailView,
|
JdProductDetailView,
|
||||||
@ -55,6 +56,7 @@ __all__ = [
|
|||||||
"JobRegenerateReportView",
|
"JobRegenerateReportView",
|
||||||
"JobResumeView",
|
"JobResumeView",
|
||||||
"ReportConfigDefaultsView",
|
"ReportConfigDefaultsView",
|
||||||
|
"StrategyConfigDefaultsView",
|
||||||
"JdProductDetailView",
|
"JdProductDetailView",
|
||||||
"JdProductListView",
|
"JdProductListView",
|
||||||
"JdProductSnapshotDetailView",
|
"JdProductSnapshotDetailView",
|
||||||
|
|||||||
@ -18,6 +18,7 @@ from ..ingest import resolve_and_validate_run_dir
|
|||||||
from ..jd.runner import (
|
from ..jd.runner import (
|
||||||
build_competitor_brief_for_job,
|
build_competitor_brief_for_job,
|
||||||
get_default_report_config,
|
get_default_report_config,
|
||||||
|
get_default_strategy_config,
|
||||||
merge_llm_supplement_with_rules_report,
|
merge_llm_supplement_with_rules_report,
|
||||||
regenerate_competitor_report,
|
regenerate_competitor_report,
|
||||||
write_competitor_analysis_markdown,
|
write_competitor_analysis_markdown,
|
||||||
@ -28,6 +29,7 @@ from ..serializers import (
|
|||||||
CreatePipelineJobSerializer,
|
CreatePipelineJobSerializer,
|
||||||
JobReportConfigPatchSerializer,
|
JobReportConfigPatchSerializer,
|
||||||
JobResumeRequestSerializer,
|
JobResumeRequestSerializer,
|
||||||
|
JobStrategyConfigPatchSerializer,
|
||||||
PipelineJobSerializer,
|
PipelineJobSerializer,
|
||||||
RegenerateReportRequestSerializer,
|
RegenerateReportRequestSerializer,
|
||||||
)
|
)
|
||||||
@ -75,6 +77,7 @@ class JobListCreateView(APIView):
|
|||||||
list_pages=data.get("list_pages") or "",
|
list_pages=data.get("list_pages") or "",
|
||||||
scenario_filter_enabled=data.get("scenario_filter_enabled"),
|
scenario_filter_enabled=data.get("scenario_filter_enabled"),
|
||||||
report_config=report_config_initial,
|
report_config=report_config_initial,
|
||||||
|
strategy_config=get_default_strategy_config(),
|
||||||
status=JobStatus.PENDING,
|
status=JobStatus.PENDING,
|
||||||
)
|
)
|
||||||
t = threading.Thread(target=execute_job, args=(job.id,), daemon=True)
|
t = threading.Thread(target=execute_job, args=(job.id,), daemon=True)
|
||||||
@ -108,6 +111,11 @@ class JobDetailView(APIView):
|
|||||||
ser.is_valid(raise_exception=True)
|
ser.is_valid(raise_exception=True)
|
||||||
job.report_config = ser.validated_data["report_config"]
|
job.report_config = ser.validated_data["report_config"]
|
||||||
update_fields.append("report_config")
|
update_fields.append("report_config")
|
||||||
|
if "strategy_config" in body:
|
||||||
|
ser = JobStrategyConfigPatchSerializer(data={"strategy_config": body["strategy_config"]})
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
job.strategy_config = ser.validated_data["strategy_config"]
|
||||||
|
update_fields.append("strategy_config")
|
||||||
if "run_dir" in body:
|
if "run_dir" in body:
|
||||||
try:
|
try:
|
||||||
job.run_dir = str(
|
job.run_dir = str(
|
||||||
@ -118,7 +126,9 @@ class JobDetailView(APIView):
|
|||||||
update_fields.append("run_dir")
|
update_fields.append("run_dir")
|
||||||
if not update_fields:
|
if not update_fields:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "请提供 report_config 或 run_dir(用于绑定已有批次目录)"},
|
{
|
||||||
|
"detail": "请提供 report_config、strategy_config 或 run_dir(用于绑定已有批次目录)"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
job.save(update_fields=update_fields + ["updated_at"])
|
job.save(update_fields=update_fields + ["updated_at"])
|
||||||
@ -207,6 +217,13 @@ class ReportConfigDefaultsView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyConfigDefaultsView(APIView):
|
||||||
|
"""返回策略生成页独立默认 JSON(与 ``report_config`` 无关)。"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response(get_default_strategy_config())
|
||||||
|
|
||||||
|
|
||||||
class JobDownloadView(APIView):
|
class JobDownloadView(APIView):
|
||||||
def get(self, request, pk: int):
|
def get(self, request, pk: int):
|
||||||
job = PipelineJob.objects.filter(pk=pk).first()
|
job = PipelineJob.objects.filter(pk=pk).first()
|
||||||
|
|||||||
@ -192,6 +192,10 @@ export function reportConfigDefaultsUrl() {
|
|||||||
return '/api/report-config-defaults/'
|
return '/api/report-config-defaults/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function strategyConfigDefaultsUrl() {
|
||||||
|
return '/api/strategy-config-defaults/'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Record<string, string | number | undefined> | string} [opts] 筛选参数对象;兼容旧调用:传入字符串视为 comments 的 sku_id
|
* @param {Record<string, string | number | undefined> | string} [opts] 筛选参数对象;兼容旧调用:传入字符串视为 comments 的 sku_id
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
||||||
import { refreshJobs, useJobs, api } from '../../composables/useJobs'
|
import {
|
||||||
|
refreshJobs,
|
||||||
|
useJobs,
|
||||||
|
api,
|
||||||
|
strategyConfigDefaultsUrl,
|
||||||
|
} from '../../composables/useJobs'
|
||||||
import {
|
import {
|
||||||
generationInFlightKey,
|
generationInFlightKey,
|
||||||
withGenerationInFlight,
|
withGenerationInFlight,
|
||||||
@ -34,6 +39,11 @@ const strategyGeneratingOtherTask = computed(
|
|||||||
strategyDraftPendingJobId.value !== selectedId.value,
|
strategyDraftPendingJobId.value !== selectedId.value,
|
||||||
)
|
)
|
||||||
const useLlm = ref(false)
|
const useLlm = ref(false)
|
||||||
|
/** 与报告页「仅规则稿」一致:勾选则本次只出规则策略稿,不调用大模型润色 */
|
||||||
|
const rulesOnlyThisRun = ref(false)
|
||||||
|
const strategyDefaults = ref({ use_llm_default: false })
|
||||||
|
const strategySaveLoading = ref(false)
|
||||||
|
const strategySaveErr = ref('')
|
||||||
|
|
||||||
const decisions = reactive({
|
const decisions = reactive({
|
||||||
product_role: '',
|
product_role: '',
|
||||||
@ -56,10 +66,6 @@ const successJobs = computed(() =>
|
|||||||
[...jobs.value].filter((j) => j.status === 'success').sort((a, b) => b.id - a.id),
|
[...jobs.value].filter((j) => j.status === 'success').sort((a, b) => b.id - a.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedJob = computed(() =>
|
|
||||||
successJobs.value.find((j) => String(j.id) === selectedId.value),
|
|
||||||
)
|
|
||||||
|
|
||||||
const positioningOptions = [
|
const positioningOptions = [
|
||||||
{ value: '', label: '暂不勾选(文稿中均为空选)' },
|
{ value: '', label: '暂不勾选(文稿中均为空选)' },
|
||||||
{ value: 'top', label: '贴顶' },
|
{ value: 'top', label: '贴顶' },
|
||||||
@ -77,8 +83,9 @@ const stanceOptions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function buildPayload() {
|
function buildPayload() {
|
||||||
|
const generator = rulesOnlyThisRun.value ? 'rules' : useLlm.value ? 'llm' : 'rules'
|
||||||
return {
|
return {
|
||||||
generator: useLlm.value ? 'llm' : 'rules',
|
generator,
|
||||||
business_notes: businessNotes.value,
|
business_notes: businessNotes.value,
|
||||||
product_role: decisions.product_role,
|
product_role: decisions.product_role,
|
||||||
time_horizon: decisions.time_horizon,
|
time_horizon: decisions.time_horizon,
|
||||||
@ -99,6 +106,65 @@ function buildPayload() {
|
|||||||
|
|
||||||
const STORAGE_KEY = (id) => `ma_strategy_draft_${id}`
|
const STORAGE_KEY = (id) => `ma_strategy_draft_${id}`
|
||||||
|
|
||||||
|
function formatJobOption(j) {
|
||||||
|
const t = j.created_at
|
||||||
|
const tail = t ? String(t).replace('T', ' ').slice(0, 16) : ''
|
||||||
|
return tail ? `#${j.id} · ${j.keyword} · ${tail}` : `#${j.id} · ${j.keyword}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStrategyConfigFromJob(job) {
|
||||||
|
const stored =
|
||||||
|
job && typeof job.strategy_config === 'object' && job.strategy_config !== null
|
||||||
|
? job.strategy_config
|
||||||
|
: {}
|
||||||
|
const eff = { ...strategyDefaults.value, ...stored }
|
||||||
|
useLlm.value = !!eff.use_llm_default
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStrategyDefaults() {
|
||||||
|
try {
|
||||||
|
const r = await api(strategyConfigDefaultsUrl())
|
||||||
|
if (r.ok) {
|
||||||
|
strategyDefaults.value = await r.json()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStrategyPreferences() {
|
||||||
|
const id = selectedId.value
|
||||||
|
if (!id) return
|
||||||
|
strategySaveErr.value = ''
|
||||||
|
strategySaveLoading.value = true
|
||||||
|
try {
|
||||||
|
const r = await api(`/api/jobs/${id}/`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
|
strategy_config: { use_llm_default: useLlm.value },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const text = await r.text()
|
||||||
|
if (!r.ok) {
|
||||||
|
try {
|
||||||
|
const j = JSON.parse(text)
|
||||||
|
strategySaveErr.value = j.detail || text
|
||||||
|
} catch {
|
||||||
|
strategySaveErr.value = text || `HTTP ${r.status}`
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const updated = JSON.parse(text)
|
||||||
|
const idx = jobs.value.findIndex((x) => x.id === updated.id)
|
||||||
|
if (idx >= 0) jobs.value[idx] = updated
|
||||||
|
applyStrategyConfigFromJob(updated)
|
||||||
|
} catch (e) {
|
||||||
|
strategySaveErr.value = String(e)
|
||||||
|
} finally {
|
||||||
|
strategySaveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadList() {
|
async function loadList() {
|
||||||
try {
|
try {
|
||||||
await refreshJobs()
|
await refreshJobs()
|
||||||
@ -144,7 +210,10 @@ async function generateAndGoPreview() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadList)
|
onMounted(async () => {
|
||||||
|
await loadStrategyDefaults()
|
||||||
|
await loadList()
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.job,
|
() => route.query.job,
|
||||||
@ -163,6 +232,23 @@ watch(
|
|||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(selectedId, async () => {
|
||||||
|
strategySaveErr.value = ''
|
||||||
|
const id = selectedId.value
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
const r = await api(`/api/jobs/${id}/`)
|
||||||
|
if (r.ok) {
|
||||||
|
const j = await r.json()
|
||||||
|
const idx = jobs.value.findIndex((x) => x.id === j.id)
|
||||||
|
if (idx >= 0) jobs.value[idx] = j
|
||||||
|
applyStrategyConfigFromJob(j)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -170,17 +256,19 @@ watch(
|
|||||||
<section class="ma-card">
|
<section class="ma-card">
|
||||||
<h2>策略生成</h2>
|
<h2>策略生成</h2>
|
||||||
<p class="hint-top">
|
<p class="hint-top">
|
||||||
选择<strong>已成功</strong>任务,在下方填空与勾选。<strong>未勾选</strong>下方选项时,由系统规则生成策略底稿;<strong>勾选「使用大模型生成」</strong>后,由大模型在底稿与数据摘要基础上成稿(服务端需已配置并可用)。提交后跳转到
|
选择<strong>已成功</strong>任务,在下方填空与勾选。策略页的默认选项保存在本任务的<strong>策略配置</strong>中(与「分析报告生成」页的<strong>报告配置</strong>相互独立)。未勾选大模型时由规则生成策略底稿;勾选后由大模型在底稿与同任务数据摘要基础上成稿(需服务端已配置网关)。策略稿与宿主报告第九章的归纳<strong>默认对齐</strong>(无需在此勾选)。提交后跳转到
|
||||||
<RouterLink to="/jd/strategy-view">策略稿预览</RouterLink>
|
<RouterLink to="/jd/strategy-view">策略稿预览</RouterLink>
|
||||||
。数据与
|
。未填项在文稿中仍保留占位提示。
|
||||||
<RouterLink to="/jd/analysis-view">同任务分析产出</RouterLink>
|
|
||||||
一致。未填项在文稿中仍保留占位提示。
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar toolbar-col">
|
||||||
<label class="chk-inline">
|
<label class="chk-inline">
|
||||||
<input v-model="useLlm" type="checkbox" />
|
<input v-model="rulesOnlyThisRun" type="checkbox" />
|
||||||
使用大模型生成(服务端需已配置并可用)
|
本次仅生成规则稿(不做大模型全文润色,更快、不调用智能服务)
|
||||||
|
</label>
|
||||||
|
<label class="chk-inline">
|
||||||
|
<input v-model="useLlm" type="checkbox" :disabled="rulesOnlyThisRun" />
|
||||||
|
使用大模型生成(与上项互斥:勾选上一项时本项无效)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@ -188,9 +276,17 @@ watch(
|
|||||||
<select v-model="selectedId" class="job-select">
|
<select v-model="selectedId" class="job-select">
|
||||||
<option value="" disabled>请选择任务</option>
|
<option value="" disabled>请选择任务</option>
|
||||||
<option v-for="j in successJobs" :key="j.id" :value="String(j.id)">
|
<option v-for="j in successJobs" :key="j.id" :value="String(j.id)">
|
||||||
#{{ j.id }} · {{ j.keyword }} · {{ j.run_dir?.split(/[/\\]/).pop() || '' }}
|
{{ formatJobOption(j) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ma-btn ma-btn-secondary"
|
||||||
|
:disabled="!selectedId || strategySaveLoading"
|
||||||
|
@click="saveStrategyPreferences"
|
||||||
|
>
|
||||||
|
{{ strategySaveLoading ? '保存中…' : '保存策略偏好' }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="ma-btn ma-btn-primary"
|
class="ma-btn ma-btn-primary"
|
||||||
@ -200,13 +296,10 @@ watch(
|
|||||||
{{ strategyGeneratingThisTask ? '生成中…' : '生成并前往预览' }}
|
{{ strategyGeneratingThisTask ? '生成中…' : '生成并前往预览' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="strategySaveErr" class="ma-err">{{ strategySaveErr }}</p>
|
||||||
<p v-if="strategyGeneratingOtherTask" class="ma-warn-banner">
|
<p v-if="strategyGeneratingOtherTask" class="ma-warn-banner">
|
||||||
任务 #{{ strategyDraftPendingJobId }} 的策略稿正在生成中,请稍候再切换任务或重复提交。
|
任务 #{{ strategyDraftPendingJobId }} 的策略稿正在生成中,请稍候再切换任务或重复提交。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-if="selectedJob?.run_dir" class="run-dir-note ma-muted">
|
|
||||||
任务目录:<span class="run-dir-path">{{ selectedJob.run_dir }}</span>
|
|
||||||
</p>
|
|
||||||
<p v-if="err" class="ma-err">{{ err }}</p>
|
<p v-if="err" class="ma-err">{{ err }}</p>
|
||||||
<p v-if="!successJobs.length" class="ma-muted">暂无成功任务,请先在「搜索采集」跑通一条流水线。</p>
|
<p v-if="!successJobs.length" class="ma-muted">暂无成功任务,请先在「搜索采集」跑通一条流水线。</p>
|
||||||
|
|
||||||
@ -336,6 +429,10 @@ watch(
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
.toolbar-col {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
.sel-label {
|
.sel-label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -354,18 +451,6 @@ watch(
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.run-dir-note {
|
|
||||||
margin: 0.75rem 0 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.run-dir-path {
|
|
||||||
display: block;
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
word-break: break-all;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
.ma-muted {
|
.ma-muted {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user