AI书签管理工具开发全记录(八):Ai创建书签功能实现

news2025/6/3 10:30:58

文章目录

  • 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 传统书签创建的痛点

在传统书签管理工具中,用户需要:

  1. 手动输入标题
  2. 复制粘贴URL
  3. 填写描述信息
  4. 选择或创建分类
    整个过程繁琐耗时,有时为了方便,除了url,其它就应付了事,造成后期维护不便。

1.2 AI解决方案设计

我们的AI智能创建功能将实现:

  1. ​自动提取元数据​​:从URL获取网页标题、描述等基础信息
  2. ​智能分类建议​​:基于网页内容自动推荐合适分类
  3. ​一键填充​​:自动填充表单字段
  4. ​分类联动​​:支持直接创建AI建议的分类

完整交互逻辑:
image.png

2. 后端API实现 ⚙️

为了后续和方便多个大模型进行对接,放弃了轻量级的http形式,使用eino框架,此处我们先对接openai模型,提供BaseUrl配置,任何和openai兼容的api都是可以使用的。

2.1 新增url相关工具方法

//internal/utils/url.go
package utils

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

func IsValidURL(urlStr string) bool {
	// 检查空字符串
	if urlStr == "" {
		return false
	}

	// 尝试解析URL
	u, err := url.ParseRequestURI(urlStr)
	if err != nil {
		return false
	}

	// 检查Scheme
	if u.Scheme != "http" && u.Scheme != "https" {
		return false
	}

	// 检查Host
	if 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 client
	client := &http.Client{}

	// Create a new request
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	// Set a custom User-Agent header
	req.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 request
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	// Check Content-Type is HTML
	contentType := resp.Header.Get("Content-Type")
	if !strings.Contains(contentType, "text/html") {
		return nil, fmt.Errorf("URL does not return HTML content")
	}

	// Read response body
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	// Parse HTML document
	doc, err := goquery.NewDocumentFromReader(bytes.NewReader(bodyBytes))
	if err != nil {
		return nil, err
	}

	// Extract <title> tag content
	title := doc.Find("title").Text()
	if title == "" {
		title = "Untitled"
	}

	// Clean up title
	title = strings.TrimSpace(title)
	title = strings.Join(strings.Fields(title), " ")

	// Get first 2000 characters of HTML
	htmlContent := string(bodyBytes)
	if len(htmlContent) > 2000 {
		htmlContent = htmlContent[:2000] + "..."
	}

	// Extract plain text (with HTML tags removed)
	textContent := doc.Text()
	// Clean up text content
	textContent = strings.TrimSpace(textContent)
	textContent = strings.Join(strings.Fields(textContent), " ")
	// Limit text content length if needed
	if 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.go

bookmark := 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-dialog
    v-model="dialogVisible"
    title="AI创建书签"
    width="600px"
  >
    <el-form
      ref="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">
          <CategorySelect
            v-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-input
            v-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模型
根据需求可以采取不容策略。

  1. ​本地模型​​:使用ollama等可以方便运行多种本地ai模型,例如qwen3系列
    • 优点:数据隐私性好
    • 缺点:需要较强的服务器资源
  2. ​第三方API​​:如OpenAI、Google AI等
    • 优点:开发简单,效果较好
    • 缺点:有API调用成本

如果对隐私没有那么高需求,可以试试chatglmGLM-4-Flash-250414模型。开发阶段采用了该模型,对于这种简单需求基本够用,最重要的是免费。
如果对隐私要求极高,可以试试ollama,目前对推理模型没有做适配,需要修改代码。

4. 效果展示 🤖

点击ai创建书签,输入url
image.png

获取信息
image.png

对分类不满意,现存的分类也没有合适的,可以点击新建分类
image.png

可以选择新建的分类
image.png

总结 📚

本文深入实现了AI智能创建书签功能,主要包括:

  1. ​智能化流程​​:简化创建书签步骤
  2. ​精准分析​​:结合元数据提取和AI内容理解
  3. ​无缝体验​​:分类建议与创建的联动设计

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2394765.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

X-plore v4.43.05 强大的安卓文件管理器-MOD解锁高级版 手机平板/电视TV通用

X-plore v4.43.05 强大的安卓文件管理器-MOD解锁高级版 手机平板/电视TV通用 应用简介&#xff1a; X-plore 是一款强大的安卓端文件管理器&#xff0c;它可以在电视或者手机上管理大量媒体文件、应用程序。…

使用多Agent进行海报生成的技术方案及评估套件-P2P、paper2poster

最近字节、滑铁卢大学相关团队同时放出了他们使用Agent进行海报生成的技术方案&#xff0c;P2P和Paper2Poster&#xff0c;传统方案如类似ppt生成等思路&#xff0c;基本上采用固定的模版&#xff0c;提取相关的关键元素进行模版填充&#xff0c;因此&#xff0c;海报生成的质量…

Redis--缓存工具封装

经过前面的学习&#xff0c;发现缓存中的问题&#xff0c;无论是缓存穿透&#xff0c;缓存雪崩&#xff0c;还是缓存击穿&#xff0c;这些问题的解决方案业务代码逻辑都很复杂&#xff0c;我们也不应该每次都来重写这些逻辑&#xff0c;我们可以将其封装成工具。而在封装的时候…

python:在 PyMOL 中如何查看和使用内置示例文件?

参阅&#xff1a;开源版PyMol安装保姆级教程 百度网盘下载 提取码&#xff1a;csub pip show pymol 简介: PyMOL是一个Python增强的分子图形工具。它擅长蛋白质、小分子、密度、表面和轨迹的3D可视化。它还包括分子编辑、射线追踪和动画。 可视化示例‌&#xff1a;打开 PyM…

SpringCloud——Docker

1.命令解读 docker run -d 解释&#xff1a;创建并运行一个容器&#xff0c;-d则是让容器以后台进程运行 --name mysql 解释&#xff1a; 给容器起个名字叫mysql -p 3306:3306 解释&#xff1a;-p 宿主机端口:容器内端口&#xff0c;设置端口映射 注意&#xff1a; 1、…

机器学习:欠拟合、过拟合、正则化

本文目录&#xff1a; 一、欠拟合二、过拟合三、拟合问题原因及解决办法四、正则化&#xff1a;尽量减少高次幂特征的影响&#xff08;一&#xff09;L1正则化&#xff08;二&#xff09;L2正则化&#xff08;三&#xff09;L1正则化与L2正则化的对比 五、正好拟合代码&#xf…

运用集合知识做斗地主案例

方法中可变参数 一种特殊形参&#xff0c;定义在方法&#xff0c;构造器的形参列表里&#xff0c;格式&#xff1a;数据类型...参数名称&#xff1b; 可变参数的特点和好处 特点&#xff1a;可以不传数据给它&#xff1b;可以传一个或者同时传多个数据给它&#xff1b;也可以…

《HelloGitHub》第 110 期

兴趣是最好的老师&#xff0c;HelloGitHub 让你对开源感兴趣&#xff01; 简介 HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。 github.com/521xueweihan/HelloGitHub 这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等&#xff0c;涵盖多种编程语言 Python、…

使用 Shell 脚本实现 Spring Boot 项目自动化部署到 Docker(Ubuntu 服务器)

使用 Shell 脚本实现 Spring Boot 项目自动化部署到 Docker&#xff08;Ubuntu 服务器&#xff09; 在日常项目开发中&#xff0c;我们经常会将 Spring Boot 项目打包并部署到服务器上的 Docker 环境中。为了提升效率、减少重复操作&#xff0c;我们可以通过 Shell 脚本实现自动…

day023-网络基础与OSI七层模型

文章目录 1. 网络基础知识点1.1 网络中的单位1.2 查看实时网速&#xff1a;iftop1.3 交换机、路由器 2. 路由表2.1 查看路由表的命令2.2 路由追踪命令 3. 通用网站网络架构4. 局域网上网原理-NAT5. 虚拟机上网原理6. 虚拟机的网络模式6.1 NAT模式6.2 桥接模式6.3 仅主机模式 7.…

SpringAI系列4: Tool Calling 工具调用 【感觉这版本有bug】

前言&#xff1a;在最近发布的 Spring AI 1.0.0.M6 版本中&#xff0c;其中一个重大变化是 Function Calling 被废弃&#xff0c;被 Tool Calling 取代。Tool Calling工具调用&#xff08;也称为函数调用&#xff09;是AI应用中的常见模式&#xff0c;允许模型通过一组API或工具…

机器人--里程计

教程 轮式里程计视频讲解 里程计分类 ros--odometry 什么是里程计 里程计是一种利用从移动传感器获得的数据来估计物体位置随时间的变化而改变的方法。该方法被用在许多机器人系统来估计机器人相对于初始位置移动的距离。 注意&#xff1a;里程计是一套算法&#xff0c;不…

设计模式——原型设计模式(创建型)

摘要 本文详细介绍了原型设计模式&#xff0c;这是一种创建型设计模式&#xff0c;通过复制现有对象&#xff08;原型&#xff09;来创建新对象&#xff0c;避免使用new关键字&#xff0c;可提高性能并简化对象创建逻辑。文章阐述了其优点&#xff0c;如提高性能、动态扩展和简…

通过mqtt 点灯

1 解析mqtt 传过来的json 用cjson 解析。 2 类似mvc的结构&#xff0c;调用具体的动作函数 定义设备处理结构体&#xff1a;使用结构体数组映射设备名称与处理函数&#xff0c;实现可扩展的指令分发分离设备逻辑&#xff1a;为每个设备&#xff08;如 LED、Motor&#xff0…

大数据-273 Spark MLib - 基础介绍 机器学习算法 决策树 分类原则 分类原理 基尼系数 熵

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大模型篇章已经开始&#xff01; 目前已经更新到了第 22 篇&#xff1a;大语言模型 22 - MCP 自动操作 FigmaCursor 自动设计原型 Java篇开…

基于 Spring Boot + Vue 的墙绘产品展示交易平台设计与实现【含源码+文档】

项目简介 本系统是一个基于 Spring Boot Vue 技术栈开发的墙绘产品展示交易平台&#xff0c;旨在提供一个高效、便捷的在线商城平台&#xff0c;方便用户浏览、选购墙绘产品&#xff0c;并提供管理员进行商品管理、订单管理等功能。系统采用了前后端分离的架构&#xff0c;前…

【机器学习】支持向量机

文章目录 一、支持向量机简述1.概念2.基本概念3.算法介绍4.线性可分5.算法流程 二、实验1.代码介绍2.模型流程3.实验结果4.实验小结 一、支持向量机简述 1.概念 支持向量机&#xff08;SVM&#xff09;是一类按监督学习方式对数据进行二元分类的广义线性分类器&#xff0c;其…

ONLYOFFICE深度解锁系列.4-OnlyOffice客户端原理-真的不支持多端同步

最近很多客户多要求直接部署onlyoffice服务端,还问能否和onlyoffice的客户端进行文件同步,当时真是一脸懵,还有的是老客户,已经安装了onlyoffice协作空间的,也在问如何配置客户端和协作空间的对接。由于问的人太多了,这里统一回复,先说结论,再说原理: 1.onlyoffice document s…

LLMTIME: 不用微调!如何用大模型玩转时间序列预测?

今天是端午节&#xff0c;端午安康&#xff01;值此传统佳节之际&#xff0c;我想和大家分享一篇关于基于大语言模型的时序预测算法——LLMTIME。随着人工智能技术的飞速发展&#xff0c;利用大型预训练语言模型&#xff08;LLM&#xff09;进行时间序列预测成为一个新兴且极具…

2.从0开始搭建vue项目(node.js,vue3,Ts,ES6)

从“0到跑起来一个 Vue 项目”&#xff0c;重点是各个工具之间的关联关系、职责边界和技术演化脉络。 从你写代码 → 到代码能跑起来 → 再到代码可以部署上线&#xff0c;每一步都有不同的工具参与。 &#x1f63a;&#x1f63a;1. 安装 Node.js —— 万事的根基 Node.js 是…