当前位置: 首页 > article >正文

milkup:桌面端 markdown AI续写和即时渲染

一、项目背景与需求分析1.1 milkup 项目简介milkup 是一个现代化的桌面端 Markdown 编辑器基于 Electron Vue 3 TypeScript 构建。项目的核心目标是提供一个功能强大、体验优雅、性能出色的 Markdown 编辑环境。核心技术栈前端框架Vue 3 TypeScript编辑器核心Milkdown基于 ProseMirror Crepe源码编辑器CodeMirror 6桌面框架Electron构建工具Vite esbuild包管理器pnpm1.2 为什么需要即时渲染模式在 Markdown 编辑器的发展历程中编辑模式经历了几个阶段分栏预览模式如早期的 MarkdownPad左侧源码右侧预览割裂感强纯所见即所得模式如 Notion完全隐藏语法失去了 Markdown 的简洁性即时渲染模式如 Typora平衡了语法可见性和渲染效果Typora 的成功证明了即时渲染模式的优越性写作流畅性不需要在源码和预览之间切换视线语法可控性需要时可以看到和编辑原始语法视觉舒适性大部分时间看到的是渲染后的效果然而Typora 是闭源软件且已经停止免费更新。市面上缺少一个开源、现代化、可扩展的即时渲染编辑器。这就是 feat-ir 分支的诞生背景。1.3 为什么需要 AI 续写功能随着 AI 技术的发展智能写作辅助已经成为现代编辑器的标配Cursor、GitHub Copilot在代码编辑领域大放异彩Notion AI、飞书妙记在文档编辑领域提供智能补全Grammarly在英文写作领域提供语法建议但在 Markdown 编辑器领域AI 集成还相对滞后。大多数 Markdown 编辑器要么完全不支持 AI要么只是简单地调用 API 生成文本缺乏对 Markdown 结构的理解。feat-ai 分支的目标是结构化理解理解文档的标题层级、上下文关系多提供商支持支持 OpenAI、Claude、Gemini、Ollama 等多种 AI 服务无缝集成像代码补全一样自然不打断写作流程本地优先支持 Ollama 等本地模型保护隐私二、feat-ir即时渲染模式详解2.1 功能特性feat-ir 分支实现了类似 Typora 的即时渲染模式核心特性包括2.1.1 智能源码显示当光标移动到 Markdown 语法元素时自动显示该元素的源码语法行内标记Marks**加粗**→ 光标进入时显示前后的***斜体*→ 显示前后的*代码→ 显示前后的~~删除线~~→ 显示前后的~~[链接文本](URL)→ 显示[]()结构块级元素Nodes标题显示对应数量的#符号如#表示一级标题图片显示![alt](milkup:///RDpcb3BlbnNvdXJjZVxtaWxrdXBcbWlsa3Vw77ya5qGM6Z2i56uvIG1hcmtkb3duIEFJ57ut5YaZ5ZKM5Y2z5pe25riy5pTLm1k/src)完整语法2.1.2 即时编辑能力不仅可以看到源码还可以直接编辑链接 URL 编辑点击 URL 部分可以直接修改链接地址图片属性编辑可以修改图片的 alt 文本和 src 路径实时生效编辑完成后按 Enter 或失焦修改立即生效2.1.3 键盘导航提供流畅的键盘操作体验ArrowLeft/Right在源码编辑器和渲染视图之间切换焦点Enter提交编辑并返回渲染视图自动跳出光标移出语法元素时自动隐藏源码2.2 实现原理2.2.1 ProseMirror 装饰器系统即时渲染的核心是 ProseMirror 的Decoration装饰器系统。装饰器允许我们在不修改文档结构的情况下在视图层添加额外的 DOM 元素。// 核心插件结构 export const sourceOnFocusPlugin $prose((ctx) { return new Plugin({ state: { init() { return DecorationSet.empty; }, apply(tr, oldState) { const { selection } tr; const decorations: Decoration[] []; // 根据光标位置动态生成装饰器 // ... return DecorationSet.create(tr.doc, decorations); } }, props: { decorations(state) { return this.getState(state); } } }); });装饰器的优势非侵入性不修改文档的实际内容高性能只在视图层渲染不影响数据层灵活性可以动态添加、移除装饰器2.2.2 Marks 处理策略对于行内标记如加粗、斜体、链接我们需要在文本前后添加语法符号实现思路遍历光标位置的 Marks获取当前光标所在位置的所有标记计算标记范围找到每个标记的起始和结束位置创建装饰器在起始位置前和结束位置后插入语法符号以链接为例// 处理链接标记 if (mark.type.name link) { const href mark.attrs.href || ; // 创建前缀 [ const prefixSpan document.createElement(span); prefixSpan.className md-source; prefixSpan.textContent [; // 创建后缀 ](URL) const suffixSpan document.createElement(span); suffixSpan.className md-source; suffixSpan.textContent ](; // 创建可编辑的 URL 部分 const urlSpan document.createElement(span); urlSpan.className md-source-url-editable; urlSpan.contentEditable true; urlSpan.textContent href; // 监听编辑事件 urlSpan.addEventListener(blur, () { const newHref urlSpan.textContent || ; if (newHref ! href) { // 更新文档中的链接 view.dispatch( view.state.tr .removeMark(start, end, mark.type) .addMark(start, end, mark.type.create({ href: newHref })) ); } }); suffixSpan.appendChild(urlSpan); const closingSpan document.createElement(span); closingSpan.className md-source; closingSpan.textContent ); suffixSpan.appendChild(closingSpan); // 添加装饰器 decorations.push( Decoration.widget(start, () prefixSpan), Decoration.widget(end, () suffixSpan) ); }关键点使用contentEditabletrue实现即时编辑通过blur事件监听编辑完成使用 ProseMirror 的transaction更新文档2.2.3 Nodes 处理策略对于块级元素如标题、图片处理方式略有不同标题处理if (node.type.name heading) { const level node.attrs.level || 1; const prefix #.repeat(level) ; const span document.createElement(span); span.className md-source; span.textContent prefix; decorations.push( Decoration.widget($from.start(), () span) ); }图片处理图片的处理更复杂因为需要同时编辑 alt 文本和 src 路径if (node.type.name image) { const { src, alt } node.attrs; // 创建 ![ const prefixSpan document.createElement(span); prefixSpan.className md-source; prefixSpan.textContent ![; // 创建可编辑的 alt const altSpan document.createElement(span); altSpan.className md-source-editable; altSpan.contentEditable true; altSpan.textContent alt || ; // 创建 ](milkup:///RDpcb3BlbnNvdXJjZVxtaWxrdXBcbWlsa3Vw77ya5qGM6Z2i56uvIG1hcmtkb3duIEFJ57ut5YaZ5ZKM5Y2z5pe25riy5pTLm1k/ %20%20const%20middleSpan%20%20document.createElement(span); middleSpan.className md-source; middleSpan.textContent ](; // 创建可编辑的 src const srcSpan document.createElement(span); srcSpan.className md-source-url-editable; srcSpan.contentEditable true; srcSpan.textContent src || ; // 创建 ) const suffixSpan document.createElement(span); suffixSpan.className md-source; suffixSpan.textContent ); // 组合所有元素 const container document.createElement(div); container.append(prefixSpan, altSpan, middleSpan, srcSpan, suffixSpan); decorations.push( Decoration.widget(pos, () container) ); }2.2.4 样式设计为了让源码显示既清晰又不突兀我们设计了专门的样式// 源码基础样式 .md-source { color: var(--text-color-4); // 使用较浅的颜色 font-family: var(--milkup-font-code); // 等宽字体 opacity: 0.6; // 半透明 background: var(--background-color-2); // 浅色背景 padding: 0 2px; border-radius: 2px; font-size: 0.9em; } // 可编辑的 URL 样式 .md-source-url-editable { display: inline-block; outline: none; cursor: text; border-bottom: 1px dashed var(--border-color); // 虚线下划线提示可编辑 min-width: 50px; :hover { background: var(--background-color-3); } :focus { border-bottom-style: solid; background: var(--background-color-3); } }设计原则低对比度使用半透明和浅色不干扰阅读等宽字体保持代码感与正文区分交互提示可编辑元素有明确的视觉反馈2.3 技术挑战与解决方案2.3.1 光标跳出问题问题当用户在可编辑的 URL 中按方向键时光标可能被困在contentEditable元素中无法跳出。解决方案监听键盘事件手动控制光标移动urlSpan.addEventListener(keydown, (e) { if (e.key ArrowLeft urlSpan.selectionStart 0) { // 光标在最左侧按左键跳出 e.preventDefault(); const pos view.posAtDOM(urlSpan, 0); view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, pos))); view.focus(); } else if (e.key ArrowRight urlSpan.selectionEnd urlSpan.textContent.length) { // 光标在最右侧按右键跳出 e.preventDefault(); const pos view.posAtDOM(urlSpan, urlSpan.textContent.length); view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, pos 1))); view.focus(); } else if (e.key Enter) { // 按 Enter 提交并跳出 e.preventDefault(); urlSpan.blur(); } });2.3.2 装饰器性能优化问题每次光标移动都重新计算所有装饰器可能导致性能问题。解决方案增量更新只在光标位置变化时更新装饰器缓存机制缓存上一次的装饰器集合避免重复计算范围限制只处理光标附近的元素不遍历整个文档apply(tr, oldState) { // 如果光标位置没变直接返回旧状态 if (!tr.docChanged !tr.selectionSet) { return oldState; } // 只处理光标附近 1000 字符范围内的元素 const { from, to } tr.selection; const rangeStart Math.max(0, from - 500); const rangeEnd Math.min(tr.doc.content.size, to 500); // 只在这个范围内查找需要装饰的元素 // ... }2.3.3 与其他插件的兼容性问题装饰器可能与其他插件如拼写检查、语法高亮冲突。解决方案装饰器优先级使用 ProseMirror 的spec.key设置优先级避免重叠检测装饰器是否重叠避免覆盖事件隔离使用stopPropagation防止事件冒泡三、feat-aiAI 续写功能详解3.1 功能特性feat-ai 分支为 milkup 带来了智能续写能力让 AI 成为你的写作助手。3.1.1 多 AI 提供商支持支持主流的 AI 服务提供商OpenAIGPT-3.5-turbo、GPT-4、GPT-4-turboAnthropicClaude 3 Haiku、Claude 3 Sonnet、Claude 3 OpusGoogleGemini Pro、Gemini Pro VisionOllama支持本地运行的开源模型Llama 2、Mistral、Qwen 等自定义 API兼容 OpenAI API 格式的任何服务配置界面用户可以在设置中轻松配置 AI 服务选择提供商输入 API Key设置 Base URL用于代理或自定义服务选择模型调整温度参数控制创造性设置防抖延迟控制触发频率3.1.2 结构化上下文理解AI 续写不是简单地续接文本而是理解文档的结构提取的上下文信息文件名了解文档主题标题层级理解文档结构和当前章节前文内容分析写作风格和上下文光标位置确定续写的起点示例假设你正在写一篇技术博客# Vue 3 组合式 API 最佳实践 ## 一、为什么选择组合式 API 组合式 API 是 Vue 3 引入的新特性它提供了更灵活的代码组织方式。 ## 二、核心概念 ### 2.1 响应式系统 Vue 3 的响应式系统基于 Proxy相比 Vue 2 的 Object.defineProperty 有以下优势 - 可以检测属性的添加和删除 - 可以检测数组索引和长度的变化 - [光标在这里]AI 会理解这是一篇关于 Vue 3 的技术文章当前在讨论响应式系统的优势前面已经列举了两个优势应该继续列举更多优势或展开说明3.1.3 智能触发机制防抖策略用户停止输入后等待 1-3 秒可配置避免频繁调用 API节省成本不打断用户的写作流程触发条件光标在段落末尾前面有足够的上下文至少 50 个字符不在代码块、表格等特殊区域内取消机制用户继续输入时自动取消当前请求文档内容变化时清除已显示的建议3.1.4 优雅的 UI 集成显示方式续写建议以半透明文本显示在光标后使用不同的颜色和字体样式与正文区分不占用实际的文档空间交互方式按Tab键接受建议按Esc键拒绝建议继续输入自动清除建议视觉设计.ai-completion-suggestion { color: var(--text-color-3); opacity: 0.5; font-style: italic; pointer-events: none; // 不影响鼠标交互 user-select: none; // 不可选中 }3.2 实现原理3.2.1 插件架构AI 续写功能通过 ProseMirror 插件实现核心文件位于src/renderer/components/editor/plugins/completionPlugin.ts。插件状态管理export const completionPlugin $prose((ctx) { const completionKey new PluginKey(completion); return new Plugin({ key: completionKey, state: { init() { return { decoration: DecorationSet.empty, suggestion: null, loading: false }; }, apply(tr, value) { // 文档内容变化时清除建议 if (tr.docChanged) { return { decoration: DecorationSet.empty, suggestion: null, loading: false }; } // 手动更新如 AI 返回结果 const meta tr.getMeta(completionKey); if (meta) { return meta; } return value; } }, props: { decorations(state) { return this.getState(state)?.decoration; }, handleKeyDown(view, event) { // 处理 Tab 键接受建议 if (event.key Tab) { const state this.getState(view.state); if (state?.suggestion) { event.preventDefault(); const tr view.state.tr.insertText( state.suggestion, view.state.selection.to ); tr.setMeta(completionKey, { decoration: DecorationSet.empty, suggestion: null, loading: false }); view.dispatch(tr); return true; } } return false; } } }); });插件状态包含三个字段decoration用于显示建议的装饰器集合suggestion当前的建议文本loading是否正在请求 AI3.2.2 多 AI 提供商集成AI 服务层位于src/renderer/services/ai.ts通过统一的接口支持多个提供商。服务接口设计export class AIService { static async complete(context: APIContext): PromiseCompletionResponse { const config useAIConfig().config.value; if (!config.enabled || !config.apiKey) { throw new Error(AI 服务未配置); } // 根据提供商构建不同的请求 const { url, headers, body } this.buildRequest(config, context); // 发送请求 const response await this.request(url, { method: POST, headers, body: JSON.stringify(body) }); // 解析响应 return this.parseResponse(response, config.provider); } }各提供商的实现差异OpenAI / 自定义 API使用response_format强制 JSON 输出case openai: case custom: return { url: ${config.baseUrl}/chat/completions, headers: { Content-Type: application/json, Authorization: Bearer ${config.apiKey} }, body: { model: config.model, messages: [ { role: system, content: SYSTEM_PROMPT }, { role: user, content: userMessage } ], temperature: config.temperature, response_format: { type: json_schema, json_schema: { name: continuation, schema: { type: object, properties: { continuation: { type: string } }, required: [continuation] } } } } };Anthropic (Claude)使用 Tool Use 机制case anthropic: return { url: ${config.baseUrl}/v1/messages, headers: { Content-Type: application/json, x-api-key: config.apiKey, anthropic-version: 2023-06-01 }, body: { model: config.model, system: SYSTEM_PROMPT, messages: [{ role: user, content: userMessage }], tools: [{ name: print_continuation, description: 输出续写内容, input_schema: { type: object, properties: { continuation: { type: string } }, required: [continuation] } }], tool_choice: { type: tool, name: print_continuation } } };Google Gemini使用responseMimeType和responseSchemacase gemini: return { url: ${config.baseUrl}/v1beta/models/${config.model}:generateContent?key${config.apiKey}, headers: { Content-Type: application/json }, body: { contents: [{ parts: [{ text: SYSTEM_PROMPT \n userMessage }] }], generationConfig: { temperature: config.temperature, responseMimeType: application/json, responseSchema: { type: OBJECT, properties: { continuation: { type: STRING } }, required: [continuation] } } } };Ollama使用format参数指定 JSON Schemacase ollama: return { url: ${config.baseUrl}/api/chat, headers: { Content-Type: application/json }, body: { model: config.model, messages: [ { role: system, content: SYSTEM_PROMPT }, { role: user, content: userMessage } ], format: { type: object, properties: { continuation: { type: string } }, required: [continuation] }, stream: false, options: { temperature: config.temperature } } };设计亮点统一的接口隐藏提供商差异充分利用各提供商的原生能力JSON Schema、Tool Use易于扩展添加新提供商只需增加一个 case3.2.3 上下文提取策略AI 续写的质量很大程度上取决于上下文的质量。milkup 实现了智能的上下文提取策略。提取的信息文件标题从文件路径中提取const fileTitle (window as any).__currentFilePath ? (window as any).__currentFilePath.split(/[\/]/).pop() : 未命名文档;前文内容获取光标前最近 200 个字符const start Math.max(0, to - 200); const previousContent doc.textBetween(start, to, \n);标题层级遍历文档提取所有标题const headers: { level: number; text: string }[] []; doc.nodesBetween(0, to, (node, pos) { if (node.type.name heading) { if (pos node.nodeSize to) { headers.push({ level: node.attrs.level, text: node.textContent }); } return false; } return true; });当前章节确定当前所在的章节和子章节let sectionTitle 未知; let subSectionTitle 未知; if (headers.length 0) { const lastHeader headers[headers.length - 1]; subSectionTitle lastHeader.text; // 查找父级标题 const parentHeader headers .slice(0, -1) .reverse() .find((h) h.level lastHeader.level); if (parentHeader) { sectionTitle parentHeader.text; } }构建 Promptprivate static buildPrompt(context: APIContext): string { return 上下文 文章标题${context.fileTitle || 未知} 大标题${context.sectionTitle || 未知} 本小节标题${context.subSectionTitle || 未知} 前面内容请紧密衔接${context.previousContent}; }System Promptconst SYSTEM_PROMPT 你是一个技术文档续写助手。 严格只输出以下 JSON**不要有任何前缀、后缀、markdown、换行、解释** {continuation: 接下来只写3–35个汉字的自然衔接内容} ;设计理念结构化理解不仅提供文本还提供文档结构精确定位明确当前所在的章节位置长度控制限制 3-35 个汉字避免过度生成格式约束强制 JSON 输出便于解析3.2.4 UI 显示机制建议的显示使用 ProseMirror 的 Decoration 系统在光标位置插入半透明的建议文本。创建建议 Widget// 创建建议元素 const widget document.createElement(span); widget.textContent result.continuation; widget.className ai-completion-suggestion; widget.style.color var(--text-color-light, #999); widget.style.opacity 0.6; widget.style.fontStyle italic; widget.style.pointerEvents none; // 不影响鼠标交互 widget.style.userSelect none; // 不可选中 widget.dataset.suggestion result.continuation; // 创建装饰器 const deco Decoration.widget(to, widget, { side: 1 }); const decoSet DecorationSet.create(view.state.doc, [deco]); // 更新编辑器状态 const tr view.state.tr.setMeta(completionKey, { decoration: decoSet, suggestion: result.continuation, loading: false }); view.dispatch(tr);样式设计.ai-completion-suggestion { color: var(--text-color-3); opacity: 0.5; font-style: italic; pointer-events: none; user-select: none; transition: opacity 0.2s ease; :hover { opacity: 0.7; } }交互设计Tab 键接受建议if (event.key Tab) { const state this.getState(view.state); if (state?.suggestion) { event.preventDefault(); // 插入建议文本 const tr view.state.tr.insertText( state.suggestion, view.state.selection.to ); // 清除建议 tr.setMeta(completionKey, { decoration: DecorationSet.empty, suggestion: null, loading: false }); view.dispatch(tr); return true; } }自动清除机制apply(tr, value) { // 文档内容变化时清除建议 if (tr.docChanged) { return { decoration: DecorationSet.empty, suggestion: null, loading: false }; } return value; }用户体验细节半透明显示不干扰正常阅读斜体样式与正文区分不可交互不影响鼠标点击和文本选择即时清除用户继续输入时自动消失快捷接受Tab 键一键接受3.3 技术挑战与解决方案3.3.1 结构化输出的挑战问题不同的 AI 模型对输出格式的控制能力不同有些模型可能输出额外的文本、markdown 代码块或解释性内容。解决方案利用各提供商的原生能力OpenAI使用response_format的 JSON SchemaClaude使用 Tool Use 机制Gemini使用responseMimeType和responseSchemaOllama使用format参数多层解析策略private static parseResponse(text: string): CompletionResponse { try { // 1. 尝试直接 JSON 解析 const cleanText text.replace(/json\n?|\n?/g, ).trim(); const json JSON.parse(cleanText); if (json.continuation) { return { continuation: json.continuation }; } } catch (e) { console.warn(JSON parse failed, trying regex extraction); } // 2. 正则提取 const match text.match(/continuation\s*:\s*([^])/); if (match match[1]) { return { continuation: match[1] }; } // 3. 兜底策略如果文本很短且不包含 JSON 结构直接使用 if (text.length 50 !text.includes({)) { return { continuation: text.trim() }; } throw new Error(Failed to parse AI response); }鲁棒性保证清理 markdown 代码块标记正则表达式兜底短文本直接使用多层容错机制3.3.2 防抖与取消机制问题用户输入时频繁触发 AI 请求浪费资源且影响体验。解决方案防抖触发let debounceTimer: NodeJS.Timeout | null null; view.updateState(view.state); // 清除旧的定时器 if (debounceTimer) { clearTimeout(debounceTimer); } // 设置新的定时器 debounceTimer setTimeout(async () { try { const result await AIService.complete(context); // 显示建议 // ... } catch (error) { console.error(AI completion failed:, error); } }, config.debounceWait || 1500);请求取消let currentAbortController: AbortController | null null; // 取消旧请求 if (currentAbortController) { currentAbortController.abort(); } // 创建新的 AbortController currentAbortController new AbortController(); const response await fetch(url, { signal: currentAbortController.signal, // ... });优化效果减少不必要的 API 调用节省成本提升响应速度避免过时的建议3.3.3 配置管理与持久化问题用户配置需要在应用重启后保持且需要响应式更新。解决方案使用 VueUse 的useStorageimport { useStorage } from vueuse/core; export function useAIConfig() { const config useStorageAIConfig( milkup-ai-config, defaultAIConfig, localStorage, { mergeDefaults: true } ); return { config }; }优势自动同步到 localStorage响应式更新配置变化立即生效支持默认值合并类型安全3.3.4 Ollama 模型列表动态获取问题Ollama 支持多种本地模型需要动态获取可用模型列表。解决方案async function fetchOllamaModels() { loadingModels.value true; try { const models await AIService.getModels(config.value); ollamaModels.value models; } catch (e) { toast.show(获取模型列表失败, error); } finally { loadingModels.value false; } } // AIService.getModels 实现 static async getModels(config: AIConfig): Promisestring[] { if (config.provider ollama) { const res await this.request(${config.baseUrl}/api/tags, { method: GET }); return res.models?.map((m: any) m.name) || []; } return []; }用户体验自动检测本地可用模型下拉选择无需手动输入实时刷新四、对比分析4.1 即时渲染模式对比特性milkup (feat-ir)TyporaNotionVS Code Markdown Preview开源✅ 是❌ 否❌ 否✅ 是即时渲染✅ 是✅ 是✅ 是❌ 否分栏预览源码可见✅ 光标聚焦时显示✅ 光标聚焦时显示❌ 完全隐藏✅ 始终显示源码可编辑✅ 链接、图片可直接编辑⚠️ 部分支持❌ 否✅ 是技术栈ProseMirror Milkdown自研自研CodeMirror扩展性✅ 插件化架构❌ 不支持插件⚠️ 有限的 API✅ VS Code 插件生态性能✅ 优秀✅ 优秀⚠️ 大文档较慢✅ 优秀跨平台✅ Windows/Mac/Linux✅ Windows/Mac/Linux✅ Web/桌面/移动✅ Windows/Mac/Linuxmilkup 的优势开源免费完全开源可自由定制现代化技术栈基于 Vue 3 TypeScript ProseMirror可扩展性强插件化架构易于添加新功能源码编辑能力链接和图片可直接编辑无需切换模式Typora 的优势成熟稳定经过多年打磨功能完善用户体验细节打磨到位交互流畅Notion 的优势协作能力多人实时协作数据库功能不仅是编辑器还是知识管理工具4.2 AI 续写功能对比特性milkup (feat-ai)CursorNotion AIGitHub Copilot支持场景Markdown 文档代码编辑文档编辑代码编辑多提供商✅ OpenAI/Claude/Gemini/Ollama❌ 仅 OpenAI❌ 自有模型❌ 仅 GitHub 模型本地模型✅ 支持 Ollama❌ 否❌ 否❌ 否结构化理解✅ 理解标题层级✅ 理解代码结构⚠️ 有限✅ 理解代码上下文触发方式自动防抖触发自动触发手动触发自动触发接受方式Tab 键Tab 键点击按钮Tab 键开源✅ 是❌ 否❌ 否❌ 否隐私保护✅ 支持本地模型❌ 数据上传云端❌ 数据上传云端❌ 数据上传云端milkup 的优势多提供商支持可自由选择 AI 服务本地优先支持 Ollama保护隐私开源透明代码公开可审计针对 Markdown专门优化文档写作场景Cursor/Copilot 的优势代码专精针对代码编辑优化上下文更丰富可以理解整个项目成熟度高经过大量用户验证Notion AI 的优势多功能不仅续写还支持总结、翻译、改写等集成度高与 Notion 生态深度集成

