普通视图

发现新文章,点击刷新页面。
昨天以前首页

ByteMD+CozeAPI+Coze平台Agent+Next搭建AI辅助博客撰写平台(逻辑清楚,推荐!)

作者 GISer_Jinger
2025年6月28日 22:20

背景

现在主流的博客平台AI接入不够完善,如CSDN接入的AI助手不支持多模态数据的交互、稀土掘金的编辑器AI功能似乎还没能很好接入(哈哈哈,似乎在考虑布局什么?)

痛点分析:

  • 用户常常以截图的形式截取内容,但是如果单纯的用解析工具,解析的为单纯文本,而在撰写博客时常常MD编辑器,这就可能要blogger继续借助第三方工具将内容转换为md格式,此过程繁琐(结合自身博客撰写遇到的问题)
  • 支持PDF等文档格式解析:PDF等格式也是常用的内容记录格式,但是如果需要将内容发布成博客,一是要复制文档内容,二是文档内容常常不是凝练好的常常有冗余,三是还需借助外部工具转化为MD格式才好发成MD格式
  • 文字常常不易理解,现主流博客编辑不支持将博客内容总结,生成一个思维导图,这让普通用户读起来或者blogger自己回顾起来可能有点吃力(调研腾讯文档在这里插入图片描述

功能:

  • 标题辅助生成、摘要总结(基本,调研CSDN)
  • 支持多模态数据的交互:图片和PDF等格式
  • 总结生成思维导图
  • 流式展示(Fetch+SSE,自己封装SSEWrapper和MessaageService,处理异常[浏览器主动断开、刷新])

支持多模态数据解析

图片

了解object_string多模态数据组织结构

当进行多模态数据进行对话时,首先了解对话的内容的数据结构是什么?object_string官方说明文档 在这里插入图片描述在这里插入图片描述在这里插入图片描述 当了解数据结构之后,我们不难知道,当单纯是普通文本对话时,内容为text格式,而涉及到多模态数据是就是object_string

多模态数据内容改如何传入到对话中(file_id和file_url)

解决这个问题,另外一个问题就是多模态数据如何处理?如何加入到对话中? 在这里插入图片描述

[
    {
        "type": "text",
        "text": "你好我有一个帽衫,我想问问它好看么,你帮我看看"
    }, {
        "type": "image",
        "file_id": "{{file_id_1}}"
    }, {
        "type": "pdf",
        "file_id": "{{file_id_2}}"
    },
]

看上面object_string组织不难理解,需要一个file_id或者file_url,file_id可以上传文件到Coze空间返回内容就有file_id,而file_url就需自己上传至自己的云存储器中,返回得到公网可访问的url地址

 // Step 1: Upload the file to Coze files API to get a file_id
            const uploadResult = await fetch('https://api.coze.cn/v1/files/upload', {
                method: 'POST',
                headers: {
                    "Authorization": `Bearer ${process.env.NEXT_PUBLIC_KEY}`,
                },
                body: formData,
            });

            if (!uploadResult.ok) {
                const errorData = await uploadResult.json();
                throw new Error(errorData.error || errorData.message || '文件上传到Coze失败。');
            }

            const uploadResponse = await uploadResult.json();
            console.log("Coze File upload response:", uploadResponse);

            const fileId = uploadResponse.data?.id;

案例:设计了一个工作流,输入数据为File格式(按正常是需要file_url,但是我避开,直接借助file_id) 如下面,按正常PDF_Reader需要一个PDFURL,就是一个公网可访问的pdf连接,但是临时去搭建比较繁琐,想着相传文件到Coze空间了,而且也有了file_id了,应该要纠结的就是id和url如何映射,CozeAPI有自动封装吗? 在这里插入图片描述

在这里插入图片描述 然后去创建工作流这去看,果然可以讲file_id传入参数中,自动会解析成url,file_id->file_url,但是我在对话中试了调用Agent,Agent自动调用并传参给工作流还是有问题——TODO:如何传参给Agent,Agent调用合适工作流并使用参数

原话:工作流开始节点的输入参数及取值,你可以在指定工作流的编排页面查看参数列表。 如果工作流输入参数为 Image 等类型的文件(条件1),可以调用上传文件 API 获取 file_id(条件2),在调用此 API 时,在 parameters 中以序列化之后的 JSON 格式传入 file_id。例如 “parameters” : { "input": "{"file_id": "xxxxx"}" } 在这里插入图片描述在这里插入图片描述在这里插入图片描述

const handleSend = useCallback(async (content: string, queryParams?: EventSourceConfig['queryParams'], isTitleGeneration: boolean = false) => {
        if (!content.trim() && uploadedFiles.length === 0) {
            message.warn('消息内容或文件不能为空!');
            return;
        }
        if (loading) {
            message.warn('AI 正在处理上一个请求,请稍候。');
            return;
        }

        const pdfFile = uploadedFiles.find(file => file.type === 'pdf');

        if (pdfFile) {
            // If a PDF is uploaded, trigger the workflow
            await executePdfWorkflowBackend(pdfFile.id, pdfFile.name);
            // Optionally, clear the input or add a user message for the PDF action
            setAiInput(''); // Clear the input after triggering PDF workflow
            setUploadedFiles([]); // Clear uploaded files after workflow trigger
            return; // Exit as the workflow is handled
        }


        let finalContentToSend: string;
        let finalContentType: ContentType;

        if (uploadedFiles.length > 0) {
            finalContentType = ContentType.MULTIMODAL;
            const objectContent: any[] = [{ type: 'text', text: content }];
            uploadedFiles.forEach(file => {
                objectContent.push({
                    type: file.type === 'pdf' ? 'file' : file.type, // Coze treats PDF as 'file' type for multimodal
                    file_id: file.id
                });
            });
            finalContentToSend = JSON.stringify(objectContent);
        } else {
            finalContentType = ContentType.TXT;
            finalContentToSend = content;
        }

        setLastRequestContent(finalContentToSend);
        console.log("AIDialogContent: Sending content:", finalContentToSend, "with contentType:", finalContentType);

        try {
            await messageService.send(finalContentToSend, {
                ...queryParams,
                contentType: finalContentType,
                llmProvider: selectedModel,
                onComplete: (latestMessageContent: string) => {
                    if (isTitleGeneration && onGenerateTitlePrompt) {
                        onGenerateTitlePrompt(latestMessageContent);
                    }
                }
            });
        } catch (error) {
            console.error("AIDialogContent: Error during message sending process:", error);
            if (!isStreamingMessage) {
                setLoading(false);
            }
        } finally {
            setAiInput('');
            console.log("AIDialogContent: Input cleared.");
        }
    }, [loading, isStreamingMessage, messageService, uploadedFiles, selectedModel, onGenerateTitlePrompt, executePdfWorkflowBackend]);

PDF

官方案例:解析文档 大模型本身并不具备文件读取和解析的能力,无法直接处理文件字节流。对于支持工具调用(Function calling)的模型,扣子提供插件功能来帮助大模型调用外部工具 API,拓展大模型的能力边界。扣子插件商店提供海量官方插件和第三方插件,例如链接读取插件可以将链接对应的文件内容解析为纯文本传递给大模型在这里插入图片描述


在这里插入图片描述

【代码随想录刷题总结】leetcode24-两两交换链表中的节点

2025年6月27日 17:42

引言

大家好啊,我是前端拿破轮😁。

跟着卡哥学算法有一段时间了,通过代码随想录的学习,受益匪浅,首先向卡哥致敬🫡。

但是在学习过程中我也发现了一些问题,很多当时理解了并且AC的题目过一段时间就又忘记了,或者不能完美的写出来。根据费曼学习法,光有输入的知识掌握的是不够牢靠的,所以我决定按照代码随想录的顺序,输出自己的刷题总结和思考。同时,由于以前学习过程使用的是JavaScript,而在2025年的今天,TypeScript几乎成了必备项,所以本专题内容也将使用TypeScript,来巩固自己的TypeScript语言能力。

题目信息

两两交换链表中的节点

leetcode题目链接

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

题目分析

本题考查对链表的基本操作,两两交换链表中的节点,这里要注意的是,我们要进行的是节点的交换,而不是节点的值的交换。不能通过修改节点的值来完成,而是需要通过调整节点间指针的指向关系,让相邻的节点互换。

对于本题,同样有递归和迭代两种实现方式。两种方式的优缺点可以参考上一期内容【代码随想录刷题总结】leetcode206-反转链表

题解

迭代法

对于迭代法,由于在过程中会涉及到对头结点位置的移动,所以使用虚拟头结点来保证对节点操作的一致性。

迭代法通常需要使用指针进行遍历,需要牢记一点,在单链表中,要想操作某一个节点,当前指针必须指向其前一个节点

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function swapPairs(head: ListNode | null): ListNode | null {
    // 剪枝
    if (!head || !head.next) return head;

    const dummy = new ListNode(0, head);

    // 当前指针
    let cur = dummy;
    while (cur && cur.next && cur.next.next) {
        const node1 = cur.next;
        const node2 = cur.next.next;

        // 改变指向
        node1.next = node2.next;
        node2.next = node1;
        cur.next = node2;

        // 移动指针
        cur = node1;
    }
    return dummy.next;
};

时间复杂度:O(n),其中n为链表的节点数量。

空间复杂度:O(1),只使用了常数个指针。

递归法

由于链表的定义具有递归性,所以本题也可以使用递归的方式来解决。

递归三部曲:

  1. 确定参数和返回值:function swapPairs(head: ListNode | null): ListNode | null
  2. 确定终止条件:当head或者head.next为null时,返回head
  3. 确定单层递归逻辑:在每一层中先将head.next.next为首的链表进行两两交换,然后再将headhead.next进行交换,并返回交换后的链表头。
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

function swapPairs(head: ListNode | null): ListNode | null {
    // 终止条件
    if (!head || !head.next) return head;

    const newList = swapPairs(head.next.next);

    // 交换前两个
    const next = head.next;

    head.next.next = head;
    head.next = newList;
    return next;
};

时间复杂度:O(n)

空间复杂度:O(n),因为递归法有递归调用栈,深度为n

总结

两两交换链表中的节点,同样有迭代和递归两种方式。迭代的空间复杂度更优,而且更容易理解,但是代码较冗长。在使用迭代法的时候要注意使用虚拟头节点。

递归法的空间复杂度较大,且理解略显困难,但是代码简洁精炼。对于超大规模问题,还是可能会导致栈溢出。

往期推荐✨✨✨

我是前端拿破轮,关注我,和您分享前端知识,我们下期见!

❌
❌