0%

使用 http 协议上传文件的协议细节和实现。

使用 curl 上传

调试 Swagger API 时一般会使用 postman 工具,但可以有更加短平快的手段、工具满足我们轻量的调试需求。

curl ,linux 平台下常用,或者 windows 平台下 git bash 中也带有 curl 命令。

1
2
3
4
5
6
7
 -X, --request <method>      Specify request method to use 
-d, --data <data> HTTP POST data
-H, --header <header/@file> Pass custom header(s) to server
-v, --verbose Make the operation more talkative
-F, --form <name=content> Specify multipart MIME data
To force the 'content' part to be a file, prefix the filename with an @ sign.
@ makes a file get attached in the post as a file upload, ...

使用上述参数,实际演练一下:使用 -d-F 参数时,curl 能够自行推断出要使用 POST 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ curl 'http://localhost:3001/api/v1/document/upload' -v \
-H 'accept: application/json' \
-H 'Authorization: Bearer S0QSFCC-MYTMPGN-M9WEQBZ-3V579DV' \
-H 'Content-Type: multipart/form-data' \
-F 'file=@tmp.txt' # -F 'user=niel'
... # 此处有省略
> POST /api/v1/document/upload HTTP/1.1
> Host: localhost:3001
> User-Agent: curl/8.6.0
> accept: application/json
> Authorization: Bearer S0QSFCC-MYTMPGN-M9WEQBZ-3V579DV
> Content-Length: 202
> Content-Type: multipart/form-data; boundary=------------------------pF28TYDnB4wmWf8sPjKctM
>
} [202 bytes data]
* We are completely uploaded and fine
< HTTP/1.1 200 OK
... # 此处有省略

接下来结合 curl 输出和 wireshark 抓包,来学习标准。

Encapsulation 封装;Disposition 布置;

协议

多用途互联网邮件扩展(英语:Multipurpose Internet Mail Extensions,缩写:MIME)是一个互联网标准,它扩展了电子邮件标准。

此外,在万维网中使用的 HTTP 协议中也使用了 MIME 的框架,标准被扩展为互联网媒体形式

MIME 是通过标准化电子邮件报文的头部的附加域(fields)而实现的;这些头部的附加域,描述新的报文类型的内容和组织形式。

更多细节请参考 MIME 类型 的详细内容!

Content-Type

MIME 类型的格式:Content-Type: type/subtype;parameter=value ,参数可选。

类型可分为两类:独立的(discrete)和多部分的(multipart)。

  • 独立类型代表单一文件或媒介,比如一段文字、一个音乐文件、一个视频文件等。

  • 而多部份类型,可以代表由多个部件组合成的文档,其中每个部分都可能有各自的 MIME 类型;此外,也可以代表多个文件被封装在单次事务中一同发送。

    multipart/form-data 作为多部分文档格式,它由 boundary 边界线划分出的不同部分组成。

1
Content-Type: multipart/form-data; boundary=aBoundaryString

每一部分有自己的 content header (零个或多个 Content- header fields) 和 body 。

Content-Disposition

最初的 MIME 规范仅描述了邮件消息的结构。它们没有解决呈现样式的问题。

在 RFC 2183 中添加了 content-disposition 标头字段来指定呈现样式。

此表头用在 http 协议中有两个使用场景:

  • As a response header for the main body

  • As a header for a multipart body ,第一个指令始终是 form-data ,并且还必须包含一个 name 参数来标识表单字段名。

    filename 参数是可选的,指示要传送的文件的初始名称的字符串。

实现

使用 Qt 库完成文件上传的功能, by Google’s Gemini

如果手动添加文件头 Content-Type:multipart/form-data 似乎会造成 boundary= 参数丢失: #TODO# 有待验证

  • 虽然手动设置文件头在前,->post(request, multiPart) 在后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void uploadFile() {
QNetworkRequest request(QUrl("http://example.com/api/upload")); // 替换为您的服务器上传接口 URL

QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);

// 1. 文件数据部分
QHttpPart filePart;
QString disposition = QString(R"(form-data; name="file"; filename="%1")").arg(QFileInfo(filePath).fileName());
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(disposition);
QFile *file = new QFile(filePath);
if (!file->open(QIODevice::ReadOnly)) {
QMessageBox::critical(this, "错误", "无法打开文件: " + filePath);
delete multiPart;
return;
}
filePart.setBodyDevice(file); // file的所有权转移给filePart
file->setParent(multiPart); // 设置父对象,防止被提前析构
multiPart->append(filePart);

