将 AI CLI 响应与 Python PySide6 桌面应用程序进行并排比较

在只能一次测试一个AI编码助手的情况下,选择合适的工具并非易事。安装了Qwen Code、GitHub Copilot CLI、OpenCode和Gemini CLI的开发者需要一种快速的方法,将相同的提示信息同时发送给它们,并并行读取结果。本项目使用PySide6构建了一个Python桌面应用程序,它能够实现这一目标——输入一个提示信息,即可同时输出四个实时响应面板。

你将构建:一个 PySide6 桌面 GUI,它可以动态检测已安装的 AI CLI 工具,同时向所有这些工具发送一个提示,并将每个响应流式传输到它自己的面板中,并提供 Markdown/渲染 HTML 切换选项以便于阅读。

AI CLI 对比

先决条件

  • Python 3.10+
  • PySide6pip install PySide6
  • markdown ( pip install markdown)
  • 至少安装并验证了以下一种 AI CLI 工具:
命令行界面 安装命令 文档
Qwen Code npm install -g @qwen-code/qwen-code github.com/QwenLM/qwen-code
GitHub Copilot CLI npm install -g @github/copilot docs.github.com
OpenCode npm install -g opencode-ai opencode.ai
Gemini CLI npm install -g @google/gemini-cli github.com/google-gemini/gemini-cli

安装依赖项

创建requirements.txt包含两个 Python 依赖项的文件并安装它们:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>PySide6>=6.6.0
markdown>=3.5
</code></span></span></span>
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>pip <span style="color:#336666">install</span> <span style="color:#330099"><strong>-r</strong></span> requirements.txt
</code></span></span></span>

定义 CLI 工具注册表

