效果优化
This commit is contained in:
1 parent
2402a84c71
commit
ab85e4df72
15 files changed
+548
-35
No files matched your search
@@ -220,6 +220,17 @@ INTRANET_SOURCE_URL_BASE + recUuid
|
||||
「查看原文」地址前缀写在 `src/core/config/index.ts` 的 `INTRANET_SOURCE_URL_BASE`。
|
||||
若内网原文详情页地址有变,只改这一个常量即可。
|
||||
|
||||
### 4.5 检索到的素材全是无关内容
|
||||
|
||||
**现象**:素材卡片内容与写作意图完全不沾边。
|
||||
|
||||
**排查**:检查后端日志里素材专家实际用的检索词。常见原因是模型把关键词提取
|
||||
提示词模板里的占位符(如「关键词1」「...」)当成真关键词去检索了。
|
||||
|
||||
`researcher.py` 的 `_clean_keywords()` 已对此做防御:剔除 `关键词\d*` / `keyword\d*` /
|
||||
`...` 等占位符,全部被滤掉时回退为写作意图本身。若仍出现,检查
|
||||
`researcher_prompts.py` 的 `EXTRACT_KEYWORDS` 提示词是否又引入了易被误抄的示例占位符。
|
||||
|
||||
---
|
||||
|
||||
## 5. 联调上线步骤
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function startWritingSession(req: AIWritingRequest): Promise<string
|
||||
word_count_target: req.wordCountTarget,
|
||||
model_name: req.modelName ?? '',
|
||||
keyword_count: req.keywordCount,
|
||||
writing_mode: req.writingMode,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`启动失败: ${resp.status}`);
|
||||
@@ -106,6 +107,17 @@ function normalizeEvent(parsed: Record<string, unknown>): Partial<ProgressEvent>
|
||||
// outline
|
||||
if (out.outline) out.outline = normalizeOutline(out.outline);
|
||||
|
||||
// blocked_sections(section_help await_user)
|
||||
if (Array.isArray(out.blocked_sections)) {
|
||||
out.blockedSections = out.blocked_sections.map((b: unknown) => {
|
||||
const o = b as Record<string, unknown>;
|
||||
return {
|
||||
sectionTitle: o.section_title ?? o.sectionTitle ?? '',
|
||||
reason: o.reason ?? '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// draft_ready: 后端发 "title"/"markdown",前端期望 draftTitle/draftMarkdown
|
||||
if ('markdown' in out && !out.draftMarkdown) out.draftMarkdown = out.markdown;
|
||||
if ('title' in out && !out.draftTitle) out.draftTitle = out.title;
|
||||
@@ -203,6 +215,10 @@ export async function resumeWriting(
|
||||
user_revision_notes: payload.userRevisionNotes ?? '',
|
||||
override_verdict: payload.overrideVerdict ?? false,
|
||||
selected_issue_indices: payload.selectedIssueIndices ?? null,
|
||||
section_decisions: (payload.sectionDecisions ?? []).map((d) => ({
|
||||
section_title: d.sectionTitle,
|
||||
decision: d.decision,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`提交干预失败: ${resp.status}`);
|
||||
|
||||
@@ -37,6 +37,7 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
|
||||
const [audience, setAudience] = useState('普通读者');
|
||||
const [wordCountInput, setWordCountInput] = useState('800');
|
||||
const [keywordCountInput, setKeywordCountInput] = useState('6');
|
||||
const [strictMode, setStrictMode] = useState(false);
|
||||
const [modelName, setModelName] = useState('');
|
||||
const [models, setModels] = useState<ModelOption[]>([]);
|
||||
|
||||
@@ -116,6 +117,7 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
|
||||
targetAudience: audience.trim() || '普通读者',
|
||||
wordCountTarget: wordCount,
|
||||
keywordCount,
|
||||
writingMode: strictMode ? 'strict' : 'loose',
|
||||
modelName: modelName || undefined,
|
||||
});
|
||||
}
|
||||
@@ -190,6 +192,23 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="strict-mode"
|
||||
checked={strictMode}
|
||||
onChange={(e) => setStrictMode(e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="mt-0.5 size-3.5 shrink-0 accent-primary cursor-pointer"
|
||||
/>
|
||||
<label htmlFor="strict-mode" className="cursor-pointer select-none">
|
||||
<span className="text-xs font-medium text-foreground/80">严格依据素材写作</span>
|
||||
<p className="text-[11px] text-muted-foreground/70 mt-0.5 leading-relaxed">
|
||||
勾选后作家只依据检索素材写作,素材不足以支撑某章节时会暂停询问你;不勾选则可结合通用知识补充论述。
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">目标读者</label>
|
||||
<input
|
||||
|
||||
@@ -34,6 +34,7 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
writing_outline: '作家规划大纲中…',
|
||||
awaiting_outline_confirm: '等待确认大纲',
|
||||
writing_draft: '作家写作中…',
|
||||
awaiting_section_help: '等待处理素材不足的章节',
|
||||
awaiting_draft_confirm: '等待确认草稿',
|
||||
reviewing: '编辑审核中…',
|
||||
awaiting_review_confirm: '等待处理编辑意见',
|
||||
@@ -428,6 +429,11 @@ export function AIWritingPanel() {
|
||||
<ReqRow label="检索关键词数量">
|
||||
<span className="text-muted-foreground">{state.request.keywordCount} 个</span>
|
||||
</ReqRow>
|
||||
<ReqRow label="写作模式">
|
||||
<span className="text-muted-foreground">
|
||||
{state.request.writingMode === 'strict' ? '严格依据素材' : '宽松(可结合通用知识)'}
|
||||
</span>
|
||||
</ReqRow>
|
||||
{state.request.modelName && (
|
||||
<ReqRow label="写作模型">
|
||||
<span className="text-muted-foreground">{state.request.modelName}</span>
|
||||
|
||||
@@ -58,6 +58,37 @@ function SectionThinking({ thinkingText, isDone }: { thinkingText: string; isDon
|
||||
);
|
||||
}
|
||||
|
||||
/** 章节正文:把 `### 要点` 行渲染成小标题,其余按段落显示(流式纯文本,非完整 markdown 解析) */
|
||||
function SectionBody({ text, showCursor }: { text: string; showCursor: boolean }) {
|
||||
const lines = text.split('\n');
|
||||
return (
|
||||
<div className="m-0">
|
||||
{lines.map((line, i) => {
|
||||
// 严格模式下作家声明素材不足的哨兵行,不展示给用户
|
||||
if (line.includes('[[SECTION_BLOCKED]]')) return null;
|
||||
const heading = /^[ \t]*#{1,6}[ \t]+/.exec(line);
|
||||
if (heading) {
|
||||
return (
|
||||
<p key={i} className="text-xs font-semibold text-foreground mt-2 mb-1">
|
||||
{line.slice(heading[0].length)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (!line.trim()) return null; // 跳过空行,避免大段空白
|
||||
return (
|
||||
<p
|
||||
key={i}
|
||||
className="text-xs leading-relaxed text-foreground/90 whitespace-pre-wrap m-0 mb-1"
|
||||
>
|
||||
{line}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
{showCursor && <TypewriterCursor />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DraftStreamingView({ title, sections }: DraftStreamingViewProps) {
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -91,11 +122,11 @@ export function DraftStreamingView({ title, sections }: DraftStreamingViewProps)
|
||||
{/* 该章节的思考过程(可折叠) */}
|
||||
<SectionThinking thinkingText={sec.thinkingText} isDone={sec.isDone} />
|
||||
|
||||
{/* 正文内容 */}
|
||||
<p className="text-xs leading-relaxed text-foreground/90 whitespace-pre-wrap m-0">
|
||||
{sec.text}
|
||||
{sec.sectionIndex === activeSectionIdx && !sec.isDone && <TypewriterCursor />}
|
||||
</p>
|
||||
{/* 正文内容(### 要点行渲染为小标题) */}
|
||||
<SectionBody
|
||||
text={sec.text}
|
||||
showCursor={sec.sectionIndex === activeSectionIdx && !sec.isDone}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@ import { useRef, useState } from 'react';
|
||||
import {
|
||||
PauseCircle, CheckCircle2, RotateCcw, Search,
|
||||
RefreshCw, PenLine, ArrowRight, ShieldAlert, ClipboardEdit, SendHorizonal,
|
||||
Trash2, BookOpen,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type {
|
||||
AIMaterial, AIOutline, ReviewResult, ReviewIssue,
|
||||
ProgressEvent, UserInterventionPayload,
|
||||
BlockedSection, SectionDecisionKind,
|
||||
} from '../../types/ai-writing';
|
||||
import { MaterialsCard } from './MaterialsCard';
|
||||
import { MaterialsToolbar } from './MaterialsToolbar';
|
||||
@@ -56,6 +59,16 @@ export function InterventionCard({
|
||||
);
|
||||
}
|
||||
|
||||
if (pausePoint === 'section_help') {
|
||||
return (
|
||||
<SectionHelpCard
|
||||
blockedSections={pauseEvent.blockedSections ?? []}
|
||||
onSubmit={onSubmit}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (pausePoint === 'review_confirm') {
|
||||
return (
|
||||
<ReviewConfirmCard
|
||||
@@ -547,3 +560,99 @@ function ReviewConfirmCard({ reviewResult, revisionCount, onSubmit, disabled }:
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 素材不足求助 ──────────────────────────────────────────────────────────────
|
||||
|
||||
interface SectionHelpCardProps {
|
||||
blockedSections: BlockedSection[];
|
||||
onSubmit: (payload: UserInterventionPayload) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DECISION_OPTIONS: { value: SectionDecisionKind; label: string; icon: typeof Search }[] = [
|
||||
{ value: 'supplement', label: '补充检索素材', icon: Search },
|
||||
{ value: 'loose', label: '按通用知识续写', icon: BookOpen },
|
||||
{ value: 'delete', label: '删除该章节', icon: Trash2 },
|
||||
];
|
||||
|
||||
function SectionHelpCard({ blockedSections, onSubmit, disabled }: SectionHelpCardProps) {
|
||||
const [decisions, setDecisions] = useState<Record<string, SectionDecisionKind>>(
|
||||
() => Object.fromEntries(blockedSections.map((b) => [b.sectionTitle, 'supplement'])),
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const hasSupplement = Object.values(decisions).some((d) => d === 'supplement');
|
||||
|
||||
async function handleSubmit() {
|
||||
setSubmitting(true);
|
||||
await onSubmit({
|
||||
action: 'section_help',
|
||||
sectionDecisions: blockedSections.map((b) => ({
|
||||
sectionTitle: b.sectionTitle,
|
||||
decision: decisions[b.sectionTitle] ?? 'supplement',
|
||||
})),
|
||||
});
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50/50 dark:border-amber-900/40 dark:bg-amber-950/20 space-y-3 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<ShieldAlert className="size-4 text-amber-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-xs font-medium">部分章节素材不足,需要你决定</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
严格依据素材模式下,以下 {blockedSections.length} 个章节缺少足够素材支撑写作。请为每个章节选择处理方式。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{blockedSections.map((b) => (
|
||||
<div key={b.sectionTitle} className="rounded-md border bg-background p-2.5 space-y-2">
|
||||
<div>
|
||||
<p className="text-xs font-medium">{b.sectionTitle}</p>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">{b.reason}</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{DECISION_OPTIONS.map((opt) => {
|
||||
const active = (decisions[b.sectionTitle] ?? 'supplement') === opt.value;
|
||||
const Icon = opt.icon;
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
disabled={disabled || submitting}
|
||||
onClick={() => setDecisions((p) => ({ ...p, [b.sectionTitle]: opt.value }))}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-1 rounded px-1.5 py-1 text-[11px] border transition-colors',
|
||||
active
|
||||
? 'border-primary bg-primary/10 text-primary font-medium'
|
||||
: 'border-input text-muted-foreground hover:bg-muted/50',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3 shrink-0" />
|
||||
{opt.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{hasSupplement && (
|
||||
<p className="text-[11px] text-amber-600 dark:text-amber-400">
|
||||
含「补充检索素材」的章节将触发重新检索并重新规划大纲,本轮其它选择不生效。
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button" size="sm" className="w-full h-8 text-xs gap-1.5"
|
||||
onClick={handleSubmit} disabled={disabled || submitting}
|
||||
>
|
||||
<CheckCircle2 className="size-3.5" />提交并继续
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -354,6 +354,11 @@ export function useAIWriting() {
|
||||
else nextStatus = 'revising'; // user_revise
|
||||
} else if (pausePoint === 'review_confirm') {
|
||||
nextStatus = payload.action === 'force_finalize' ? 'done' : 'revising'; // accept_review
|
||||
} else if (pausePoint === 'section_help') {
|
||||
// 含「补充检索」→ 回检索;否则(删章节 / 通用知识续写)→ 直接回写作
|
||||
nextStatus = payload.sectionDecisions?.some((d) => d.decision === 'supplement')
|
||||
? 'researching'
|
||||
: 'writing_draft';
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -25,6 +25,22 @@ export interface AIOutline {
|
||||
sections: OutlineSection[];
|
||||
}
|
||||
|
||||
// ── 素材不足求助 ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** 严格模式下素材不足、待用户决策的章节 */
|
||||
export interface BlockedSection {
|
||||
sectionTitle: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** 单个被阻塞章节的处理决策 */
|
||||
export type SectionDecisionKind = 'supplement' | 'delete' | 'loose';
|
||||
|
||||
export interface SectionDecision {
|
||||
sectionTitle: string;
|
||||
decision: SectionDecisionKind;
|
||||
}
|
||||
|
||||
// ── 编辑审核 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ReviewIssue {
|
||||
@@ -54,6 +70,7 @@ export type WritingStatus =
|
||||
| 'writing_outline'
|
||||
| 'awaiting_outline_confirm'
|
||||
| 'writing_draft'
|
||||
| 'awaiting_section_help'
|
||||
| 'awaiting_draft_confirm'
|
||||
| 'reviewing'
|
||||
| 'awaiting_review_confirm'
|
||||
@@ -109,8 +126,10 @@ export interface ProgressEvent {
|
||||
// review_ready
|
||||
reviewResult?: ReviewResult;
|
||||
// await_user
|
||||
pausePoint?: 'material_confirm' | 'outline_confirm' | 'draft_confirm' | 'review_confirm';
|
||||
pausePoint?: 'material_confirm' | 'outline_confirm' | 'draft_confirm' | 'review_confirm' | 'section_help';
|
||||
pauseContext?: Record<string, unknown>;
|
||||
// section_help:素材不足、待用户决策的章节
|
||||
blockedSections?: BlockedSection[];
|
||||
}
|
||||
|
||||
// ── 用户干预提交体 ────────────────────────────────────────────────────────────
|
||||
@@ -125,6 +144,8 @@ export interface UserInterventionPayload {
|
||||
overrideVerdict?: boolean;
|
||||
// 用户勾选的审核意见索引(基于 reviewResult.revisionNotes),未传则默认全选
|
||||
selectedIssueIndices?: number[];
|
||||
// section_help:每个被阻塞章节的处理决策
|
||||
sectionDecisions?: SectionDecision[];
|
||||
}
|
||||
|
||||
// ── 写作请求 ──────────────────────────────────────────────────────────────────
|
||||
@@ -137,6 +158,8 @@ export interface AIWritingRequest {
|
||||
modelName?: string;
|
||||
/** 素材检索关键词数量(1~10,默认 6) */
|
||||
keywordCount: number;
|
||||
/** 写作模式:strict=严格依据素材 / loose=可结合通用知识(默认 loose) */
|
||||
writingMode: 'strict' | 'loose';
|
||||
}
|
||||
|
||||
// ── 流式审核条目 ──────────────────────────────────────────────────────────────
|
||||
@@ -175,7 +198,7 @@ export interface StreamingDraftSection {
|
||||
// ── 已完成干预记录 ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CompletedIntervention {
|
||||
pausePoint: 'material_confirm' | 'outline_confirm' | 'draft_confirm' | 'review_confirm';
|
||||
pausePoint: 'material_confirm' | 'outline_confirm' | 'draft_confirm' | 'review_confirm' | 'section_help';
|
||||
action: string;
|
||||
// material_confirm
|
||||
materials?: AIMaterial[];
|
||||
|
||||
Reference in new issue
Block a user