相关文章:

milkup:桌面端 markdown AI续写和即时渲染

一、项目背景与需求分析1.1 milkup 项目简介milkup 是一个现代化的桌面端 Markdown 编辑器,基于 Electron Vue 3 TypeScript 构建。项目的核心目标是提供一个功能强大、体验优雅、性能出色的 Markdown 编辑环境。核心技术栈:前端框架:Vue 3…...

Shell脚本进程锁机制解析

1. 命令行参数解析 (第9-21行)12345while getopts "m:o:r:" arg; docase $arg in# ... 参数处理逻辑(代码中省略了具体内容)esacdone使用 getopts 解析命令行参数支持三个带参数的选项:-m、-o、-r具体处理逻辑在代码中被省略了2. 文…...

FastBle单元测试终极指南:Mockito在Android蓝牙BLE开发中的7个实战技巧

FastBle单元测试终极指南:Mockito在Android蓝牙BLE开发中的7个实战技巧 【免费下载链接】FastBle Android Bluetooth Low Energy (BLE) Fast Development Framework. It uses simple ways to filter, scan, connect, read ,write, notify, readRssi, setMTU, and mu…...

收藏备用!小白程序员必看,大模型核心原理拆解(通俗易懂版)

本文专为CSDN小白程序员、AI入门者打造,用“技术拆解通俗类比”的方式,深入解析大模型的核心原理,避开专业术语壁垒。明确大模型的AI分支定位,拆解其三大底层逻辑,补充微调、提示工程的实操要点,澄清新手常…...

