AI+ 项目代码解释、分析、纠错器

770 阅读3分钟

前言

AI 强势来袭,开发了一个 AI+ 项目代码分析的工具,实现 上传整个项目、上传单个文件、解释当前代码、评估代码质量、优化当前代码 能力

1111111.gif

1 下午2.44.16.jpg

技术栈

Electron + React + TypeScript + Express+ Openai + EventSource(流式渲染) + HttpProxyAgent + Zustand + MonacoEditor

核心技术点

1. 上传项目、文件并解析成树结构(Electron 主进程 -> 渲染进程通信)

渲染进程的事件

const handleOpenFolder / handleOpenFile = () => {
    window.electron.ipcRenderer.sendMessage('open-folder-dialog');
    window.electron.ipcRenderer.sendMessage('open-file-dialog');
};

打开文件夹打开文件 在主进程中的操作,区别在于 openDirectory 还是 openFile

const result = await dialog.showOpenDialog(mainWindow!, {
    properties: ['openDirectory'],
    properties: ['openFile'],
});

打开文件夹后,找到下面所有的文件,并梳理成树结构返回渲染进程

import { ipcMain, dialog, BrowserWindow } from 'electron';
import { readdir } from 'fs/promises';
ipcMain.on('open-folder-dialog', async () => {
    try {
      const result = await dialog.showOpenDialog(mainWindow!, {
        properties: ['openDirectory'],
      });
      if (!result.canceled && result.filePaths.length > 0) {
        const rootStructure = [
          {
            isRoot: true,
            key: result.filePaths[0],
            title: path.basename(selectedDirectory);,
            children: await readDirectoryAsync(selectedDirectory),
            currentOpenType: 'folder',
          },
        ];
        mainWindow?.webContents.send('init-tree-structure', rootStructure);
      }
    } catch (error) {
      console.error(error);
    }
});

// 找到下面所有的文件
export async function readDirectoryAsync(
  dirPath: string,
): Promise<Directory[]> {
  try {
    const items = await readdir(dirPath, { withFileTypes: true });
    const results: Directory[] = await Promise.all(
      items.map(async (item) => {
        const itemPath = path.join(dirPath, item.name);
        const isDirectory = item.isDirectory();

        return isDirectory
          ? {
              key: itemPath,
              title: item.name,
              children: await readDirectoryAsync(itemPath),
            }
          : { key: itemPath, title: item.name, isLeaf: true };
      }),
    );
    return results;
  } catch (error) {
    console.error(error);
  }
}
2. EventSource 流式渲染,实现数据实时传输,干货

2.1 renderer 渲染进程里抽象出一层(被调用)

import { EventStreamContentType, fetchEventSource } from '@fortaine/fetch-event-source';

// 检查是否需要流式传输
if (shouldStream) {
    // 用于存储完整的响应文本
    let responseText = '';
    // 用于存储未处理的剩余文本
    let remainText = '';
    // 标志流式传输是否完成
    let finished = false;

    // 动画函数,用于更新响应文本
    function animateResponseText() {
        // 如果传输完成或已中止,追加剩余文本并返回
        if (finished || controller.signal.aborted) {
            responseText += remainText;
            return;
        }

        // 如果还有剩余文本,按批次处理并更新响应文本
        if (remainText.length > 0) {
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
            const fetchText = remainText.slice(0, fetchCount);
            responseText += fetchText;
            remainText = remainText.slice(fetchCount);
            options.onUpdate?.(responseText, fetchText);
        }

        // 使用 requestAnimationFrame 调度下一次动画帧
        requestAnimationFrame(animateResponseText);
    }

    // 启动动画函数
    animateResponseText();

    // 完成函数,用于标记传输完成并调用完成回调
    const finish = () => {
        if (!finished) {
            finished = true;
            options.onFinish(responseText + remainText);
        }
    };

    // 设置中止信号的处理函数
    controller.signal.onabort = finish;

    // 使用 fetchEventSource 开始流式传输
    fetchEventSource(chatPath, {
        // 传递 payload 和配置
        ...chatPayload,
        // 打开连接时的处理函数
        async onopen(res) {
            // 清除请求超时定时器
            clearTimeout(requestTimeoutId);
            // 获取响应内容类型
            const contentType = res.headers.get('content-type');

            // 如果内容类型是纯文本,读取响应文本并完成传输
            if (contentType?.startsWith('text/plain')) {
                responseText = await res.clone().text();
                return finish();
            }

            // 检查响应状态和内容类型是否符合预期
            if (
                !res.ok ||
                !res.headers
                    .get('content-type')
                    ?.startsWith(EventStreamContentType) ||
                res.status !== 200
            ) {
                // 处理错误响应,获取额外信息
                const responseTexts = [responseText];
                let extraInfo = await res.clone().text();
                try {
                    const resJson = await res.clone().json();
                    extraInfo = prettyObject(resJson);
                } catch (e) {
                    console.log(e);
                }
                if (res.status === 401) {
                    responseTexts.push('401');
                }
                if (extraInfo) {
                    responseTexts.push(extraInfo);
                }
                responseText = responseTexts.join('\n\n');
                return finish();
            }
        },
        // 收到消息时的处理函数
        onmessage(msg) {
            // 如果消息是 '[DONE]' 或传输已经完成,结束传输
            if (msg.data === '[DONE]' || finished) {
                return finish();
            }
            const text = msg.data;
            try {
                // 解析消息内容,提取 delta 并追加到剩余文本
                const json = JSON.parse(text) as {
                    choices: Array<{
                        delta: {
                            content: string;
                        };
                    }>;
                };
                const delta = json.choices[0]?.delta?.content;
                if (delta) {
                    remainText += delta;
                }
            } catch (e) {
                console.error('[Request] parse error', text);
            }
        },
        // 连接关闭时的处理函数
        onclose() {
            finish();
        },
        // 发生错误时的处理函数
        onerror(e) {
            options.onError?.(e);
            throw e;
        },
        // 允许在窗口隐藏时继续保持连接
        openWhenHidden: true,
    });
}

2.2 main 主进程中启动一个服务

import express, { Express } from 'express';
import { HttpProxyAgent } from 'http-proxy-agent';
import fetch from 'electron-fetch';

const app = express();
app.use(cors());
app.use(express.json());

// 端口为你本机的网络代理端口
const agent = new HttpProxyAgent('http://127.0.0.1:7890');

app.post('/api/openai/v1/chat/completions', async (req, res) => {
  // 这是主进程要请求openai的地址
  const fetchUrl = `${baseUrl}/${path}`;
  const fetchOptions = {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'no-store',
      [authHeaderName]: authValue,
    },
    method: req.method,
    body: JSON.stringify(req.body),
    redirect: 'manual',
    duplex: 'half',
    signal: controller.signal,
    agent,
  };

  try {
    const response = await fetch(fetchUrl, fetchOptions);
    // 这里的流式响应设置
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    
    for await (const chunk of response.body) {
      res.write(chunk.toString());
    }
  } finally {
    clearTimeout(timeoutId);
  }
});
3. 全局数据流 zustand + monacoEditor

全局数据流 zustand 和 monacoEditor 编辑器在前端的使用就不过多赘述了,想看源码的同学 AI项目代码解释分析纠错器-GitHub