chore(config): update default Ollama model to gpt-oss:120b
Remove obsolete planning documents that are no longer needed: - copilot-prompt-system-analysis.md - ghost-text-markdown-rendering.md - image-button-plan.md - image-processing-plan.md - refactor-backend.md
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
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')
|
OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.0.120:11434')
|
||||||
VLM_MODEL = os.getenv('VLM_MODEL', 'qwen3-vl:30b')
|
VLM_MODEL = os.getenv('VLM_MODEL', 'qwen3-vl:30b')
|
||||||
|
|
||||||
|
|||||||
@@ -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 = object> = P & Readonly<PromptAttributes & { children?: PromptComponentChildren }>;
|
|
||||||
|
|
||||||
// 组件上下文,提供状态管理
|
|
||||||
export interface ComponentContext {
|
|
||||||
useState<S>(initialState: S): [S, Dispatch<StateUpdater<S>>];
|
|
||||||
useData<T>(typePredicate: TypePredicate<T>, consumer: DataConsumer<T>): void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 默认提示词组件结构
|
|
||||||
|
|
||||||
来源: [`lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx`](../completions-sample-code/lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx)
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
function defaultCompletionsPrompt(accessor: ServicesAccessor) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CompletionsContext>
|
|
||||||
<DocumentMarker tdms={tdms} weight={0.7} />
|
|
||||||
<Traits weight={0.6} />
|
|
||||||
<Diagnostics tdms={tdms} weight={0.65} />
|
|
||||||
<CodeSnippets tdms={tdms} weight={0.9} />
|
|
||||||
<SimilarFiles tdms={tdms} instantiationService={instantiationService} weight={0.8} />
|
|
||||||
<RecentEdits tdms={tdms} recentEditsProvider={recentEditsProvider} weight={0.99} />
|
|
||||||
</CompletionsContext>
|
|
||||||
<CurrentFile weight={1} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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>{text}</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 <Text>{suffixToUse}</Text>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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<string>, b: Set<string>): number;
|
|
||||||
|
|
||||||
// 分词器:将代码转换为 token 集合
|
|
||||||
class Tokenizer {
|
|
||||||
tokenize(a: string): Set<string> {
|
|
||||||
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. **流式响应**: 实时返回补全结果
|
|
||||||
@@ -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 键拒绝功能正常
|
|
||||||
- [ ] 导出时虚拟文本正确处理
|
|
||||||
- [ ] 性能无明显下降
|
|
||||||
@@ -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
|
|
||||||
<!-- Image button with dropdown -->
|
|
||||||
<div class="image-btn-wrapper">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="action-btn"
|
|
||||||
aria-label="Insert Image"
|
|
||||||
title="Insert Image"
|
|
||||||
@click="toggleImageDropdown"
|
|
||||||
>
|
|
||||||
<!-- Image SVG icon -->
|
|
||||||
<svg>...</svg>
|
|
||||||
<span class="btn-tooltip">Insert Image</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Dropdown menu -->
|
|
||||||
<div v-if="showImageDropdown" class="image-dropdown">
|
|
||||||
<button @click="triggerImageUpload">Upload Local Image</button>
|
|
||||||
<button @click="showUrlDialog = true">Insert from URL</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden file input for image upload -->
|
|
||||||
<input type="file" ref="imageInputRef" @change="handleImageUpload" accept="image/*" style="display:none">
|
|
||||||
|
|
||||||
<!-- URL input dialog -->
|
|
||||||
<div v-if="showUrlDialog" class="url-dialog-overlay" @click.self="showUrlDialog = false">
|
|
||||||
<div class="url-dialog">
|
|
||||||
<input v-model="imageUrl" placeholder="Enter image URL" />
|
|
||||||
<button @click="insertImageFromUrl">Insert</button>
|
|
||||||
<button @click="showUrlDialog = false">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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)
|
|
||||||
@@ -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上传回调添加同样逻辑 |
|
|
||||||
@@ -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 构建逻辑 |
|
|
||||||
Reference in New Issue
Block a user