基于BiTCN - BiGRU的分类预测Matlab代码实践:新手友好指南

基于BiTCN-BiGRU分类 Matlab代码 基于双向时间卷积网络结合双向门控循环单元(BiTCN-BiGRU)的数据分类预测(可以更换为单、多变量时序预测/回归,),Matlab代码,可直接运行,适合小白新手 程序已经调试好,无需更改代码替换…...

3分钟上手Hysteria2:从安装到连接的超简单教程

3分钟上手Hysteria2:从安装到连接的超简单教程 Hysteria2是一款高效的网络加速工具,通过一键安装脚本即可快速部署,特别适合新手用户。本教程将带你在3分钟内完成从安装到连接的全过程,让你轻松享受高速网络体验。 准备工作&#…...

COMSOL 流固共轭传热拓扑优化:解锁高效液冷流道设计

COMSOL流固共轭传热拓扑优化 流固共轭传热为同时包含传导、对流的流热耦合场问题,流固共轭传热的拓扑优化技术通常应用于复杂液冷流道的设计,常见于微通道散热器的设计 使用COMSOL软件搭建拓扑优化流程,实现流道流阻小,换热量大等…...

FlutterFire云函数终极部署指南:Firebase函数一键部署前必做的10个检查

FlutterFire云函数终极部署指南:Firebase函数一键部署前必做的10个检查 【免费下载链接】flutterfire 🔥 A collection of Firebase plugins for Flutter apps. 项目地址: https://gitcode.com/gh_mirrors/fl/flutterfire FlutterFire是Firebase官…...