每个 AI CLI 都有自己的二进制名称、非交互式调用标志和品牌颜色。应用程序将这些信息存储为字典列表,以便在启动时动态生成面板。

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>CLI_DEFS <span style="color:#555555">=</span> [
    {
        <span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">qwen</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">Qwen Code</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>: <span style="color:#006699"><strong>lambda</strong></span> p: [<span style="color:#cc3300">"</span><span style="color:#cc3300">qwen</span><span style="color:#cc3300">"</span>, p],
        <span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">#4A9EEB</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">https://github.com/QwenLM/qwen-code</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">npm install -g @qwen-code/qwen-code</span><span style="color:#cc3300">"</span>,
    },
    {
        <span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">copilot</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">GitHub Copilot CLI</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>: <span style="color:#006699"><strong>lambda</strong></span> p: [_REAL_COPILOT, <span style="color:#cc3300">"</span><span style="color:#cc3300">-p</span><span style="color:#cc3300">"</span>, p] <span style="color:#006699"><strong>if</strong></span> _REAL_COPILOT <span style="color:#006699"><strong>else</strong></span> [<span style="color:#cc3300">"</span><span style="color:#cc3300">copilot</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">-p</span><span style="color:#cc3300">"</span>, p],
        <span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">#9B6FE8</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">https://docs.github.com/en/copilot/github-copilot-in-the-cli</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">gh extension install github/gh-copilot</span><span style="color:#cc3300">"</span>,
    },
    {
        <span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">opencode</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">OpenCode</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>: <span style="color:#006699"><strong>lambda</strong></span> p: [<span style="color:#cc3300">"</span><span style="color:#cc3300">opencode</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">run</span><span style="color:#cc3300">"</span>, p],
        <span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">#E8623A</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">https://opencode.ai</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">npm install -g opencode-ai</span><span style="color:#cc3300">"</span>,
    },
    {
        <span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">gemini</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">Gemini CLI</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>: <span style="color:#006699"><strong>lambda</strong></span> p: [<span style="color:#cc3300">"</span><span style="color:#cc3300">gemini</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">-p</span><span style="color:#cc3300">"</span>, p],
        <span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">#34A853</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">https://github.com/google-gemini/gemini-cli</span><span style="color:#cc3300">"</span>,
        <span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">npm install -g @google/gemini-cli</span><span style="color:#cc3300">"</span>,
    },
]
</code></span></span></span>

每个命令行界面的非交互式调用语法都不同:

命令行界面 命令模式
Qwen Code qwen "<prompt>"
GitHub Copilot CLI copilot -p "<prompt>"
OpenCode opencode run "<prompt>"
Gemini CLI gemini -p "<prompt>"

处理 Windows 子进程

在 Windows 系统中,通过 npm 安装的 CLI 工具是.CMD包装脚本,subprocess.Popen不能直接运行——它们需要通过 . 来执行cmd /c。该build_cmd辅助工具会透明地处理这一过程,同时跳过包装脚本,直接运行真正的.exe二进制文件:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>_IS_WINDOWS <span style="color:#555555">=</span> platform.<span style="color:#cc00ff">system</span>() <span style="color:#555555">==</span> <span style="color:#cc3300">"</span><span style="color:#cc3300">Windows</span><span style="color:#cc3300">"</span>

<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">build_cmd</span>(args: <span style="color:#336666">list</span>[<span style="color:#336666">str</span>]) <span style="color:#555555">-></span> <span style="color:#336666">list</span>[<span style="color:#336666">str</span>]:
    <span style="color:#cc3300">"""</span><span style="color:#cc3300">On Windows .CMD/.BAT scripts cannot be spawned directly; wrap with cmd /c.
    If the binary is already a .exe, no wrapping is needed.</span><span style="color:#cc3300">"""</span>
    <span style="color:#006699"><strong>if</strong></span> _IS_WINDOWS <span style="color:#000000"><strong>and</strong></span> <span style="color:#000000"><strong>not</strong></span> args[<span style="color:#ff6600">0</span>].<span style="color:#cc00ff">lower</span>().<span style="color:#cc00ff">endswith</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">.exe</span><span style="color:#cc3300">"</span>):
        <span style="color:#006699"><strong>return</strong></span> [<span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">/c</span><span style="color:#cc3300">"</span>] <span style="color:#555555">+</span> args
    <span style="color:#006699"><strong>return</strong></span> args
</code></span></span></span>

GitHub Copilot CLI 还有个额外的问题:VS Code 扩展会安装一个 PowerShell 包装器(copilot.ps1),该包装器使用执行交互式版本检查Read-Host。当stdin版本为 0 时/dev/null,这会导致进程立即退出,而不会运行查询。该函数通过暂时从 PATH 中移除包装器的目录_find_real_copilot来解析实际的二进制文件:copilot.exe

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">_find_real_copilot</span>() <span style="color:#555555">-></span> <span style="color:#336666">str</span> <span style="color:#555555">|</span> <span style="color:#336666">None</span>:
    <span style="color:#cc3300">"""</span><span style="color:#cc3300">Find the real copilot binary, skipping the VS Code PS1/BAT wrapper.</span><span style="color:#cc3300">"""</span>
    wrapper_path <span style="color:#555555">=</span> shutil.<span style="color:#cc00ff">which</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">copilot</span><span style="color:#cc3300">"</span>)
    <span style="color:#006699"><strong>if</strong></span> <span style="color:#000000"><strong>not</strong></span> wrapper_path:
        <span style="color:#006699"><strong>return</strong></span> <span style="color:#336666">None</span>

    <span style="color:#006699"><strong>if</strong></span> wrapper_path.<span style="color:#cc00ff">lower</span>().<span style="color:#cc00ff">endswith</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">.exe</span><span style="color:#cc3300">"</span>):
        <span style="color:#006699"><strong>return</strong></span> wrapper_path

    wrapper_dir <span style="color:#555555">=</span> os.path.<span style="color:#cc00ff">dirname</span>(os.path.<span style="color:#cc00ff">abspath</span>(wrapper_path))
    filtered <span style="color:#555555">=</span> [p <span style="color:#006699"><strong>for</strong></span> p <span style="color:#000000"><strong>in</strong></span> os.environ.<span style="color:#cc00ff">get</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">PATH</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">""</span>).<span style="color:#cc00ff">split</span>(os.pathsep)
                <span style="color:#006699"><strong>if</strong></span> os.path.<span style="color:#cc00ff">normcase</span>(os.path.<span style="color:#cc00ff">abspath</span>(p)) <span style="color:#555555">!=</span> os.path.<span style="color:#cc00ff">normcase</span>(wrapper_dir)]
    old_path <span style="color:#555555">=</span> os.environ[<span style="color:#cc3300">"</span><span style="color:#cc3300">PATH</span><span style="color:#cc3300">"</span>]
    os.environ[<span style="color:#cc3300">"</span><span style="color:#cc3300">PATH</span><span style="color:#cc3300">"</span>] <span style="color:#555555">=</span> os.pathsep.<span style="color:#cc00ff">join</span>(filtered)
    <span style="color:#006699"><strong>try</strong></span>:
        real <span style="color:#555555">=</span> shutil.<span style="color:#cc00ff">which</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">copilot</span><span style="color:#cc3300">"</span>)
    <span style="color:#006699"><strong>finally</strong></span>:
        os.environ[<span style="color:#cc3300">"</span><span style="color:#cc3300">PATH</span><span style="color:#cc3300">"</span>] <span style="color:#555555">=</span> old_path
    <span style="color:#006699"><strong>return</strong></span> real
</code></span></span></span>

使用后台 QThread 工作线程流式传输 CLI 输出

每个 CLI 都在各自的服务器上运行,QThread以保持用户界面的响应速度。该类CLIWorker会生成一个子进程,逐行读取标准输出,去除 ANSI 转义码,并将每个数据块作为 Qt 信号发出:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>_ANSI_RE <span style="color:#555555">=</span> re.<span style="color:#cc00ff">compile</span>(r<span style="color:#cc3300">"</span><span style="color:#cc3300">\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])</span><span style="color:#cc3300">"</span>)

<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">strip_ansi</span>(text: <span style="color:#336666">str</span>) <span style="color:#555555">-></span> <span style="color:#336666">str</span>:
    <span style="color:#006699"><strong>return</strong></span> _ANSI_RE.<span style="color:#cc00ff">sub</span>(<span style="color:#cc3300">""</span>, text)

<span style="color:#006699"><strong>class</strong></span> <span style="color:#00aa88"><strong>CLIWorker</strong></span>(QThread):
    output_chunk <span style="color:#555555">=</span> <span style="color:#00aa88"><strong>Signal</strong></span>(<span style="color:#336666">str</span>)
    finished <span style="color:#555555">=</span> <span style="color:#00aa88"><strong>Signal</strong></span>(<span style="color:#336666">bool</span>, <span style="color:#336666">str</span>)

    <span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">__init__</span>(self, cmd: <span style="color:#336666">list</span>[<span style="color:#336666">str</span>]):
        <span style="color:#cc00ff">super</span>().<span style="color:#cc00ff">__init__</span>()
        self._cmd <span style="color:#555555">=</span> cmd
        self._cancelled <span style="color:#555555">=</span> <span style="color:#336666">False</span>
        self._process: subprocess.Popen <span style="color:#555555">|</span> <span style="color:#336666">None</span> <span style="color:#555555">=</span> <span style="color:#336666">None</span>

    <span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">cancel</span>(self):
        self._cancelled <span style="color:#555555">=</span> <span style="color:#336666">True</span>
        <span style="color:#006699"><strong>if</strong></span> self._process <span style="color:#000000"><strong>and</strong></span> self._process.<span style="color:#cc00ff">poll</span>() <span style="color:#000000"><strong>is</strong></span> <span style="color:#336666">None</span>:
            self._process.<span style="color:#cc00ff">kill</span>()

    <span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">run</span>(self):
        <span style="color:#006699"><strong>try</strong></span>:
            self._process <span style="color:#555555">=</span> subprocess.<span style="color:#00aa88"><strong>Popen</strong></span>(
                self._cmd,
                stdout<span style="color:#555555">=</span>subprocess.PIPE,
                stderr<span style="color:#555555">=</span>subprocess.STDOUT,
                stdin<span style="color:#555555">=</span>subprocess.DEVNULL,
                text<span style="color:#555555">=</span><span style="color:#336666">True</span>,
                encoding<span style="color:#555555">=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">utf-8</span><span style="color:#cc3300">"</span>,
                errors<span style="color:#555555">=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">replace</span><span style="color:#cc3300">"</span>,
            )
            <span style="color:#006699"><strong>for</strong></span> line <span style="color:#000000"><strong>in</strong></span> <span style="color:#cc00ff">iter</span>(self._process.stdout.readline, <span style="color:#cc3300">""</span>):
                <span style="color:#006699"><strong>if</strong></span> self._cancelled:
                    self._process.<span style="color:#cc00ff">kill</span>()
                    self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">False</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">Cancelled</span><span style="color:#cc3300">"</span>)
                    <span style="color:#006699"><strong>return</strong></span>
                self.output_chunk.<span style="color:#cc00ff">emit</span>(<span style="color:#cc00ff">strip_ansi</span>(line))
            self._process.stdout.<span style="color:#cc00ff">close</span>()
            self._process.<span style="color:#cc00ff">wait</span>()
            rc <span style="color:#555555">=</span> self._process.returncode
            <span style="color:#006699"><strong>if</strong></span> rc <span style="color:#555555">==</span> <span style="color:#ff6600">0</span>:
                self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">True</span>, <span style="color:#cc3300">""</span>)
            <span style="color:#006699"><strong>else</strong></span>:
                self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">False</span>, f<span style="color:#cc3300">"</span><span style="color:#cc3300">Process exited with code </span><span style="color:#aa0000">{</span>rc<span style="color:#aa0000">}</span><span style="color:#cc3300">"</span>)
        <span style="color:#006699"><strong>except</strong></span> <span style="color:#336666">FileNotFoundError</span>:
            self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">False</span>, f<span style="color:#cc3300">"</span><span style="color:#cc3300">Command not found: </span><span style="color:#aa0000">{</span>self._cmd[<span style="color:#ff6600">0</span>]<span style="color:#aa0000">}</span><span style="color:#cc3300">"</span>)
        <span style="color:#006699"><strong>except</strong></span> <span style="color:#336666">Exception</span> <span style="color:#006699"><strong>as</strong></span> exc:
            self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">False</span>, <span style="color:#cc00ff">str</span>(exc))
</code></span></span></span>

关键设计决策:

  • stdin=subprocess.DEVNULL防止 CLI 工具在交互式提示符处阻塞。
  • stderr=subprocess.STDOUT将错误输出合并到面板中,这样就不会悄无声息地丢失任何信息。
  • 逐行迭代(readline)实现了实时流式传输,而无需等待进程完成。

使用 Markdown 渲染构建每个 CLI 的响应面板

每个元素CLIPanelQFrame包含一个头部(命令行名称、切换按钮、状态指示器)和两个堆叠的内容区域——一个等宽纯文本视图和一个渲染后的 HTML 视图。切换按钮用于在两者之间切换:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">_on_toggle</span>(self, checked: <span style="color:#336666">bool</span>):
    self._rendered_mode <span style="color:#555555">=</span> checked
    <span style="color:#006699"><strong>if</strong></span> checked:
        self._toggle_btn.<span style="color:#cc00ff">setText</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">✎ Markdown</span><span style="color:#cc3300">"</span>)
        self.render_area.<span style="color:#cc00ff">setHtml</span>(<span style="color:#cc00ff">markdown_to_html</span>(self._raw_text))
        self.text_area.<span style="color:#cc00ff">setVisible</span>(<span style="color:#336666">False</span>)
        self.render_area.<span style="color:#cc00ff">setVisible</span>(<span style="color:#336666">True</span>)
    <span style="color:#006699"><strong>else</strong></span>:
        self._toggle_btn.<span style="color:#cc00ff">setText</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">⟳ Render</span><span style="color:#cc3300">"</span>)
        self.render_area.<span style="color:#cc00ff">setVisible</span>(<span style="color:#336666">False</span>)
        self.text_area.<span style="color:#cc00ff">setVisible</span>(<span style="color:#336666">True</span>)
</code></span></span></span>

Markdown 到 HTML 的转换使用了 Pythonmarkdown库,并扩展了代码块、表格和换行符的功能,同时还使用了深色主题的 CSS 样式表:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>_MD_EXTENSIONS <span style="color:#555555">=</span> [<span style="color:#cc3300">"</span><span style="color:#cc3300">fenced_code</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">tables</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">nl2br</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">sane_lists</span><span style="color:#cc3300">"</span>]

<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">markdown_to_html</span>(text: <span style="color:#336666">str</span>) <span style="color:#555555">-></span> <span style="color:#336666">str</span>:
    body <span style="color:#555555">=</span> md_lib.<span style="color:#cc00ff">markdown</span>(text, extensions<span style="color:#555555">=</span>_MD_EXTENSIONS)
    <span style="color:#006699"><strong>return</strong></span> f<span style="color:#cc3300">"""</span><span style="color:#cc3300"><!DOCTYPE html><html><head><meta charset=</span><span style="color:#cc3300">'</span><span style="color:#cc3300">utf-8</span><span style="color:#cc3300">'</span><span style="color:#cc3300">></span><span style="color:#aa0000">{</span>_MD_CSS<span style="color:#aa0000">}</span><span style="color:#cc3300"></head>
<body></span><span style="color:#aa0000">{</span>body<span style="color:#aa0000">}</span><span style="color:#cc3300"></body></html></span><span style="color:#cc3300">"""</span>
</code></span></span></span>

如果未安装 CLI,面板将显示一个带有可点击下载 URL 和建议安装命令的样式信息卡,而不是空白文本区域:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">_show_download_info</span>(self):
    self.<span style="color:#cc00ff">_set_status</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">● Not installed</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">#f44336</span><span style="color:#cc3300">"</span>)
    url <span style="color:#555555">=</span> self.cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>]
    hint <span style="color:#555555">=</span> self.cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>]
    name <span style="color:#555555">=</span> self.cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>]
    color <span style="color:#555555">=</span> self.cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>]
    html <span style="color:#555555">=</span> f<span style="color:#cc3300">"""</span><span style="color:#cc3300">
<div style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">color:#e0e0e0; font-family:</span><span style="color:#cc3300">'</span><span style="color:#cc3300">Segoe UI</span><span style="color:#cc3300">'</span><span style="color:#cc3300">,sans-serif; padding:16px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">>
  <p style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">font-size:16px; font-weight:bold; color:</span><span style="color:#aa0000">{</span>color<span style="color:#aa0000">}</span><span style="color:#cc3300">;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">></span><span style="color:#aa0000">{</span>name<span style="color:#aa0000">}</span><span style="color:#cc3300"></p>
  <p style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">color:#f44336; font-size:13px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">>&#9888; Not installed on this system</p>
  <p style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">font-size:12px; color:#aaa; margin-top:16px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">>Download / Documentation:</p>
  <p style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">margin-top:4px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">>
    <a href=</span><span style="color:#cc3300">"</span><span style="color:#aa0000">{</span>url<span style="color:#aa0000">}</span><span style="color:#cc3300">"</span><span style="color:#cc3300"> style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">color:#4A9EEB; font-size:13px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">></span><span style="color:#aa0000">{</span>url<span style="color:#aa0000">}</span><span style="color:#cc3300"></a>
  </p>
</div>
</span><span style="color:#cc3300">"""</span>
    self.text_area.<span style="color:#cc00ff">setHtml</span>(html)
</code></span></span></span>

组装主窗口和提示栏

MainWindow会动态检查已安装的命令行界面 (CLI) shutil.which,为每个 CLI 创建一个CLIPanel列表,并将它们水平排列QSplitter。底部的提示输入区域同时支持“发送”按钮和Ctrl+Enter快捷键:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#0099ff"><em># ── CLI panels ───────────────────────────────────────────────────────
</em></span>splitter <span style="color:#555555">=</span> <span style="color:#00aa88"><strong>QSplitter</strong></span>(Qt.Orientation.Horizontal)

available_count <span style="color:#555555">=</span> <span style="color:#ff6600">0</span>
<span style="color:#006699"><strong>for</strong></span> cli_def <span style="color:#000000"><strong>in</strong></span> CLI_DEFS:
    avail <span style="color:#555555">=</span> shutil.<span style="color:#cc00ff">which</span>(cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>]) <span style="color:#000000"><strong>is</strong></span> <span style="color:#000000"><strong>not</strong></span> <span style="color:#336666">None</span>
    panel <span style="color:#555555">=</span> <span style="color:#00aa88"><strong>CLIPanel</strong></span>(cli_def, avail)
    splitter.<span style="color:#cc00ff">addWidget</span>(panel)
    self._panels.<span style="color:#cc00ff">append</span>(panel)
    <span style="color:#006699"><strong>if</strong></span> avail:
        available_count <span style="color:#555555">+=</span> <span style="color:#ff6600">1</span>
</code></span></span></span>

当用户点击“发送”按钮时,提示文本会同时发送到所有已安装的面板:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">_send_prompt</span>(self):
    prompt <span style="color:#555555">=</span> self._prompt_input.<span style="color:#cc00ff">toPlainText</span>().<span style="color:#cc00ff">strip</span>()
    <span style="color:#006699"><strong>if</strong></span> <span style="color:#000000"><strong>not</strong></span> prompt:
        <span style="color:#006699"><strong>return</strong></span>
    self._prompt_input.<span style="color:#cc00ff">clear</span>()
    <span style="color:#006699"><strong>for</strong></span> panel <span style="color:#000000"><strong>in</strong></span> self._panels:
        panel.<span style="color:#cc00ff">start_query</span>(prompt)
</code></span></span></span>

Ctrl+Enter快捷键是通过QEvent对提示输入进行筛选来实现的:

<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">eventFilter</span>(self, obj, event):
    <span style="color:#006699"><strong>if</strong></span> obj <span style="color:#000000"><strong>is</strong></span> self._prompt_input <span style="color:#000000"><strong>and</strong></span> event.<span style="color:#cc00ff">type</span>() <span style="color:#555555">==</span> QEvent.Type.KeyPress:
        key_ev: QKeyEvent <span style="color:#555555">=</span> event
        ctrl <span style="color:#555555">=</span> Qt.KeyboardModifier.ControlModifier
        <span style="color:#cc00ff">if </span>(
            key_ev.<span style="color:#cc00ff">key</span>() <span style="color:#555555">==</span> Qt.Key.Key_Return
            <span style="color:#000000"><strong>and</strong></span> key_ev.<span style="color:#cc00ff">modifiers</span>() <span style="color:#555555">&</span> ctrl
        ):
            self.<span style="color:#cc00ff">_send_prompt</span>()
            <span style="color:#006699"><strong>return</strong></span> <span style="color:#336666">True</span>
    <span style="color:#006699"><strong>return</strong></span> <span style="color:#cc00ff">super</span>().<span style="color:#cc00ff">eventFilter</span>(obj, event)
</code></span></span></span>

源代码

https://github.com/yushulx/multi-ai-cli-comparison

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