AI书签管理工具开发全记录(八):Ai创建书签功能实现
文章目录
- AI书签管理工具开发全记录(八):AI智能创建书签功能深度解析
- 前言 📝
- 1. AI功能设计思路 🧠
- 1.1 传统书签创建的痛点
- 1.2 AI解决方案设计
- 2. 后端API实现 ⚙️
- 2.1 新增url相关工具方法
- 2.1 创建后端api
- 2.2 创建createAIBookmark
- 2.3 初始化ai模型
- 2.4 编写promot获取建议信息
- 2.5 元数据处理
- 2.6 将信息返回给前端
- 3. 前端实现 💻
- 3.1 增加api实现
- 3.2 调用ai创建书签方法
- 3.3 创建书签组件
- 4. AI模型集成说明 🤖
- 4.1 技术选型
- 4. 效果展示 🤖
- 总结 📚
AI书签管理工具开发全记录(八):AI智能创建书签功能深度解析
前言 📝
在前一篇文章中,我们完成了书签和分类管理的基础功能实现。本文将聚焦于项目中特色的功能之一,AI智能创建书签,详细解析如何利用AI技术实现智能化的书签创建流程,大幅提升用户操作效率。
1. AI功能设计思路 🧠
1.1 传统书签创建的痛点
在传统书签管理工具中,用户需要:
- 手动输入标题
- 复制粘贴URL
- 填写描述信息
- 选择或创建分类
整个过程繁琐耗时,有时为了方便,除了url,其它就应付了事,造成后期维护不便。
1.2 AI解决方案设计
我们的AI智能创建功能将实现:
- 自动提取元数据:从URL获取网页标题、描述等基础信息
- 智能分类建议:基于网页内容自动推荐合适分类
- 一键填充:自动填充表单字段
- 分类联动:支持直接创建AI建议的分类
完整交互逻辑:
2. 后端API实现 ⚙️
为了后续和方便多个大模型进行对接,放弃了轻量级的http形式,使用eino
框架,此处我们先对接openai
模型,提供BaseUrl配置,任何和openai兼容的api都是可以使用的。
2.1 新增url相关工具方法
//internal/utils/url.go
package utilsimport ("bytes""fmt""io""net/http""net/url""regexp""strings""github.com/PuerkitoBio/goquery"
)func IsValidURL(urlStr string) bool {// 检查空字符串if urlStr == "" {return false}// 尝试解析URLu, err := url.ParseRequestURI(urlStr)if err != nil {return false}// 检查Schemeif u.Scheme != "http" && u.Scheme != "https" {return false}// 检查Hostif u.Host == "" {return false}// 简单验证域名格式domainRegex := `^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$`if matched, _ := regexp.MatchString(domainRegex, u.Host); !matched {return false}return true
}type WebpageInfo struct {URL string `json:"url"`Title string `json:"title"`HTML string `json:"html"`Text string `json:"text"`
}func GetWebpageInfo(url string) (*WebpageInfo, error) {// Create a new HTTP clientclient := &http.Client{}// Create a new requestreq, err := http.NewRequest("GET", url, nil)if err != nil {return nil, err}// Set a custom User-Agent headerreq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")// Make the requestresp, err := client.Do(req)if err != nil {return nil, err}defer resp.Body.Close()// Check Content-Type is HTMLcontentType := resp.Header.Get("Content-Type")if !strings.Contains(contentType, "text/html") {return nil, fmt.Errorf("URL does not return HTML content")}// Read response bodybodyBytes, err := io.ReadAll(resp.Body)if err != nil {return nil, err}// Parse HTML documentdoc, err := goquery.NewDocumentFromReader(bytes.NewReader(bodyBytes))if err != nil {return nil, err}// Extract <title> tag contenttitle := doc.Find("title").Text()if title == "" {title = "Untitled"}// Clean up titletitle = strings.TrimSpace(title)title = strings.Join(strings.Fields(title), " ")// Get first 2000 characters of HTMLhtmlContent := string(bodyBytes)if len(htmlContent) > 2000 {htmlContent = htmlContent[:2000] + "..."}// Extract plain text (with HTML tags removed)textContent := doc.Text()// Clean up text contenttextContent = strings.TrimSpace(textContent)textContent = strings.Join(strings.Fields(textContent), " ")// Limit text content length if neededif len(textContent) > 2000 {textContent = textContent[:2000] + "..."}return &WebpageInfo{URL: url,Title: title,HTML: htmlContent,Text: textContent,}, nil
}func TruncateURLForName(urlStr string) string {u, err := url.Parse(urlStr)if err != nil {return urlStr}// 使用域名作为名称host := u.Hostname()if strings.HasPrefix(host, "www.") {host = host[4:]}return host
}
2.1 创建后端api
//internal/api/api.gobookmark := api.Group("/bookmarks")
{...bookmark.POST("/ai", server.createAIBookmark)
}
2.2 创建createAIBookmark
//internal/api/api.go// CreateAIBookmark godoc
// @Summary 使用AI创建书签
// @Description 根据URL使用AI自动生成书签信息
// @Tags bookmarks
// @Accept json
// @Produce json
// @Param request body models.AIBookmarkRequest true "URL信息"
// @Success 200 {object} models.AIBookmarkResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/bookmarks/ai [post]
func (s *Server) createAIBookmark(c *gin.Context) {// 返回网页信息和AI建议c.JSON(200, nil)
}
2.3 初始化ai模型
//internal/api/api.go// 初始化AI模型
ctx := context.Background()
config := common.AppConfigModel
maxTokens := config.AI.MaxTokens
temperature := float32(config.AI.Temperature)
model, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{BaseURL: config.AI.BaseURL,APIKey: config.AI.PIKey,Timeout: time.Duration(config.AI.Timeout) * time.Second,Model: config.AI.Model,MaxTokens: &maxTokens,Temperature: &temperature,
})
if err != nil {c.JSON(500, gin.H{"error": "AI模型初始化失败"})return
}
2.4 编写promot获取建议信息
//internal/api/api.go// 准备消息
messages := []*schema.Message{{Role: "system",Content: `你是一个专业的书签助手,负责分析网页内容并生成合适的书签信息。
请遵循以下规则:
1. 分类选择:
- 分类名称应该简洁明了,尽量在10个字符以内,或者2-4个汉字
- 避免使用测试、测试1等明显不合适的分类
- 现有分类中没有合适的分类,优先创建新分类2. 名称生成:
- 使用网页标题作为基础,但要去除网站名称、分隔符等无关信息
- 保持简洁,通常不超过10个汉字
- 如果标题不够清晰,可以根据内容补充关键信息3. 描述生成:
- 总结网页的核心内容和价值
- 突出最重要的2-3个要点
- 使用简洁的语言,不超过50个汉字
- 避免使用"这是一个..."等冗余表达
- 使用markdown格式请以JSON格式返回结果,格式如下:
{
"category": "分类名称",
"name": "书签名称",
"description": "书签描述"
}`,},{Role: "user",Content: fmt.Sprintf(`请分析以下网页内容并生成书签信息:现有分类列表:%v网页信息:
标题:%s
内容:%s请确保:
1. 避免使用测试、测试1等明显不合适的分类
2. 如果已经有适合的分类,不要创建重复的分类,例如已经有ai,就不要创建人工智能等分类
3. 生成的名称要简洁明了
4. 描述要突出网页的核心价值`, categoryNames, webpageInfo.Title, webpageInfo.Text),},
}// 生成回复
response, err := model.Generate(ctx, messages)
if err != nil {c.JSON(500, gin.H{"error": "AI生成失败"})return
}
2.5 元数据处理
//internal/api/api.go// 使用正则表达式去除 ```json 和 ```
re := regexp.MustCompile("(?s)^\\s*```json\\s*(.*?)\\s*```\\s*$")
matches := re.FindStringSubmatch(response.Content)
if len(matches) < 2 {c.JSON(500, gin.H{"error": "无法提取JSON内容"})return
}
cleanedJSON := matches[1]var suggestion models.BookmarkSuggestion
err = json.Unmarshal([]byte(cleanedJSON), &suggestion)
if err != nil {c.JSON(500, gin.H{"error": "解析AI响应失败"})return
}
2.6 将信息返回给前端
//internal/api/api.go// 返回AI建议
aiResp := models.AIBookmarkResponse{Suggestion: suggestion,Webpage: models.WebpageInfo{Title: webpageInfo.Title, URL: webpageInfo.URL},
}// 返回网页信息和AI建议
c.JSON(200, aiResp)
3. 前端实现 💻
3.1 增加api实现
3.2 调用ai创建书签方法
//web/src/api/bookmark/index.js// 使用AI创建书签
export function createAIBookmark(data) {return request({url: '/api/bookmarks/ai',method: 'post',data})
}
3.3 创建书签组件
创建AICreateDialog
,编写ai创建书签组件
<!--web/src/views/bookmark/components/AICreateDialog.vue-->
<template><el-dialogv-model="dialogVisible"title="AI创建书签"width="600px"><el-formref="formRef":model="form":rules="rules"label-width="80px"><el-form-item label="URL" prop="url"><el-input v-model="form.url" placeholder="请输入网页URL"><template #append><el-button @click="handleFetchMetadata" :loading="fetchingMetadata">获取信息</el-button></template></el-input></el-form-item><template v-if="form.metadata"><el-divider>网页信息</el-divider><el-descriptions :column="1" border><el-descriptions-item label="标题">{{ form.metadata.webpage.title }}</el-descriptions-item><el-descriptions-item label="URL">{{ form.metadata.webpage.url }}</el-descriptions-item></el-descriptions><el-divider>AI建议</el-divider><el-form-item label="分类" prop="category_id"><CategorySelectv-model="form.category_id":category-options="categoryOptions":ai-suggestion="form.metadata.suggestion.category"@update:category-options="categoryOptions = $event"/></el-form-item><el-form-item label="标题" prop="title"><el-input v-model="form.title" placeholder="请输入书签标题" /></el-form-item><el-form-item label="描述" prop="description"><el-inputv-model="form.description"type="textarea"placeholder="请输入书签描述"/></el-form-item></template></el-form><template #footer><span class="dialog-footer"><el-button @click="handleCancel">取消</el-button><el-button type="primary" @click="handleSubmit" :disabled="!form.metadata">确定</el-button></span></template></el-dialog>
</template><script setup>
import { ref, defineProps, defineEmits, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { createAIBookmark } from '/@/api/bookmark'
import CategorySelect from './CategorySelect.vue'const props = defineProps({modelValue: {type: Boolean,required: true},categoryOptions: {type: Array,required: true}
})const emit = defineEmits(['update:modelValue', 'success', 'update:categoryOptions'])// 对话框可见性
const dialogVisible = ref(props.modelValue)// 监听modelValue变化
watch(() => props.modelValue, (val) => {dialogVisible.value = val
})// 监听dialogVisible变化
watch(() => dialogVisible.value, (val) => {emit('update:modelValue', val)
})// 表单相关
const formRef = ref(null)
const fetchingMetadata = ref(false)
const form = ref({url: '',title: '',description: '',category_id: undefined,metadata: null
})// 移除不再需要的变量和函数
const categoryDialogVisible = ref(false)
const categoryForm = ref({name: '',description: ''
})// URL验证函数
const validateUrl = (rule, value, callback) => {...
}// 表单验证规则
const rules = {...
}// 获取网页元数据
const handleFetchMetadata = async () => {...
}// 处理提交
const handleSubmit = async () => {//...
}// 处理取消
const handleCancel = () => {...
}
</script><style scoped>
...
</style>
4. AI模型集成说明 🤖
4.1 技术选型
我们采用了eino
框架,很方便对接多种ai模型
根据需求可以采取不容策略。
- 本地模型:使用
ollama
等可以方便运行多种本地ai模型,例如qwen3系列
- 优点:数据隐私性好
- 缺点:需要较强的服务器资源
- 第三方API:如OpenAI、Google AI等
- 优点:开发简单,效果较好
- 缺点:有API调用成本
如果对隐私没有那么高需求,可以试试chatglm
的GLM-4-Flash-250414
模型。开发阶段采用了该模型,对于这种简单需求基本够用,最重要的是免费。
如果对隐私要求极高,可以试试ollama
,目前对推理模型没有做适配,需要修改代码。
4. 效果展示 🤖
点击ai创建书签,输入url
获取信息
对分类不满意,现存的分类也没有合适的,可以点击新建分类
可以选择新建的分类
总结 📚
本文深入实现了AI智能创建书签功能,主要包括:
- 智能化流程:简化创建书签步骤
- 精准分析:结合元数据提取和AI内容理解
- 无缝体验:分类建议与创建的联动设计