PromptSource批量操作工具:一次性修改数百个提示模板的技巧

PromptSource批量操作工具:一次性修改数百个提示模板的技巧 【免费下载链接】promptsource Toolkit for creating, sharing and using natural language prompts. 项目地址: https://gitcode.com/gh_mirrors/pr/promptsource PromptSource是一个强大的自然语…...

如何实现open62541与物联网协议集成:MQTT、CoAP和HTTP的完美结合

如何实现open62541与物联网协议集成:MQTT、CoAP和HTTP的完美结合 【免费下载链接】open62541 Open source implementation of OPC UA (OPC Unified Architecture) aka IEC 62541 licensed under Mozilla Public License v2.0 项目地址: https://gitcode.com/gh_mi…...

RustBook 搜索算法大全:从顺序搜索到哈希搜索的完整实现

RustBook 搜索算法大全:从顺序搜索到哈希搜索的完整实现 【免费下载链接】RustBook A book about Rust Data Structures and Algorithms. 项目地址: https://gitcode.com/gh_mirrors/ru/RustBook RustBook 是一本专注于 Rust 数据结构与算法的开源书籍&#…...

Muon最佳实践:10个提升开发效率的实用技巧

Muon最佳实践:10个提升开发效率的实用技巧 【免费下载链接】muon GPU based Electron on a diet 项目地址: https://gitcode.com/gh_mirrors/mu/muon Muon作为一款基于GPU的轻量级Electron替代方案,采用Golang开发并使用Ultralight引擎&#xff0…...

