fix(content-gen): strip URLs and site-list sections from generated content
Forbid URLs/domains/markdown links across prompt rules (runtime, platform templates, outline/imitation/title flows) and rename "官网" placeholder to "官网信息" with explicit instructions that links are background only. Add sanitizeGeneratedArticleMarkdown to strip URLs, HTML images, and "网站列表/官网列表" labels from LLM output, wired into article, prompt-rule, template, and outline generation. Drop the site_list outline section seed and filter blocked outline keys in the wizard so stored drafts cannot resurrect it.
This commit is contained in:
@@ -384,7 +384,7 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
|
||||
return
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(result.Content)
|
||||
content := sanitizeGeneratedArticleMarkdown(result.Content)
|
||||
if content == "" {
|
||||
s.failGeneration(ctx, job, "llm_generate", fmt.Errorf("模型返回空内容"))
|
||||
return
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
generatedArticleSchemeURLPattern = `https?://[^\s<>"',。;、?!\]))}]+`
|
||||
generatedArticleDomainPattern = `(?:www\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\.(?:com|cn|net|org|io|ai|co|cc|top|xyz|shop|site|online|tech|app|gov|edu|info|biz|me|tv)(?:/[^\s<>"',。;、?!\]))}]*)?`
|
||||
generatedArticleURLPattern = `(?:` + generatedArticleSchemeURLPattern + `|` + generatedArticleDomainPattern + `)`
|
||||
)
|
||||
|
||||
var (
|
||||
generatedArticleMarkdownLinkPattern = regexp.MustCompile(`!?\[([^\]\n]*)\]\(([^)\s]+)\)`)
|
||||
generatedArticleHTMLImageParagraph = regexp.MustCompile(`(?is)<p\b[^>]*>\s*<img\b[^>]*\bsrc\s*=\s*["']?` + generatedArticleURLPattern + `["']?[^>]*>\s*</p>`)
|
||||
generatedArticleHTMLImagePattern = regexp.MustCompile(`(?is)<img\b[^>]*\bsrc\s*=\s*["']?` + generatedArticleURLPattern + `["']?[^>]*>`)
|
||||
generatedArticleURLLabelPattern = regexp.MustCompile(`(?i)(?:官网|官方网站|网站|网址|链接|URL|website|official website)\s*[::]?\s*` + generatedArticleURLPattern)
|
||||
generatedArticleBareURLPattern = regexp.MustCompile(`(?i)\b` + generatedArticleURLPattern)
|
||||
generatedArticleDanglingURLLabelPattern = regexp.MustCompile(`(?i)(?:官网|官方网站|网站|网址|链接|URL|website|official website)\s*[::]?\s*$`)
|
||||
generatedArticleBlankLinePattern = regexp.MustCompile(`\n{3,}`)
|
||||
generatedArticlePunctuationGapPattern = regexp.MustCompile(`\s+([,。;、?!,.;!?])`)
|
||||
generatedArticleEmptyPunctuationPattern = regexp.MustCompile(`[::]\s*([,。;、?!,.;!?])`)
|
||||
generatedArticleRepeatedSpacePattern = regexp.MustCompile(`[ \t]{2,}`)
|
||||
)
|
||||
|
||||
func sanitizeGeneratedArticleMarkdown(markdown string) string {
|
||||
text := strings.TrimSpace(markdown)
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
text = generatedArticleMarkdownLinkPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||||
parts := generatedArticleMarkdownLinkPattern.FindStringSubmatch(match)
|
||||
if len(parts) != 3 || !looksLikeGeneratedArticleURL(parts[2]) {
|
||||
return match
|
||||
}
|
||||
if strings.HasPrefix(match, "![") {
|
||||
return ""
|
||||
}
|
||||
label := strings.TrimSpace(parts[1])
|
||||
if label == "" || looksLikeGeneratedArticleURL(label) {
|
||||
return ""
|
||||
}
|
||||
return label
|
||||
})
|
||||
|
||||
text = generatedArticleHTMLImageParagraph.ReplaceAllString(text, "")
|
||||
text = generatedArticleHTMLImagePattern.ReplaceAllString(text, "")
|
||||
text = generatedArticleURLLabelPattern.ReplaceAllString(text, "")
|
||||
text = generatedArticleBareURLPattern.ReplaceAllString(text, "")
|
||||
text = cleanGeneratedArticleURLArtifacts(text)
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func sanitizeGeneratedOutlineText(text string) string {
|
||||
text = sanitizeGeneratedArticleMarkdown(text)
|
||||
replacer := strings.NewReplacer(
|
||||
"网站列表", "品牌信息整理",
|
||||
"官网列表", "品牌信息整理",
|
||||
"网址列表", "品牌信息整理",
|
||||
"官方网站", "公开信息",
|
||||
"官方网址", "公开信息",
|
||||
)
|
||||
return strings.TrimSpace(replacer.Replace(text))
|
||||
}
|
||||
|
||||
func looksLikeGeneratedArticleURL(text string) bool {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
return generatedArticleBareURLPattern.MatchString(text)
|
||||
}
|
||||
|
||||
func cleanGeneratedArticleURLArtifacts(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
cleaned := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
originalLine := line
|
||||
line = strings.TrimRight(line, " \t")
|
||||
line = generatedArticleDanglingURLLabelPattern.ReplaceAllString(line, "")
|
||||
line = strings.ReplaceAll(line, "网站列表", "品牌信息整理")
|
||||
line = strings.ReplaceAll(line, "官网列表", "品牌信息整理")
|
||||
line = strings.ReplaceAll(line, "网址列表", "品牌信息整理")
|
||||
line = generatedArticleEmptyPunctuationPattern.ReplaceAllString(line, "$1")
|
||||
line = generatedArticlePunctuationGapPattern.ReplaceAllString(line, "$1")
|
||||
line = generatedArticleRepeatedSpacePattern.ReplaceAllString(line, " ")
|
||||
if line != originalLine {
|
||||
line = strings.TrimRight(line, " \t::,,;;")
|
||||
}
|
||||
if strings.TrimSpace(line) == "" {
|
||||
if strings.TrimSpace(originalLine) == "" {
|
||||
cleaned = append(cleaned, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isGeneratedArticleURLOnlyLine(line) {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, line)
|
||||
}
|
||||
return generatedArticleBlankLinePattern.ReplaceAllString(strings.Join(cleaned, "\n"), "\n\n")
|
||||
}
|
||||
|
||||
func isGeneratedArticleURLOnlyLine(line string) bool {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
trimmed = strings.TrimLeft(trimmed, "#-*+> \t")
|
||||
trimmed = strings.Trim(trimmed, " \t::,,;;。")
|
||||
if trimmed == "" {
|
||||
return true
|
||||
}
|
||||
switch strings.ToLower(trimmed) {
|
||||
case "官网", "官方网站", "官方网址", "网站", "网址", "链接", "url", "website", "official website":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeGeneratedArticleMarkdownRemovesURLs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
markdown := strings.Join([]string{
|
||||
"# 标题",
|
||||
"",
|
||||
"品牌官网:https://www.defnit.cn,可查询产品参数。",
|
||||
"也可以查看 www.example.com/path 了解更多。",
|
||||
"参考 [官网](https://defnit.cn/contact) 的信息整理。",
|
||||
}, "\n")
|
||||
|
||||
got := sanitizeGeneratedArticleMarkdown(markdown)
|
||||
|
||||
for _, unexpected := range []string{"https://", "www.", "defnit.cn", "example.com", "[官网]("} {
|
||||
if strings.Contains(got, unexpected) {
|
||||
t.Fatalf("sanitizeGeneratedArticleMarkdown() = %q, contains %q", got, unexpected)
|
||||
}
|
||||
}
|
||||
for _, expected := range []string{"# 标题", "可查询产品参数。", "也可以查看 了解更多。", "参考 官网 的信息整理。"} {
|
||||
if !strings.Contains(got, expected) {
|
||||
t.Fatalf("sanitizeGeneratedArticleMarkdown() = %q, want %q", got, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeneratedArticleMarkdownRemovesHTMLImageURLs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
markdown := `<p class="article-editor-image"><img src="https://example.com/cover.webp" data-asset-id="88" /></p>`
|
||||
|
||||
got := sanitizeGeneratedArticleMarkdown(markdown)
|
||||
|
||||
if strings.TrimSpace(got) != "" {
|
||||
t.Fatalf("sanitizeGeneratedArticleMarkdown() = %q, want generated image URL removed", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeneratedArticleMarkdownKeepsNormalPunctuation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
markdown := strings.Join([]string{
|
||||
"## 选择建议:",
|
||||
"下面从几个维度看。",
|
||||
}, "\n")
|
||||
|
||||
got := sanitizeGeneratedArticleMarkdown(markdown)
|
||||
|
||||
if got != markdown {
|
||||
t.Fatalf("sanitizeGeneratedArticleMarkdown() = %q, want %q", got, markdown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOutlineNodesRemovesURLsAndWebsiteListLabel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
nodes := []OutlineNode{
|
||||
{
|
||||
Outline: "网站列表",
|
||||
Children: []OutlineNode{
|
||||
{Outline: "丹福尼门锁官方网站:www.defnit.cn,可查询产品参数"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := normalizeOutlineNodes(nodes, 2)
|
||||
if len(got) != 1 || got[0].Outline != "品牌信息整理" {
|
||||
t.Fatalf("normalizeOutlineNodes() = %#v, want top outline sanitized", got)
|
||||
}
|
||||
if len(got[0].Children) != 1 {
|
||||
t.Fatalf("normalizeOutlineNodes() children = %#v, want one child", got[0].Children)
|
||||
}
|
||||
child := got[0].Children[0].Outline
|
||||
if strings.Contains(child, "www.") || strings.Contains(child, "defnit.cn") || strings.Contains(child, "官方网站") {
|
||||
t.Fatalf("normalizeOutlineNodes() child = %q, still contains website text", child)
|
||||
}
|
||||
if !strings.Contains(child, "丹福尼门锁") || !strings.Contains(child, "可查询产品参数") {
|
||||
t.Fatalf("normalizeOutlineNodes() child = %q, lost useful context", child)
|
||||
}
|
||||
}
|
||||
@@ -559,7 +559,7 @@ func (s *PromptRuleGenerationService) executeGeneration(ctx context.Context, job
|
||||
return
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(result.Content)
|
||||
content := sanitizeGeneratedArticleMarkdown(result.Content)
|
||||
if content == "" {
|
||||
s.failGeneration(ctx, job, title, "llm_generate", fmt.Errorf("模型返回空内容"))
|
||||
return
|
||||
|
||||
@@ -1222,7 +1222,7 @@ func normalizeOutlineNodes(nodes []OutlineNode, depth int) []OutlineNode {
|
||||
|
||||
items := make([]OutlineNode, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
outline := strings.TrimSpace(node.Outline)
|
||||
outline := sanitizeGeneratedOutlineText(node.Outline)
|
||||
if outline == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ func buildPromptContext(params map[string]interface{}) string {
|
||||
return
|
||||
}
|
||||
switch key {
|
||||
case "knowledge_group_ids", "knowledge_groups", "knowledge_context":
|
||||
case "knowledge_group_ids", "knowledge_groups", "knowledge_context", "official_website", "website", "input_params":
|
||||
return
|
||||
}
|
||||
if key == "outline_sections" && hasStructuredOutline(params["article_outline"]) {
|
||||
|
||||
@@ -520,7 +520,7 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
|
||||
return
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(result.Content)
|
||||
content := sanitizeGeneratedArticleMarkdown(result.Content)
|
||||
if content == "" {
|
||||
s.failGeneration(ctx, job, title, "llm_generate", fmt.Errorf("模型返回空内容"))
|
||||
return
|
||||
|
||||
@@ -107,7 +107,6 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
"preview_caption": "预览会根据已选标题和结构实时更新。",
|
||||
"sections": [
|
||||
{ "key": "intro", "label": "引言", "default": true },
|
||||
{ "key": "site_list", "label": "网站列表", "default": true },
|
||||
{ "key": "key_points", "label": "文章关键要点", "default": true },
|
||||
{ "key": "what_is_question", "label_template": "围绕{{brand_question}}展开", "default": false },
|
||||
{ "key": "features", "label": "特点", "default": false },
|
||||
|
||||
Reference in New Issue
Block a user