1. 从零实现 QPlainTextEdit 代码组件:为什么纯代码自定义比拖控件更靠谱
如果你正在用 Qt 写一个桌面 IDE、代码查看器或者带语法高亮的编辑器,大概率绕不开QPlainTextEdit。它比QTextEdit轻量,处理大文本时性能更好,但它默认不带行号、不带当前行高亮、也不带自动补全。这些能力都得自己写。很多人第一反应是拖一个QPlainTextEdit到.ui文件里,再在旁边摆一个QLabel或QWidget当行号栏,结果一滚动就错位,一换行就崩。纯代码自定义组件的好处就在这里:行号栏、高亮、滚动同步全部在一个类里闭环,逻辑可控,复用也方便。
这篇要交付的是一条完整路径:用纯代码方式继承QPlainTextEdit,实现行号显示、当前行高亮、鼠标点击定位、滚轮同步,再通过 TaoToken 的统一 Key 和 API 通道接入模型服务,让这个代码组件具备 AI 补全的调用能力。适合谁?适合已经会一点 Qt、写过QWidget子类、但还没把编辑器组件和 AI 服务打通的人。你不需要先有完整的 IDE,只要有一个能跑起来的 Qt Widgets 工程就行。
核心检索词先摆出来:QPlainTextEdit自定义代码组件、行号栏实现、当前行高亮、TaoToken 统一 Key 接入。这几个词会贯穿全文。我试过把行号逻辑写在主窗口里,结果每开一个标签页就要复制一遍信号槽,后来改成独立组件,主窗口只负责addTab,清爽很多。
先说清楚这个组件的结构。它由两个类组成:MyCodeEdit继承QPlainTextEdit,负责编辑器主体逻辑;LineNumberWidget继承QWidget,只负责把绘制、鼠标、滚轮事件转发回MyCodeEdit。这种“转发”设计是关键,因为行号栏本身不持有文档数据,所有行号计算都依赖QPlainTextEdit的block体系。如果你让行号栏自己去数行,滚动和折行时一定对不上。
MyCodeEdit需要处理的信号有三个:cursorPositionChanged触发当前行高亮,blockCountChanged触发行号栏宽度重算,updateRequest触发行号栏局部重绘或滚动。这三个信号缺一个,行号就会在增删行、滚动、移动光标时出现残影或错位。下面这段头文件是组件的骨架,注意LineNumberWidget的前向声明,否则在MyCodeEdit里使用它会报unknown type name。
#ifndef MYCODEEDIT_H #define MYCODEEDIT_H #include <QPlainTextEdit> class LineNumberWidget; class MyCodeEdit : public QPlainTextEdit { Q_OBJECT public: explicit MyCodeEdit(QWidget *parent = nullptr); void lineNumberWidgetPaintEvent(QPaintEvent *event); void lineNumberWidgetMousePressEvent(QMouseEvent *event); void lineNumberWidgetWheelEvent(QWheelEvent *event); private slots: void highlightCurrentLine(); void updateLineNumberWidget(QRect rect, int dy); void updateLineNumberWidgetWidth(); protected: void resizeEvent(QResizeEvent *event) override; private: void initFont(); void initConnection(); void initHighlighter(); int getLineNumberWidgetWidth(); LineNumberWidget *lineNumberWidget; }; class LineNumberWidget : public QWidget { public: explicit LineNumberWidget(MyCodeEdit *editor = nullptr) : QWidget(editor) { codeEditor = editor; } protected: void paintEvent(QPaintEvent *event) override { codeEditor->lineNumberWidgetPaintEvent(event); } void mousePressEvent(QMouseEvent *event) override { codeEditor->lineNumberWidgetMousePressEvent(event); } void wheelEvent(QWheelEvent *event) override { codeEditor->lineNumberWidgetWheelEvent(event); } private: MyCodeEdit *codeEditor; }; #endif // MYCODEEDIT_H这里有个细节值得展开:explicit关键字。LineNumberWidget的构造函数只有一个参数,如果不加explicit,编译器可能在某些场景下把MyCodeEdit*隐式转换成LineNumberWidget,产生难以察觉的错误。加上之后,必须显式构造,安全性更高。这个点在 Qt 组件开发里很常见,尤其是单参数构造函数。
行号栏的宽度计算也不能写死。行数从 9 行变成 10 行、99 行变成 100 行时,宽度要跟着变,否则行号会被截断。getLineNumberWidgetWidth()用QFontMetricsF测量字符宽度,再乘以行号位数,加上左右留白。旧版 Qt 没有horizontalAdvance,可以用width替代,兼容性更好。
int MyCodeEdit::getLineNumberWidgetWidth() { return 30 + QString::number(blockCount() + 1).length() * QFontMetricsF(font()).width(QChar('0')); }当前行高亮用的是QTextEdit::ExtraSelection。它允许你在不修改文档内容的前提下,给某一行加背景色。关键属性是QTextFormat::FullWidthSelection,设为true后高亮会铺满整行宽度,而不是只覆盖文字部分。光标移动时重新设置一次extraSelections,旧高亮自动被替换。
void MyCodeEdit::highlightCurrentLine() { QList<QTextEdit::ExtraSelection> extraSelections; QTextEdit::ExtraSelection selection; selection.format.setBackground(QColor(0, 100, 100, 20)); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); extraSelections.append(selection); setExtraSelections(extraSelections); }行号绘制发生在lineNumberWidgetPaintEvent里。它从firstVisibleBlock()开始,逐个block往下画,直到超出事件矩形底部。每个 block 的 top 和 bottom 通过blockBoundingGeometry和blockBoundingRect计算,再叠加contentOffset()。当前光标所在行用黑色,其他行用灰色,视觉上能快速定位。
void MyCodeEdit::lineNumberWidgetPaintEvent(QPaintEvent *event) { QPainter painter(lineNumberWidget); painter.fillRect(event->rect(), QColor(100, 100, 100, 100)); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int cursorTop = blockBoundingGeometry(textCursor().block()) .translated(contentOffset()).top(); int top = blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + blockBoundingRect(block).height(); while (block.isValid() && top <= event->rect().bottom()) { painter.setPen(cursorTop == top ? Qt::black : Qt::gray); painter.drawText(0, top, getLineNumberWidgetWidth() - 5, blockBoundingRect(block).height(), Qt::AlignRight, QString::number(blockNumber + 1)); block = block.next(); top = bottom; bottom = top + blockBoundingRect(block).height(); blockNumber++; } }滚动同步靠updateRequest信号。dy不为零时说明是滚动,直接让行号栏scroll(0, dy);否则是局部重绘,调用update指定区域。这样行号栏和文本区始终对齐,不会出现“文字滚了行号没滚”的情况。
void MyCodeEdit::updateLineNumberWidget(QRect rect, int dy) { if (dy) { lineNumberWidget->scroll(0, dy); } else { lineNumberWidget->update(0, rect.y(), getLineNumberWidgetWidth(), rect.height()); } }鼠标点击行号栏时,根据点击的 y 坐标反推行号,再把光标设到对应 block。滚轮事件则转发给编辑器的滚动条,保证在行号栏上滚动也能带动文本区。这两处如果不处理,用户体验会很割裂。
void MyCodeEdit::lineNumberWidgetMousePressEvent(QMouseEvent *event) { QTextBlock block = document()->findBlockByNumber( event->y() / fontMetrics().height() + verticalScrollBar()->value()); setTextCursor(QTextCursor(block)); } void MyCodeEdit::lineNumberWidgetWheelEvent(QWheelEvent *event) { if (event->orientation() == Qt::Horizontal) { horizontalScrollBar()->setValue( horizontalScrollBar()->value() - event->delta()); } else { verticalScrollBar()->setValue( verticalScrollBar()->value() - event->delta()); } event->accept(); }到这里,一个可用的纯代码QPlainTextEdit代码组件就成型了。它不依赖.ui文件,直接new MyCodeEdit(this)就能塞进QTabWidget。下一步是让它具备 AI 补全的调用能力,这就轮到 TaoToken 的统一 Key 和 API 通道出场。
2. TaoToken 前置准备:统一 Key 与 API 通道怎么配
在给编辑器接 AI 补全之前,先把服务端的入口理清楚。TaoToken 提供的是统一 Key 和统一 API 通道,也就是说你不需要为每个模型单独申请一套凭证,一个 Key 就能调用多个模型。这对代码组件来说很实用:补全用轻量模型,重构建议用强模型,切换时只改model字段,不用换 Key。
官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API 基础地址是 https://taotoken.net/api ,注意这个地址不带 UTM 参数,配置时直接写这个。
你需要准备三样东西:Base URL、API Key、Model ID。这三件套在后面的配置片段里会反复出现。Base URL 就是https://taotoken.net/api,API Key 在控制台创建,Model ID 根据你要用的模型填。控制台地址:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。API Keys 管理页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
创建 Key 的步骤不复杂:登录后进控制台,找到 API Keys 页面,点新建,复制生成的 Key。这个 Key 只显示一次,建议先存到本地配置文件或环境变量里。不要硬编码在源码里提交到仓库,这是常见的安全坑。
模型对话调试页可以用来验证 Key 是否可用:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。在页面上选一个模型,发一条测试消息,如果能正常返回,说明 Key 和通道都没问题。这一步建议在写代码之前做,避免后面把网络问题误判成代码问题。
如果你打算长期做编码类 Agent,可以了解 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它面向的是持续编码场景,和单次补全的调用方式略有不同,但底层还是同一套 Key 和通道。
接入文档在这里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。文档里有完整的请求格式和参数说明,建议配置前扫一遍,尤其是messages数组的结构和stream参数。
对于 Claude Code 这类工具,Anthropic 兼容入口是:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecodeanthropic&utm_campaign=rewrite 。如果你用的是 Claude Code 而不是自己写 Qt 组件,这个入口更直接。
回到 Qt 组件这边,我们需要在MyCodeEdit里加一个触发补全的入口,比如Ctrl+Space快捷键,或者在某个信号里发起请求。请求本身用QNetworkAccessManager发 HTTP POST,body 是 JSON,header 里带Authorization: Bearer <你的Key>。下面是一个最小可用的请求构造。
QNetworkRequest request(QUrl("https://taotoken.net/api/v1/chat/completions")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8()); QJsonObject message; message["role"] = "user"; message["content"] = "补全以下代码:\n" + currentLineText; QJsonArray messages; messages.append(message); QJsonObject body; body["model"] = "your-model-id"; body["messages"] = messages; body["stream"] = false; QNetworkReply *reply = manager->post(request, QJsonDocument(body).toJson());这里apiKey和model建议从配置文件读,不要写死在代码里。可以用QSettings存,和前面主窗口保存历史记录用的是同一套机制。配置文件里至少要有三项:base_url、api_key、model。这样换模型或换 Key 时不用重新编译。
[taotoken] base_url=https://taotoken.net/api api_key=sk-你的Key model=your-model-id读取时用QSettings的value方法,取不到就给默认值。注意base_url末尾不要多加斜杠,拼接路径时容易出双斜杠。请求路径是/v1/chat/completions,和 OpenAI 兼容格式一致。
如果你用的是 Cline 或 MCP 类工具,配置方式类似,核心还是 Base URL、Key、Model ID 三件套。Cline 的 MCP 配置里,baseUrl填https://taotoken.net/api,apiKey填你的 Key,model填模型 ID。Codex 的auth.json里也是这三个字段,只是键名可能不同。不管哪种工具,只要这三项对齐,通道就能通。
有一点要提醒:不要把 TaoToken 当成非法中转来理解,它是正常的 API 聚合服务,提供统一入口。配置时按文档来,不要自己拼奇怪的路径。
3. 可复制配置:把 AI 补全接进 QPlainTextEdit 组件
这一节给可直接复制的配置片段。目标是在MyCodeEdit里加一个requestCompletion()方法,绑定到快捷键,请求成功后把补全内容插入光标位置。整个过程分三步:读配置、发请求、处理响应。
先看配置读取。用QSettings从settings.ini读[taotoken]段,如果文件不存在或字段缺失,用默认值兜底。这样即使配置文件没准备好,程序也不会崩。
struct TaoTokenConfig { QString baseUrl; QString apiKey; QString model; }; TaoTokenConfig loadTaoTokenConfig() { QSettings settings("settings.ini", QSettings::IniFormat); TaoTokenConfig cfg; cfg.baseUrl = settings.value("taotoken/base_url", "https://taotoken.net/api").toString(); cfg.apiKey = settings.value("taotoken/api_key", "").toString(); cfg.model = settings.value("taotoken/model", "").toString(); return cfg; }然后在MyCodeEdit构造函数里初始化QNetworkAccessManager,并绑定快捷键。QShortcut的Ctrl+Space在编辑器里比较通用,不容易和系统快捷键冲突。
MyCodeEdit::MyCodeEdit(QWidget *parent) : QPlainTextEdit(parent) { lineNumberWidget = new LineNumberWidget(this); networkManager = new QNetworkAccessManager(this); initConnection(); initFont(); initHighlighter(); highlightCurrentLine(); updateLineNumberWidgetWidth(); setLineWrapMode(QPlainTextEdit::NoWrap); QShortcut *shortcut = new QShortcut(QKeySequence("Ctrl+Space"), this); connect(shortcut, &QShortcut::activated, this, &MyCodeEdit::requestCompletion); }requestCompletion()的核心是取当前光标所在行的文本作为上下文,构造请求,发出去。为了简单,这里用非流式请求,等完整响应回来再插入。流式请求处理起来更复杂,需要逐块解析 SSE,适合后续优化。
void MyCodeEdit::requestCompletion() { TaoTokenConfig cfg = loadTaoTokenConfig(); if (cfg.apiKey.isEmpty() || cfg.model.isEmpty()) { qDebug() << "TaoToken config missing: api_key or model"; return; } QTextCursor cursor = textCursor(); QString currentLine = cursor.block().text(); QJsonObject message; message["role"] = "user"; message["content"] = QString( "请补全以下代码,只返回补全部分,不要解释:\n%1") .arg(currentLine); QJsonArray messages; messages.append(message); QJsonObject body; body["model"] = cfg.model; body["messages"] = messages; body["stream"] = false; QNetworkRequest request( QUrl(cfg.baseUrl + "/v1/chat/completions")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", QString("Bearer %1").arg(cfg.apiKey).toUtf8()); QNetworkReply *reply = networkManager->post(request, QJsonDocument(body).toJson()); connect(reply, &QNetworkReply::finished, this, [this, reply]() { handleCompletionReply(reply); }); }响应处理要区分成功和失败。成功时解析choices[0].message.content,插入到光标位置;失败时打印错误信息,方便排查。注意reply用完要deleteLater(),否则会内存泄漏。
void MyCodeEdit::handleCompletionReply(QNetworkReply *reply) { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { qDebug() << "TaoToken request failed:" << reply->errorString(); return; } QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); if (!doc.isObject()) { qDebug() << "Invalid JSON response:" << data; return; } QJsonObject obj = doc.object(); QJsonArray choices = obj["choices"].toArray(); if (choices.isEmpty()) { qDebug() << "No choices in response:" << data; return; } QString content = choices[0].toObject() ["message"].toObject() ["content"].toString(); if (content.isEmpty()) { qDebug() << "Empty completion content"; return; } QTextCursor cursor = textCursor(); cursor.insertText(content); setTextCursor(cursor); }如果你用的是 Cline MCP 或 Codex 的auth.json,配置字段对应关系如下表。核心还是 Base URL、Key、Model ID 三件套,只是键名不同。
| 工具 | Base URL 字段 | Key 字段 | Model 字段 |
|---|---|---|---|
| Qt 组件 | base_url | api_key | model |
| Cline MCP | baseUrl | apiKey | model |
| Codex auth.json | base_url | api_key | model |
| Claude Code | ANTHROPIC_BASE_URL | ANTHROPIC_API_KEY | ANTHROPIC_MODEL |
Claude Code 的接入方式略有不同,它走 Anthropic 兼容入口,环境变量名是ANTHROPIC_BASE_URL、ANTHROPIC_API_KEY、ANTHROPIC_MODEL。如果你在终端里用 Claude Code,可以这样设置。
export ANTHROPIC_BASE_URL=https://taotoken.net/api export ANTHROPIC_API_KEY=sk-你的Key export ANTHROPIC_MODEL=your-model-id设置完在终端里跑claude命令,如果能正常对话,说明通道通了。这一步和 Qt 组件是独立的,但底层用的是同一套 Key 和通道。
回到 Qt 组件,还有一个细节:补全内容插入后,光标位置要更新,否则下一次补全会基于旧位置。setTextCursor(cursor)就是干这个的。另外,如果补全内容包含换行,插入后行号栏会自动更新,因为blockCountChanged信号会触发宽度重算。
如果你想让补全更智能,可以把光标前后的若干行都作为上下文传给模型,而不是只传当前行。上下文越长,补全质量越高,但请求体也越大。建议控制在 20 行以内,平衡质量和延迟。
4. 验证请求与成功结果:从发起到插入的完整链路
配置写完之后,必须验证整条链路能跑通。验证分两层:先验证 TaoToken 通道本身可用,再验证 Qt 组件能正确发起请求并处理响应。
第一层验证用模型对话页最直接。打开 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,选一个模型,输入一句测试消息,比如“返回 hello”。如果页面上能正常显示回复,说明 Key 有效、通道通畅。这一步不涉及代码,排除掉服务端问题。
第二层验证在 Qt 程序里。启动程序,新建一个标签页,按Ctrl+Space,观察控制台输出。如果配置正确,应该能看到补全内容插入到光标位置。如果没反应,先看控制台有没有打印错误。下面是一个正常的请求和响应示例。
请求体:
{ "model": "your-model-id", "messages": [ { "role": "user", "content": "请补全以下代码,只返回补全部分,不要解释:\nint main() {" } ], "stream": false }响应体:
{ "choices": [ { "message": { "role": "assistant", "content": " return 0;\n}" } } ] }解析后content是return 0;\n},插入到int main() {后面,编辑器里就变成完整的main函数。行号栏会自动多出一行,当前行高亮也会跟着光标移动。
验证时可以用curl先测通道,排除 Qt 代码问题。命令如下,把sk-你的Key和your-model-id替换成实际值。
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{ "model": "your-model-id", "messages": [{"role": "user", "content": "返回 hello"}], "stream": false }'如果curl能返回正常 JSON,说明通道没问题,问题在 Qt 代码里。如果curl也失败,先检查 Key 和模型 ID 是否正确,再看网络是否能访问taotoken.net。
Qt 这边,建议在handleCompletionReply里加日志,把reply->errorString()和原始响应体都打出来。这样出错时能快速定位。日志可以用qDebug(),输出到控制台。
qDebug() << "Response status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); qDebug() << "Response body:" << data;成功插入补全内容后,你可以继续按Ctrl+Space触发下一次补全。每次补全都基于当前光标位置,所以连续补全可以逐步把代码写完整。实测下来,这种“按需触发”的方式比自动补全更可控,不会在打字时频繁打断。
还有一个验证点:行号栏在补全插入后是否正确更新。插入多行内容时,blockCountChanged会触发updateLineNumberWidgetWidth(),行号栏宽度自动调整。如果行号显示不全,检查getLineNumberWidgetWidth()的返回值是否随行数变化。
如果你用的是 Claude Code 而不是 Qt 组件,验证方式是在终端里跑一个简单对话。设置好环境变量后,输入claude进入交互模式,问一句“你好”,能正常回复就说明通道通了。Claude Code 的接入文档在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecodeanthropic&utm_campaign=rewrite ,里面有详细的环境变量说明。
验证通过后,整个链路就闭环了:Qt 组件负责编辑体验,TaoToken 负责模型调用,两者通过 HTTP 请求连接。后续要换模型,只改配置文件里的model字段,不用动代码。
5. 常见错误排查:401、local proxy failed、reading choices、OAuth
接入过程中最容易踩的坑集中在几个报错上。这一节按报错信息逐个排查,给出原因和修改方法。
401 Unauthorized。这是最常见的错误,说明 Key 无效或没带上。检查三处:配置文件里的api_key是否为空,请求头Authorization是否拼成了Bearer sk-xxx,Key 是否已经过期或被删除。如果用的是环境变量,检查变量名是否拼错。Claude Code 用的是ANTHROPIC_API_KEY,不是OPENAI_API_KEY,拼错就会 401。
// 错误写法:少了 Bearer 前缀 request.setRawHeader("Authorization", apiKey.toUtf8()); // 正确写法 request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8());local proxy failed。这个报错通常出现在工具配置了本地代理但代理没启动时。检查工具的代理设置,如果不需要代理,把代理配置清空。Qt 这边如果设置了QNetworkProxy,也要检查是否指向了不可用的地址。排查方法是先用curl直连https://taotoken.net/api,如果能通,说明是工具代理配置的问题。
reading choices 报错。这个错误说明响应 JSON 里没有choices字段,或者choices是空数组。常见原因有三个:请求体里model字段填错,服务端返回了错误信息而不是补全结果;messages数组为空或格式不对;响应被截断,JSON 解析失败。排查时先把原始响应体打印出来,看服务端到底返回了什么。
QJsonArray choices = obj["choices"].toArray(); if (choices.isEmpty()) { qDebug() << "Full response:" << data; // 如果 data 里有 "error" 字段,说明是服务端错误 if (obj.contains("error")) { qDebug() << "Server error:" << obj["error"].toObject(); } return; }OAuth 相关报错。如果你用的是 Claude Code 或类似工具,可能会遇到 OAuth 认证失败。这类工具默认走 OAuth 流程,但用 TaoToken 的 Key 接入时,应该走 API Key 模式。检查配置里是否误开了 OAuth,或者环境变量里是否残留了旧的 OAuth token。Claude Code 的接入文档里说明了用 API Key 的方式,按文档配置即可。
行号栏错位。这不是网络错误,但很常见。表现是滚动时行号跟不上文字,或者行号栏出现残影。原因是updateRequest信号没连上,或者dy处理逻辑写错。检查initConnection()里是否连了updateRequest,以及updateLineNumberWidget里dy的判断是否正确。
void MyCodeEdit::initConnection() { connect(this, &QPlainTextEdit::cursorPositionChanged, this, &MyCodeEdit::highlightCurrentLine); connect(this, &QPlainTextEdit::blockCountChanged, this, &MyCodeEdit::updateLineNumberWidgetWidth); connect(this, &QPlainTextEdit::updateRequest, this, &MyCodeEdit::updateLineNumberWidget); }注意信号名拼写。cursorPositionChanged不是cursorPositionCHanged,大小写错了信号连不上,高亮就不会触发。这个坑在 excerpt 里也提到过,是真实会遇到的。
补全内容插入位置错误。如果补全内容插到了文件开头而不是光标位置,说明textCursor()取的不是当前光标。检查是否在请求发出后、响应回来前,用户移动了光标。这种情况下,应该在请求发出时保存光标位置,响应回来后再恢复。
// 请求发出前保存光标 QTextCursor savedCursor = textCursor(); // 响应回来后用 savedCursor 插入 savedCursor.insertText(content); setTextCursor(savedCursor);请求超时。如果请求长时间没响应,检查网络是否能访问taotoken.net,以及模型 ID 是否有效。有些模型响应较慢,可以适当增加超时时间。QNetworkAccessManager默认超时较长,一般不用改。
排查顺序建议:先用curl验证通道,再检查 Qt 代码里的请求构造,最后看响应解析。这样能快速定位问题在哪一层。
6. 继续接入:模型对话、Coding Plan 与文档入口
组件跑通之后,你可以按需扩展。如果只是想验证模型效果,用模型对话页最方便:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。在页面上切换不同模型,对比补全质量,找到适合你场景的那个。
如果你打算把这个代码组件做成长期使用的编码工具,或者接入 Agent 流程,可以了解 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它面向持续编码场景,和单次补全的调用方式不同,但底层还是同一套 Key 和通道。
Key 管理在 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。建议定期轮换 Key,避免泄露风险。控制台入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。
完整的接入文档在这里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。里面有请求格式、参数说明、错误码解释,配置前扫一遍能少走弯路。
Claude Code 的 Anthropic 兼容入口:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecodeanthropic&utm_campaign=rewrite 。如果你用 Claude Code 而不是自己写 Qt 组件,这个入口更直接。
官网首页:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API 基础地址:https://taotoken.net/api 。
最后给一个实用技巧:把settings.ini里的api_key用环境变量覆盖,这样本地开发和部署时可以用不同的 Key,不用改配置文件。读取时先查环境变量,没有再读配置文件。
QString apiKey = qEnvironmentVariable("TAOTOKEN_API_KEY"); if (apiKey.isEmpty()) { apiKey = settings.value("taotoken/api_key", "").toString(); }这样在 CI 或服务器上,只需要设置环境变量,配置文件里不用存敏感信息。本地开发时把 Key 写在配置文件里,方便调试。两种方式都支持,按场景选。