Flow错误处理与监控:集成Sentry实现生产级错误追踪

Flow错误处理与监控:集成Sentry实现生产级错误追踪 【免费下载链接】flow Browser-based ePub reader 项目地址: https://gitcode.com/gh_mirrors/flo/flow Flow作为一款基于浏览器的ePub阅读器,为用户提供流畅的电子书阅读体验。在开发过程中&am…...

2026届必备的六大AI写作助手推荐

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 在现在这个人工智能生成内容已经被广泛运用的当下,降低AIGC检测概率的工具顺势冒…...

EMS智慧能源管理、物联网双碳、建筑用能、能耗统计、能源流向、损耗分析、班组用能、水电数据、能耗分析、零碳园区、碳汇管理、工艺优化分析、用能诊断、计量仪表、用能预警、配电

基于 Vue3 / Spring Boot/Spring Cloud & Alibaba 微服务架构 项目技术框架 RuoYi-Cloud 基础框架上开发而成 源智优控AI能源大脑,能源AI版,即将上线 仓库地址: https://gitee.com/guangdong122/energy-management 一、系统介绍 能源…...

2026届学术党必备的六大AI辅助论文工具实际效果

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 知网在近期对自己的 AIGC 检测服务进行了升级,其目的在于识别存在于论文之中的、…...