// 2. 其他表单字段 (可选)
QHttpPart namePart;
namePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"user\""));
namePart.setBody("niel");
multiPart->append(namePart);

QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this, &MyWidget::uploadFinished);

QNetworkReply *reply = manager->post(request, multiPart);
multiPart->setParent(reply); // 设置父对象,确保multiPart在reply完成前有效

// 可以添加请求头,例如设置认证信息
// request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data"); // QHttpMultiPart 会自动设置
// request.setHeader(QNetworkRequest::AuthorizationHeader, "Bearer YourAccessToken");
}
void uploadFinished(QNetworkReply *reply) {
reply->deleteLater(); // 必须 deleteLater 释放 reply 对象
}

LLM

如何更新 Ollama 呢?

How can I specify the context window size?

其中的“上下文窗口”是什么概念?上下文是什么概念?

prompt 提示词又会带来什么?

Ollama 会动态地加载(或卸载)其中的大语言模型,DeepSeek 等大模型并非一直在运行的。

AnythingLLM

参考其 github 的 README 文件

这个单库由三个主要部分组成:frontend / server / collector

通过 yarn 分别启动三个服务时,端口占用 3000 / 3001 / 8888 ,可以通过 nmap 扫描到这三个端口。

直接启动 Desktop 桌面软件时,只监听了 localhost (无法通过局域网地址访问),telnet 3001 / 8888 能够访问,telnet 3000 失败。

  • Desktop 桌面软件如何打包这三个服务的?
  • 针对 fronted ,和其他两个服务有什么区别?

使用 Desktop 版本提供 AnythingLLM 服务:设置-管理员-系统-Enable network discovery ,局域网内能够访问 3001 端口。

方案:只定制前端项目,保留最小的功能集合。后端服务由 Desktop 桌面软件提供,同时也用于配置 LLM 等管理功能。

——前端项目配置文件 .env 修改变量 VITE_API_BASE='http://${desktop}:3001/api'

Docker vs Desktop

参考 AnythingLLM 官方文档

如果你不需要多用户支持,你可以安装桌面版本。Docker 版本,是通过浏览器以网页形式访问。

系统设置上,容器版本比桌面版本多出以下选项:

  • 用户与安全:启用多用户模式、密码保护等(在启用多用户模式后,此入口消失。即,无法关闭多用户模式)
  • 外观:自定义图标、自定义页脚图标、站点名称、站点图标等
  • 管理员:在启用多用户模式之后,会增加用户、邀请两个入口,用于用户管理等

通过网页“设置-用户与安全-启用多用户模式”:

  • 访问桌面版软件的 3001 服务,启用多用户模式失败!报错 “ Internal Server Error”
  • 访问 Docker 或 npm run dev:server 服务,启用多用户模式成功。

Desktop 只是单用户版本;Docker 版本才支持多用户(独立部署也可以,但官方不推荐也不维护)。

在 Arm + Linux 上没有 Desktop 版本;只能通过浏览器 http://x.x.x:3000 访问独立部署的服务。

Desktop 版本是如何将网页(严格来说是三个服务)打包成独立软件,我并不清楚。

三级用户

官方手册 Security and Access

Once in multi-user mode, you cannot revert back to single-user mode.

用户有三类:Default / Manager / Admin

  • 普通用户:只能发送聊天,不能上传文件。
  • 管理账户:管理所有工作区,但不能设置 LLM 以及有关的向量数据库等。

问题

新建对话时,设置的 name 属性不生效!

新增的对话的名称总是 “Thread” ,虽然请求 /api/workspace/test1/thread/new 接口时指定了 {"name":"未命名"}

数据库 sqlite

了解 prisma 和 ORM 工具的概念。

对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术

ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。

以下命令会在 server\storage\ 创建 anythingllm.db 数据库文件(sqlite),并完成数据表的创建。

cd server && npx prisma migrate deploy --schema=./prisma/schema.prisma

删掉此 .db 文件并重建,即可关闭多用户模式(系统的配置和用户等信息也会丢失)。

Run AnythingLLM in production without Docker

AnythingLLM 核心团队不支持这种部署方式,仅仅作为参考。

Run AnythingLLM in production without Docker

以下目录需要指向 ./anything-llm/server/storage 的绝对路径,不能随意指定:

1
STORAGE_DIR="/your/absolute/path/to/server/storage"

因为 server 项目将上传的文件放到了 ${STORAGE_DIR}/../../collector/hotdir ,而 collector 项目监视 ./anything-llm/collector/hotdir

如果随意配置 STORAGE_DIR 变量,在上传文件时就会报错。

开发语言

