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 构建逻辑 |