2026届最火的五大降AI率网站实际效果

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 现当下各种AI检测工具正变得越发普及,要是用户所提交的文本被判定为有着高AI生成…...

2026最权威的AI学术平台推荐

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 把维普系统检测 AI 生成文本的特性揪住,要使 AI 率降下来,得从词汇、…...

3个妙招搞定Cursor限制:开源工具让你告别API限制烦恼

3个妙招搞定Cursor限制:开源工具让你告别API限制烦恼 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pro 功能: Youve reached your tria…...

S-UI系统调用分析:与操作系统交互的底层实现

S-UI系统调用分析:与操作系统交互的底层实现 还在为网络代理管理系统的底层实现而困惑?本文将深入解析S-UI如何通过系统调用与操作系统深度交互,让你全面掌握这套高级Web面板的底层工作原理。 读完本文你将了解: S-UI如何处理系…...

S-UI缓存策略设计:API响应与静态资源缓存

S-UI缓存策略设计:API响应与静态资源缓存 还在为S-UI面板加载缓慢而烦恼?本文将为你深度解析S-UI的缓存策略设计,帮你提升系统性能和用户体验。 读完本文你将获得: S-UI现有缓存机制全面解析静态资源优化配置技巧API响应缓存最…...

ai辅助开发:让快马平台智能诊断并生成最优的wsl ubuntu环境配置方案

