diff --git a/backend/llm.py b/backend/llm.py index 4f88721..38b8e5d 100644 --- a/backend/llm.py +++ b/backend/llm.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv load_dotenv() -OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gpt-oss:20b') +OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gpt-oss:120b') OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.0.120:11434') VLM_MODEL = os.getenv('VLM_MODEL', 'qwen3-vl:30b') diff --git a/plans/copilot-prompt-system-analysis.md b/plans/copilot-prompt-system-analysis.md deleted file mode 100644 index b400a12..0000000 --- a/plans/copilot-prompt-system-analysis.md +++ /dev/null @@ -1,473 +0,0 @@ -# GitHub Copilot 提示词系统分析 - -## 概述 - -GitHub Copilot 的提示词系统是一个复杂的代码补全引擎,采用声明式组件架构来构建发送给 LLM 的提示词。本文档基于 `completions-sample-code/` 目录的源代码分析。 - -## 核心架构 - -```mermaid -flowchart TB - subgraph Input - A[用户光标位置] --> B[CompletionState] - C[当前文档] --> B - D[相似文件] --> B - end - - subgraph PromptFactory - B --> E[VirtualPrompt] - E --> F[组件树构建] - end - - subgraph Components - F --> G[CompletionsContext] - G --> H[DocumentMarker] - G --> I[Traits] - G --> J[Diagnostics] - G --> K[CodeSnippets] - G --> L[SimilarFiles] - G --> M[RecentEdits] - F --> N[CurrentFile] - end - - subgraph Rendering - H --> O[CompletionsPromptRenderer] - I --> O - J --> O - K --> O - L --> O - M --> O - N --> O - O --> P[Prompt对象] - end - - subgraph Output - P --> Q[API请求] - Q --> R[LLM补全] - end -``` - -## 1. 提示词基础配置 - -### 1.1 Token 限制 - -来源: [`prompt/src/prompt.ts`](../completions-sample-code/prompt/src/prompt.ts) - -```typescript -// 最大补全长度 -export const DEFAULT_MAX_COMPLETION_LENGTH = 500; - -// 最大提示词长度 (模型上下文窗口 - 补全长度) -export const DEFAULT_MAX_PROMPT_LENGTH = 8192 - DEFAULT_MAX_COMPLETION_LENGTH; - -// 默认代码片段数量 -export const DEFAULT_NUM_SNIPPETS = 4; - -// 后缀匹配阈值 -export const DEFAULT_SUFFIX_MATCH_THRESHOLD = 10; -``` - -### 1.2 提示词分配比例 - -```typescript -export const DEFAULT_PROMPT_ALLOCATION_PERCENT = { - prefix: 35, // 光标前代码 - suffix: 15, // 光标后代码 - stableContext: 35, // 稳定上下文 - volatileContext: 15 // 动态上下文 -}; -``` - -## 2. 语言标记系统 - -### 2.1 支持的语言 - -来源: [`prompt/src/languageMarker.ts`](../completions-sample-code/prompt/src/languageMarker.ts) - -支持 60+ 种编程语言,每种语言定义了: -- `lineComment`: 单行注释标记 (start, end) -- `markdownLanguageIds`: Markdown 代码块语言标识符 - -示例: -```typescript -python: { - lineComment: { start: '#', end: '' }, - markdownLanguageIds: ['python', 'py', 'gyp'], -}, -javascript: { - lineComment: { start: '//', end: '' }, - markdownLanguageIds: ['javascript', 'js'], -}, -``` - -### 2.2 语言标记生成 - -```typescript -// 获取语言标记 -export function getLanguageMarker(doc: DocumentInfo): string { - if (dontAddLanguageMarker.indexOf(languageId) === -1 && !hasLanguageMarker(doc)) { - if (languageId in shebangLines) { - return shebangLines[languageId]; // 如 #!/usr/bin/env python3 - } else { - return `Language: ${languageId}`; - } - } - return ''; -} - -// 获取路径标记 -export function getPathMarker(doc: DocumentInfo): string { - if (doc.relativePath) { - return `Path: ${doc.relativePath}`; - } - return ''; -} -``` - -## 3. 组件系统架构 - -### 3.1 声明式组件 - -来源: [`prompt/src/components/components.ts`](../completions-sample-code/prompt/src/components/components.ts) - -Copilot 使用类似 React 的 JSX 语法来声明提示词组件: - -```typescript -// 基础组件类型 -export type PromptElementProps