我熟悉 http api 调用。使用 Qt 开发的话还要面对 Markdown 渲染:

Markdown Editor demonstrates how to use QWebChannel and JavaScript libraries to provide a rich text preview tool for a custom markup language.

试验在 Qt 5.14 引入的 QTextEdit::setMarkdown() 接口 能否满足目前的渲染需求。

具体过程

  1. 部署 LLM、Ollama 、向量数据库等服务,主要由同事完成 @lix
  2. 定制 AnythingLLM 前端页面 @niel
  3. 了解、学习 AnythingLLM 使用流程和相关概念
  4. 学习、试验 AnythingLLM api ,开发 Qt 桌面软件 @sunlb

前端开发环境

JavaScript 框架和库,提供了结构化的方式来组织代码,提高了开发效率和代码可维护性。

推荐 React

构建工具,现代前端项目通常需要经过构建工具的处理,才能在浏览器中高效运行。

推荐 Vite

包管理器,前端项目会依赖各种第三方库和工具(例如 React 本身,以及各种 UI 组件库、工具库等)

推荐 npm 或 yarn

React 文档?

1
2
3
4
5
npm install -g create-react-app
npm install -g yarn
create-react-app my-react-project
cd my-react-project
nmp start # http://localhost:3000

离线环境

Running Yarn offline

1
2
3
4
5
6
7
8
9
10
11
npm install -g yarn
yarn config set yarn-offline-mirror ./npm-packages-offline-cache
yarn config set yarn-offline-mirror-pruning true
mv ~/.yarnrc ./
rm -rf ./node_modules/ yarn.lock
yarn cache clean
yarn install
ls ~/npm-packages-offline-cache
# 关掉网络,清理数据后验证(保留 yarn.lock)
yarn cache clean && rm -rf ./node_modules/
yarn install --offline

离线镜像的目录是相对路径:

./npm-packages-offline-cache is an example location relative to home folder where all the source.tar.gz files will be downloaded to from the registry.

npm 和 yarn 的区别

QUdpSocket

使用上存在几个注意事项,当其结果不符合预期时看看这里有没有记录。

QUdpSocket::readyRead()

记录一个关于 信号的 bug ,有数据到达但此信号不再 emit :官方相似 bug 描述

其内部实现依赖 hasPendingData 标志位判断是否发出信号(并调用关联的槽函数) ,但此标志位容易出错, Qt 将此标志位的复位操作交给了用户:

Note: An incoming datagram should be read when you receive the readyRead() signal, otherwise this signal will not be emitted for the next datagram.

阅读全文 »

开发 Qt 软件过程中发现折线图(Qt Charts 二维折线图)显示不全、圆环刻度值(QPainter 自绘)显示异常。

上述问题和程序运行时使用的 Qt 版本有关,但和编译时使用的 Qt 版本无关:

  • 项目源码是在 Qt 5.13.2 下构建开发的,未针对 5.12.12 做适配
  • 但使用 Qt 5.12.12 也能够编译成功;
  • 运行时必须使用 Qt 5.13.2 ,如果运行时使用 5.12.12 就会出现上述现象。

使用 Qt 5.13.2 构建的可执行文件,运行时库如果使用 5.12.12 会启动失败,报错:
undefined symbol: qt_resourceFeatureZlib, version Qt_5

在 Qt Creator 中运行、调试项目时,使用的 Qt 版本一般和编译期的版本一致,但我们可以打破这种一致性:

  • 编译时,关闭 QT.global.enabled_features = rpath 特性:此特性在 5.12.12 中默认关闭;在 5.13.2 中默认开启。
  • 运行时,修改 LD_LIBRARY_PATH 使找到的 Qt 运行时库和编译时的库版本不一致

在设备上部署软件后,如何约束运行时使用的 Qt 版本(以及第三方库):

  • 通过 -rpath 特性约束,要求设备上存在对应的目录
  • 通过 LD_LIBRARY_PATH 环境变量约束

可以使用 readelf -d a.out 查看可执行文件是否指定了运行时库的查找路径。

阅读全文 »

搭建环境:

  • Qt 在 Qt5.12.12 之前提供 Windows/Linux/Mac 离线安装包,只有 x86 架构的
  • Qt 目前已经支持 ARM 架构,但只有在线安装包
  • Qt 从源码构建安装,费时费力还容易出错。我还没掌握
  • Qt 在 Linux 还可以通过各系统的软件仓库安装,版本陈旧,也需要联网

软件仓库

系统环境:

1
2
3
4
5
6
7
niel@ubuntu:~$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.1 LTS"
niel@ubuntu:~$ uname -a
Linux ubuntu 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