在折腾开发环境配置的路上,相信不少朋友都踩过WSL安装Ubuntu的坑。从选择版本、处理依赖到解决网络问题,整个过程就像开盲盒。最近尝试用AI辅助完成这个任务时,意外发现了一条捷径——通过智能交互就能生成量身定制的环境方案。 传统配置的痛…...

怎么把webp转换成png?4种方法,新手也能零失误

在日常工作和生活中,webp转换成png挺实用的。比如PNG是无损压缩,还能保留透明背景,做图标、按钮、PPT配图都合适;而WebP虽然压缩效率高、省空间,但很多软件不兼容,像一些老版PS、办公软件,打开W…...

OpenClaw核心:上下文工程如何让AI更懂你?(万字源码深度解析)

我们之前说过除了记忆系统,Agent 是没什么技术难度的。 比如你自己做了个 Agent,如果只是想用他去装载几个 skill,去完成日常自媒体的选题、或者去小红书等平台上自动发发文章,那是比较简单的。 但,如果你想让这个 Age…...

高效微信聊天记录管理:解决数据丢失风险的本地化方案

高效微信聊天记录管理:解决数据丢失风险的本地化方案 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeChat…...

水泥路面裂缝分类数据集该数据集包含有图片40000张,类别是有裂缝和没有裂缝图像大小是227x227可直接进行使用