= P & Readonly; - -// 组件上下文,提供状态管理 -export interface ComponentContext { - useState(initialState: S): [S, Dispatch>]; - useData(typePredicate: TypePredicate, consumer: DataConsumer): void; -} -``` - -### 3.2 默认提示词组件结构 - -来源: [`lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx`](../completions-sample-code/lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx) - -```tsx -function defaultCompletionsPrompt(accessor: ServicesAccessor) { - return ( - <> - - - - - - - - - - - ); -} -``` - -### 3.3 组件权重说明 - -| 组件 | 权重 | 说明 | -|------|------|------| -| RecentEdits | 0.99 | 最近编辑内容,最高优先级 | -| CodeSnippets | 0.9 | 代码片段 | -| SimilarFiles | 0.8 | 相似文件内容 | -| DocumentMarker | 0.7 | 文档标记(语言/路径) | -| Diagnostics | 0.65 | 诊断信息(错误/警告) | -| Traits | 0.6 | 代码特征 | -| CurrentFile | 1.0 | 当前文件内容(必须包含) | - -## 4. 当前文件组件 - -来源: [`lib/src/prompt/components/currentFile.tsx`](../completions-sample-code/lib/src/prompt/components/currentFile.tsx) - -### 4.1 光标前代码 (BeforeCursor) - -```tsx -export function BeforeCursor(props: { - document: CompletionRequestDocument | undefined; - position: Position | undefined; - maxCharacters: number; -}) { - let text = props.document.getText({ start: { line: 0, character: 0 }, end: props.position }); - if (text.length > props.maxCharacters) { - text = text.slice(-props.maxCharacters); // 截取最后 maxCharacters 字符 - } - return {text}; -} -``` - -### 4.2 光标后代码 (AfterCursor) - -```tsx -export function AfterCursor(props: {...}, context: ComponentContext) { - // 获取光标后所有文本 - let suffix = props.document.getText({ - start: props.position, - end: { line: Number.MAX_VALUE, character: Number.MAX_VALUE }, - }); - - // 后缀缓存机制:使用编辑距离判断是否复用缓存 - const dist = findEditDistanceScore(firstSuffixTokens.tokens, cachedSuffixTokens.tokens); - if (100 * dist < suffixMatchThreshold * tokens.length) { - suffixToUse = cachedSuffix; // 使用缓存的后缀 - } - - return {suffixToUse}; -} -``` - -## 5. 相似文件与代码片段 - -### 5.1 相似文件选择 - -来源: [`prompt/src/snippetInclusion/similarFiles.ts`](../completions-sample-code/prompt/src/snippetInclusion/similarFiles.ts) - -```typescript -export interface SimilarFilesOptions { - snippetLength: number; // 代码片段长度(行数) - threshold: number; // 相似度阈值 - maxTopSnippets: number; // 最大返回片段数 - maxCharPerFile: number; // 每文件最大字符数 - maxNumberOfFiles: number; // 最大文件数 - maxSnippetsPerFile: number; // 每文件最大片段数 -} - -// 默认配置 -export const defaultSimilarFilesOptions: SimilarFilesOptions = { - snippetLength: 60, - threshold: 0.0, - maxTopSnippets: 4, - maxCharPerFile: 10000, - maxNumberOfFiles: 20, - maxSnippetsPerFile: 1, -}; -``` - -### 5.2 Jaccard 相似度匹配 - -来源: [`prompt/src/snippetInclusion/selectRelevance.ts`](../completions-sample-code/prompt/src/snippetInclusion/selectRelevance.ts) - -```typescript -// 使用 Jaccard 相似度计算代码片段相关性 -abstract class WindowedMatcher { - protected abstract similarityScore(a: Set, b: Set): number; - - // 分词器:将代码转换为 token 集合 - class Tokenizer { - tokenize(a: string): Set { - return new Set(splitIntoWords(a).filter(x => !this.stopsForLanguage.has(x))); - } - } -} -``` - -### 5.3 代码片段格式化 - -来源: [`prompt/src/snippetInclusion/snippets.ts`](../completions-sample-code/prompt/src/snippetInclusion/snippets.ts) - -```typescript -export function announceSnippet(snippet: SnippetToAnnounce) { - const headline = snippet.relativePath - ? `Compare ${pluralizedSemantics} ${semantics} from ${snippet.relativePath}:` - : `Compare ${pluralizedSemantics} ${semantics}:`; - return { headline, snippet: snippet.snippet }; -} -``` - -## 6. API 请求格式 - -### 6.1 请求结构 - -来源: [`lib/src/openai/fetch.ts`](../completions-sample-code/lib/src/openai/fetch.ts) - -```typescript -type CompletionRequest = { - prompt: string; // 前缀代码 - suffix: string; // 后缀代码 - stream: true; // 始终使用流式响应 - max_tokens: number; // 最大生成 token 数 - n: number; // 并行补全数量 - temperature: number; // 温度参数 - top_p: number; // nucleus 采样参数 - stop: string[]; // 停止标记 - logprobs?: number; // logprob 数量 - extra: { - language: string; // 语言 ID - trim_by_indentation?: boolean; - force_indent?: number; - next_indent?: number; - prompt_tokens: number; - suffix_tokens: number; - context?: string[]; // 额外上下文 - }; -}; -``` - -### 6.2 停止标记 - -来源: [`lib/src/openai/openai.ts`](../completions-sample-code/lib/src/openai/openai.ts) - -```typescript -const stopsForLanguage: { [key: string]: string[] } = { - markdown: ['\n\n\n'], - python: ['\ndef ', '\nclass ', '\nif ', '\n\n#'], -}; - -export function getStops(languageId?: string) { - return stopsForLanguage[languageId ?? ''] ?? ['\n\n\n', '\n```']; -} -``` - -### 6.3 温度参数 - -```typescript -export function getTemperatureForSamples(numShots: number): number { - if (numShots <= 1) return 0.0; - else if (numShots < 10) return 0.2; - else if (numShots < 20) return 0.4; - else return 0.8; -} -``` - -## 7. Tokenization - -来源: [`prompt/src/tokenization/tokenizer.ts`](../completions-sample-code/prompt/src/tokenization/tokenizer.ts) - -### 7.1 支持的 Tokenizer - -```typescript -export enum TokenizerName { - cl100k = 'cl100k_base', // GPT-3.5/GPT-4 - o200k = 'o200k_base', // GPT-4o - mock = 'mock', // 测试用 -} -``` - -### 7.2 Tokenizer 接口 - -```typescript -export interface Tokenizer { - tokenLength(text: string): number; - tokenize(text: string): number[]; - detokenize(tokens: number[]): string; - tokenizeStrings(text: string): string[]; - takeLastTokens(text: string, n: number): { text: string; tokens: number[] }; - takeFirstTokens(text: string, n: number): { text: string; tokens: number[] }; - takeLastLinesTokens(text: string, n: number): string; -} -``` - -## 8. Tree-sitter 代码解析 - -来源: [`prompt/src/parse.ts`](../completions-sample-code/prompt/src/parse.ts) - -### 8.1 支持的语言 - -```typescript -export enum WASMLanguage { - Python = 'python', - JavaScript = 'javascript', - TypeScript = 'typescript', - TSX = 'tsx', - Go = 'go', - Ruby = 'ruby', - CSharp = 'c-sharp', - Java = 'java', - Php = 'php', - Cpp = 'cpp', -} -``` - -### 8.2 用途 - -- 判断代码块是否为空块开始 (`isEmptyBlockStart`) -- 判断代码块是否完成 (`isBlockBodyFinished`) -- 获取语法节点起始位置 (`getNodeStart`) - -## 9. 提示词构建流程 - -```mermaid -sequenceDiagram - participant User as 用户 - participant VSCode as VS Code - participant GT as GhostText - participant PF as PromptFactory - participant Components as 组件系统 - participant Tokenizer as Tokenizer - participant API as OpenAI API - - User->>VSCode: 输入代码 - VSCode->>GT: 请求补全 - GT->>PF: extractPrompt - PF->>Components: 构建组件树 - - Components->>Components: DocumentMarker - Components->>Components: Traits - Components->>Components: Diagnostics - Components->>Components: CodeSnippets - Components->>Components: SimilarFiles - Components->>Components: RecentEdits - Components->>Components: CurrentFile - - Components->>Tokenizer: 计算 token 数量 - Tokenizer-->>Components: 返回 token 数 - - Components->>Components: Elision 省略处理 - Components-->>PF: Prompt 对象 - PF-->>GT: PromptResponse - GT->>API: 发送请求 - API-->>GT: 流式返回补全 - GT-->>VSCode: 显示 Ghost Text - VSCode-->>User: 展示建议 -``` - -## 10. 关键设计模式 - -### 10.1 声明式组件 - -使用 JSX 语法声明提示词结构,支持: -- 组件组合 -- 权重分配 -- 状态管理 -- 数据订阅 - -### 10.2 虚拟提示词树 - -在渲染前构建虚拟树结构,支持: -- 增量更新 -- 高效 diff -- 条件渲染 - -### 10.3 Token 预算管理 - -- 每个组件有权重属性 -- 根据 token 预算动态省略内容 -- 优先保留高权重组件 - -### 10.4 后缀缓存 - -- 使用编辑距离判断后缀相似度 -- 相似时复用缓存的后缀 -- 减少 token 波动,提高缓存命中率 - -## 11. 实现参考 - -如果要在自己的项目中实现类似的提示词系统,需要关注以下核心模块: - -1. **Tokenizer**: 使用 tiktoken 进行准确的 token 计数 -2. **语言标记**: 为不同语言生成适当的标记 -3. **上下文收集**: 收集相似文件、最近编辑等上下文 -4. **Token 预算**: 动态分配 token 给不同组件 -5. **FIM 格式**: 使用 Fill-In-the-Middle 格式发送请求 - -## 总结 - -GitHub Copilot 的提示词系统是一个精心设计的工程系统,核心特点包括: - -1. **模块化组件架构**: 使用声明式组件构建提示词 -2. **智能上下文选择**: 通过 Jaccard 相似度选择相关代码片段 -3. **Token 预算管理**: 动态分配 token 给不同优先级的内容 -4. **多语言支持**: 支持 60+ 种编程语言 -5. **Tree-sitter 解析**: 精确理解代码结构 -6. **流式响应**: 实时返回补全结果 diff --git a/plans/ghost-text-markdown-rendering.md b/plans/ghost-text-markdown-rendering.md deleted file mode 100644 index 9a2a686..0000000 --- a/plans/ghost-text-markdown-rendering.md +++ /dev/null @@ -1,157 +0,0 @@ -# 虚拟文本 Markdown 渲染解决方案 - -## 问题分析 - -当前虚拟文本(灰色字)无法正确渲染 Markdown 和换行符,根本原因是: - -1. **纯文本插入**:`insertGhostText` 使用 `tr.insertText()` 直接插入纯文本 -2. **绕过解析器**:文本未经过 Milkdown 的 Markdown 解析流程 -3. **节点结构错误**:`\n` 字符被当作普通字符,而非创建新段落节点 - -## 解决方案架构 - -```mermaid -flowchart TB - subgraph 当前流程 - A1[LLM 返回 Markdown] --> B1[insertText 直接插入] - B1 --> C1[添加 copilot_ghost mark] - C1 --> D1[显示为灰色纯文本] - end - - subgraph 新流程 - A2[LLM 返回 Markdown] --> B2[调用 parserCtx 解析] - B2 --> C2[生成 ProseMirror 节点] - C2 --> D2[为所有节点添加 ghost 属性] - D2 --> E2[插入到文档] - E2 --> F2[显示为格式化灰色文本] - end - - style D1 fill:#f99 - style F2 fill:#9f9 -``` - -## 技术方案 - -### 方案一:使用 Milkdown Parser 解析(推荐) - -**优点**: -- 完整支持 Markdown 语法 -- 与编辑器行为一致 -- 自动处理换行 - -**实现步骤**: - -1. 获取 `parserCtx` 从 Milkdown 上下文 -2. 使用 parser 将 Markdown 解析为 ProseMirror Fragment -3. 遍历所有节点,添加 `copilot_ghost` mark -4. 使用 `tr.replaceWith()` 插入节点 - -### 方案二:使用 Decoration API(备选) - -**优点**: -- 不修改实际文档内容 -- 更轻量级 - -**缺点**: -- 实现复杂 -- 可能与某些功能冲突 - -## 详细实现计划 - -### 步骤 1:修改 copilotPlugin.ts - -需要修改以下部分: - -```typescript -// 新增导入 -import { parserCtx } from '@milkdown/kit/core' - -// 修改 insertGhostText 函数 -async function insertGhostText(view: EditorView, suggestion: string, from: number) { - if (!currentCtx || !suggestion) return - - const schema = view.state.schema - const markType = schema.marks.copilot_ghost - - if (!markType) return - - // 使用 parser 解析 Markdown - const parser = currentCtx.get(parserCtx) - const doc = await parser(suggestion) - - if (!doc) return - - // 为所有文本节点添加 ghost mark - const ghostDoc = doc.descendants((node, pos) => { - if (node.isText) { - // 添加 mark - } - }) - - // 插入节点 - const tr = view.state.tr - tr.replaceWith(from, from, ghostDoc.content) - tr.setMeta(COPILOT_PLUGIN_KEY, { from, to: from + doc.content.size, suggestion }) - view.dispatch(tr) -} -``` - -### 步骤 2:处理换行符 - -换行符处理策略: - -| 换行类型 | 处理方式 | -|---------|---------| -| 单个 `\n` | 创建 `hard_break` 节点 | -| 双个 `\n\n` | 创建新段落节点 | -| 列表项换行 | 创建新列表项节点 | - -### 步骤 3:样式处理 - -需要修改 CSS 以支持格式化的虚拟文本: - -```css -.copilot-ghost-text { - color: #999; - opacity: 0.6; - pointer-events: none; -} - -/* 虚拟文本内的格式化元素 */ -.copilot-ghost-text strong, -.copilot-ghost-text em, -.copilot-ghost-text code { - opacity: inherit; -} -``` - -### 步骤 4:状态管理 - -需要跟踪虚拟节点的范围,以便: -- Tab 键接受时正确移除 mark -- 用户输入时正确清除虚拟内容 -- 导出时正确处理虚拟文本 - -## 文件修改清单 - -| 文件 | 修改内容 | -|------|---------| -| `src/plugins/copilotPlugin.ts` | 重构 insertGhostText,添加解析逻辑 | -| `src/components/MilkdownEditor.vue` | 更新 CSS 样式 | -| `src/plugins/types.ts` | 可能需要更新类型定义 | - -## 风险与注意事项 - -1. **性能考虑**:解析 Markdown 可能有延迟,需要考虑用户体验 -2. **嵌套处理**:复杂的 Markdown 结构(如嵌套列表)需要特殊处理 -3. **撤销/重做**:确保虚拟文本的接受/拒绝正确处理 undo stack -4. **光标位置**:插入多段落内容后光标位置需要正确设置 - -## 验收标准 - -- [ ] Markdown 语法正确渲染(粗体、斜体、代码等) -- [ ] 换行符正确转换为段落 -- [ ] Tab 键接受功能正常 -- [ ] Escape 键拒绝功能正常 -- [ ] 导出时虚拟文本正确处理 -- [ ] 性能无明显下降 diff --git a/plans/image-button-plan.md b/plans/image-button-plan.md deleted file mode 100644 index b39d27e..0000000 --- a/plans/image-button-plan.md +++ /dev/null @@ -1,269 +0,0 @@ -# Image Button Implementation Plan - -## Overview - -Add an image button to the MilkdownEditor that allows users to insert images at the cursor position. The button will provide a dropdown menu with two options: upload local file or input image URL. - -## Current Architecture Analysis - -### Existing Image Handling - -The editor already has image support through `@milkdown/crepe`: - -```javascript -// From MilkdownEditor.vue lines 217-231 -features: { - [Crepe.Feature.Latex]: true, - [Crepe.Feature.ImageBlock]: true, -}, -featureConfigs: { - [Crepe.Feature.ImageBlock]: { - onUpload: (file) => { - const objectUrl = URL.createObjectURL(file) - objectUrls.add(objectUrl) - performOCR(file, objectUrl) - return objectUrl - } - } -} -``` - -### Editor Access Pattern - -The code uses `editorViewCtx` to access the ProseMirror editor view: - -```javascript -crepe.editor.action((ctx) => { - const view = ctx.get(editorViewCtx) - // manipulate editor state -}) -``` - -## Implementation Plan - -### 1. Template Changes - -Add new button with dropdown menu in the `action-buttons` section: - -```html - -