克隆仓库需要较大的磁盘空间,下载过程也耗时比较久:

1
2
3
apt install apt-mirror
vim /etc/apt/mirror.list
apt-mirror
阅读全文 »

在 Qt 中虽然存在 QTimeZone 类型,但其官方手册中多次强调了用户不应直接使用 QTimeZone ,而是结合 Qt::TimeSpec 枚举值使用 QDateTime

This class is primarily designed for use in QDateTime; most applications will not need to access this class directly and should instead use QDateTime with a Qt::TimeSpec of Qt::TimeZone.

If you require a QDateTime that uses the current system time zone at any given moment then you should use a Qt::TimeSpec of Qt::LocalTime.

Time Zone Offsets 时差

The total offset is comprised of two component parts, the standard time offset and the daylight-saving time offset. 夏时制

阅读全文 »

在 Qt Creator 如何运行终端程序呢?

在项目的 Build & Run 配置中,勾选 Run in terminal 即可。

选中 Run in terminal 之前,应用程序 Run in 哪里呢?

通过 Windows 任务管理器查看,勾选后作为 qtcreator_process_stub.exe (和 Qt Creator 进程同级别)的子进程运行,没有勾选的话作为 Qt Creator 的子进程运行。

Qt 的日志框架

最常用的函数: qDebug(const char *message, ...)

Calls the message handler with the debug message message.

  • If no message handler has been installed, the message is printed to stderr.
  • Under Windows the message is sent to the console, if it is a console application; otherwise, it is sent to the debugger.
  • On QNX, the message is sent to slogger2.
  • This function does nothing if QT_NO_DEBUG_OUTPUT was defined during compilation.
阅读全文 »

我们在谈到“串口”的时候,往往还会提到 RS-232 、RS-485 等等,它们之间的关系是什么?

串行端口 的概念,是和 并行端口 相对的。串口一次只传输 1bit ,但并行端口会同时传输多个 bit 。

通信,传输字符/字节。

历史上字节长度曾基于硬件为 1-48 bit不等,最初通常使用6 bit 或9 bit 为一字节。今日标准以 8 bit 作为一字节。

虽然以太网、FireWire 和 USB 等接口也以串行流的形式发送数据,但术语 串行端口 通常表示符合 RS-232 或相关标准(如 RS-485 或 RS-422)的硬件。

我们可能会用到 232 串口线、或者 422 串口线连接两个设备,但使用 Qt serial port 模块写软件不用考虑使用了哪种串口线,只需要关注收发两端的波特率、校验位、停止位等一致。

The most well-known options are

  • speed,
  • number of data bits per character,
  • parity, and
  • number of stop bits per character.

硬件

RS-232 是美国电子工业联盟制定的串行数据通信的接口标准,它广泛用于计算机串行接口外设连接。

它规定连接电缆和机械、电气特性、信号功能及发送过程。其他常用电气标准还有 RS-422-A、RS-423A、RS-485。

出于节省资金和空间的考虑,25 个管脚的 DB-25 连接器已经不常见,9 个管脚的 DB-9 型连接器被广泛使用。RS-232 中 DB-9 型连接器的信号和管脚分配:2-收,3-发,5-接地。

阅读全文 »

从桌面开发突然进入 VxWorks 嵌入式开发,有几个认知上的调整:

  1. 桌面开发的时候,驱动、操作系统和 IDE 之后,我们才开始新建项目、编写代码;
  2. 嵌入式开发,从板级支持包、系统镜像就需要新建对应的项目了。
  3. 理解嵌入式系统可裁剪,镜像大概率并不支持那些在桌面操作系统上常见的指令、服务和开发所需的库,需要有针对性的一个一个配置相应的驱动、组件。

Das U-Boot

加载桌面操作系统之前的引导阶段,按下任意键进入 BIOS。与之类似,我们在板子引导程序读秒期间(使用串口调试工具)发送任意键能够进入 uboot,使用 printenv 等命令查看状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 查看版本
version
# 获取帮助
help
?
# 打印环境变量
printenv
# 查询 FAT 格式设备的目录和文件信息
fatls scsi 0:8
fatls scsi 0:1
# bootcmd 自动启动时执行命令
setenv bootcmd "fatload scsi 0:1 0x90100000 Ft2004.elf; bootvx32 0x90100000;"
saveenv
# 查看日期时间
date
# 查看帮助
help date

# 更多
fatinfo - print information about filesystem
fatload - load binary file from a dos filesystem
fatls - list files in a directory (default /)
阅读全文 »