水泥路面裂缝分类数据集 该数据集包含有图片40000张,类别是有裂缝和没有裂缝 图像大小是227x227 可直接进行使用...

AI大模型系统学习路线:零基础入门人工智能,附AI大模型学习与面试资源!【非常详细】

人工智能(AI)正在重塑全球产业格局,从自动驾驶到医疗诊断,从金融风控到内容创作,AI技术已成为21世纪的核心竞争力。对于零基础学习者而言,构建系统化的学习路径至关重要。1. 明确学习动机职业转型 &#xf…...

S-UI前端工程化:ESLint与Prettier代码质量保障

S-UI前端工程化:ESLint与Prettier代码质量保障 还在为代码风格混乱、团队协作困难而头疼吗?S-UI作为专业的代理面板项目,通过完善的工程化配置确保了代码质量。本文将为你解析如何在类似项目中配置ESLint和Prettier,打造规范的开…...

闲鱼数据采集实战:从技术原理到商业洞察的完整指南

闲鱼数据采集实战:从技术原理到商业洞察的完整指南 【免费下载链接】xianyu_spider 闲鱼APP数据爬虫 项目地址: https://gitcode.com/gh_mirrors/xia/xianyu_spider 作为一名数据采集工程师,我曾面临这样的困境:电商平台数据分散、反爬…...

KRaft VS RocketMQ NameServer

Kafka KRaft 和 RocketMQ NameServer 是两大消息队列用于元数据/路由管理的核心组件,但设计哲学完全不同:KRaft 是强一致的共识集群(CP),NameServer 是无状态的分布式路由表(AP)。下面从架构、原理、优缺点、选型做全面对比。 一、核心定位与本质区别 Kafka KRaft 定位…...