- - - -
- - -
-
- - - - - -
-
- - - -
-
-``` - -### 2. Script Changes - -Add new refs and methods: - -```javascript -// New refs -const imageInputRef = ref(null) -const showImageDropdown = ref(false) -const showUrlDialog = ref(false) -const imageUrl = ref('') - -// Toggle dropdown -const toggleImageDropdown = () => { - showImageDropdown.value = !showImageDropdown.value -} - -// Trigger file input -const triggerImageUpload = () => { - showImageDropdown.value = false - imageInputRef.value?.click() -} - -// Handle file upload - reuse existing onUpload logic -const handleImageUpload = async (event) => { - const file = event.target.files?.[0] - if (!file) return - - const objectUrl = URL.createObjectURL(file) - objectUrls.add(objectUrl) - performOCR(file, objectUrl) - - // Insert image at cursor - insertImageAtCursor(objectUrl) - event.target.value = '' -} - -// Insert image from URL -const insertImageFromUrl = () => { - if (!imageUrl.value.trim()) return - insertImageAtCursor(imageUrl.value.trim()) - imageUrl.value = '' - showUrlDialog.value = false -} - -// Core function: insert image at cursor position -const insertImageAtCursor = (src) => { - if (!crepe) return - - crepe.editor.action((ctx) => { - const view = ctx.get(editorViewCtx) - const { state } = view - const { selection, schema } = state - - // Get image node type from schema - const imageType = schema.nodes.image - if (!imageType) return - - // Create image node - const imageNode = imageType.create({ src }) - - // Create transaction to insert at cursor - const tr = state.tr - tr = tr.replaceSelectionWith(imageNode) - - view.dispatch(tr) - }) -} -``` - -### 3. Style Changes - -Add styles for dropdown and dialog: - -```css -/* Image button wrapper */ -.image-btn-wrapper { - position: relative; -} - -/* Dropdown menu */ -.image-dropdown { - position: absolute; - bottom: 100%; - right: 0; - margin-bottom: 8px; - background: #fff; - border: 1px solid #ddd; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.15); - overflow: hidden; - z-index: 10000; - min-width: 160px; -} - -.image-dropdown button { - display: block; - width: 100%; - padding: 10px 16px; - border: none; - background: none; - text-align: left; - cursor: pointer; - font-size: 14px; - color: #333; -} - -.image-dropdown button:hover { - background: #f5f5f5; -} - -/* URL dialog overlay */ -.url-dialog-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0,0,0,0.3); - display: flex; - align-items: center; - justify-content: center; - z-index: 10001; -} - -.url-dialog { - background: #fff; - padding: 20px; - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0,0,0,0.2); -} - -.url-dialog input { - width: 300px; - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 4px; - margin-bottom: 12px; -} - -.url-dialog button { - padding: 8px 16px; - margin-right: 8px; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; -} -``` - -## Workflow Diagram - -```mermaid -flowchart TD - A[Click Image Button] --> B{Toggle Dropdown} - B --> C[Show Dropdown Menu] - C --> D{User Choice} - D -->|Upload Local| E[Open File Picker] - D -->|From URL| F[Show URL Dialog] - E --> G[Select Image File] - G --> H[Create Object URL] - H --> I[Perform OCR] - I --> J[Insert Image at Cursor] - F --> K[Enter URL] - K --> L[Click Insert] - L --> J - J --> M[Image Appears in Editor] -``` - -## Key Implementation Notes - -1. **Reuse existing logic**: The `onUpload` callback logic for `Crepe.Feature.ImageBlock` should be reused for local file uploads to maintain consistency with OCR processing. - -2. **ProseMirror API**: Use `schema.nodes.image.create()` and `replaceSelectionWith()` to insert images at cursor position. - -3. **Click outside to close**: The dropdown should close when clicking outside. This can be achieved with a click-outside directive or by listening to document clicks. - -4. **Accessibility**: Ensure proper ARIA labels and keyboard navigation support. - -## Files to Modify - -- `src/components/MilkdownEditor.vue` - All changes will be in this single file - -## Dependencies - -No new dependencies required. All functionality uses existing: -- Vue 3 Composition API -- Milkdown/ProseMirror APIs -- Native browser APIs (URL.createObjectURL, FileReader) \ No newline at end of file diff --git a/plans/image-processing-plan.md b/plans/image-processing-plan.md deleted file mode 100644 index 50cda7e..0000000 --- a/plans/image-processing-plan.md +++ /dev/null @@ -1,79 +0,0 @@ -# 图片处理优化计划 - -## 需求概述 -1. 图片上传大小限制为100MB -2. 为每个图片做哈希,相同哈希不重复调用OCR -3. 上传图片时打断之前的ghost text - -## 需要修改的文件 - -### 1. `src/utils/ocrCache.js` - 扩展OCR缓存模块 - -**新增功能:** -- 图片哈希缓存:`imageHashCache` Map,用于存储 `hash -> ocrText` 的映射 -- 哈希计算函数:使用 `crypto.subtle.digest('SHA-256', imageBytes)` 计算哈希 -- 哈希检查函数:在OCR前检查哈希是否已存在 -- 100MB大小限制常量 - -```javascript -// 新增 -export const IMAGE_SIZE_LIMIT = 100 * 1024 * 1024 // 100MB - -export async function calculateImageHash(imageBytes) { - const hashBuffer = await crypto.subtle.digest('SHA-256', imageBytes) - const hashArray = Array.from(new Uint8Array(hashBuffer)) - return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') -} - -export function getOcrByHash(hash) { - return imageHashCache.get(hash) || '' -} - -export function setOcrByHash(hash, text) { - imageHashCache.set(hash, text) -} -``` - -### 2. `src/components/MilkdownEditor.vue` - 修改图片上传逻辑 - -**修改点:** - -1. **`handleImageUpload` 函数 (约第392行)** - - 添加文件大小检查,超过100MB则提示错误 - - 计算图片哈希,检查是否已存在OCR结果 - - 上传图片前调用 `clearGhostSuggestion` 打断现有ghost text - -2. **`performOCR` 函数 (约第212行)** - - 接收哈希参数,OCR完成后存储到哈希缓存 - -3. **Milkdown `onUpload` 回调 (约第267行)** - - 同样添加大小限制和哈希检查 - -### 3. `src/plugins/copilotPlugin.ts` - 可能需要导出清除函数 - -- 确保 `clearGhostSuggestion` 可以被外部调用(目前已导出) - -## 实现步骤 - -```mermaid -graph TD - A[用户上传图片] --> B{文件大小 <= 100MB?} - B -->|否| C[提示文件过大错误] - B -->|是| D[计算图片哈希] - D --> E{哈希已存在OCR结果?} - E -->|是| F[直接使用缓存的OCR结果] - E -->|否| G[调用OCR API] - G --> H[存储OCR结果到哈希缓存] - F --> I[打断现有ghost text] - H --> I - I --> J[插入图片到编辑器] -``` - -## 关键代码修改位置 - -| 文件 | 函数/位置 | 修改内容 | -|------|-----------|----------| -| `src/utils/ocrCache.js` | 新增 | 添加哈希相关函数和常量 | -| `src/components/MilkdownEditor.vue` | `handleImageUpload` | 添加大小检查、哈希检查、打断ghost text | -| `src/components/MilkdownEditor.vue` | `performOCR` | 接收哈希参数 | -| `src/components/MilkdownEditor.vue` | `onUpload` | Milkdown上传回调添加同样逻辑 | diff --git a/plans/refactor-backend.md b/plans/refactor-backend.md deleted file mode 100644 index 5c1613a..0000000 --- a/plans/refactor-backend.md +++ /dev/null @@ -1,77 +0,0 @@ -# 重构计划:统一 backend/llm.py 和 backend/main.py - -## 目标 - -消除 `llm.py` 和 `main.py` 之间的代码冗余,建立清晰的职责分离。 - -## 当前问题 - -```mermaid -graph LR - A[llm.py] -->|流式调用| B[Ollama API] - C[main.py] -->|非流式调用| B - A -.->|未被使用| D[❌ 冗余] -``` - -## 重构后架构 - -```mermaid -graph LR - A[main.py] -->|导入调用| B[llm.py] - B -->|非流式调用| C[Ollama API] - A --> D[FastAPI 路由处理] -``` - -## 具体步骤 - -### 步骤 1:重构 llm.py - -将 `stream_openai` 函数改为非流式调用,参考 main.py 的实现: - -```python -# 新的 llm.py 结构 -async def call_ollama(prompt: str) -> dict: - # 非流式调用 - # 返回 {"content": str, "thinking": str} -``` - -关键改动: -- 移除 `AsyncGenerator` 类型,改为返回 `dict` -- 设置 `stream=False` -- 使用 `temperature=0.2`(与 main.py 一致) -- 返回 content 和 thinking 字段 - -### 步骤 2:重构 main.py - -导入并使用 llm.py: - -```python -# main.py 改动 -from llm import call_ollama - -@app.post("/v1/completions") -async def create_completion(request: CompletionRequest): - prompt = build_prompt(request.prefix, request.suffix) - result = await call_ollama(prompt) - # 使用 result["content"] 和 result["thinking"] -``` - -删除的代码: -- 直接导入 `ollama` 的代码 -- 重复创建 `AsyncClient` 的代码 -- 重复的 API 调用逻辑 -- 重复的环境变量读取 - -### 步骤 3:清理冗余 - -- 移除 llm.py 中不再需要的 `AsyncGenerator` 导入 -- 移除 main.py 中重复的环境变量定义 -- 确保调试日志保留但不过度 - -## 文件职责划分 - -| 文件 | 职责 | -|------|------| -| `llm.py` | Ollama API 调用封装、模型配置 | -| `main.py` | FastAPI 路由、请求解析、响应格式化 | -| `prompt.py` | Prompt 构建逻辑 |