最近在研究ai,通过ollama部署了deepseek之后,对ollama调用方式感到很有兴趣,所有研究了一下,发现了一些以前没见过的技术,下面来说一下吧。

Ollama流式响应

一开始我是使用了Page Assist这个浏览器插件对ollama进行调用,这里展示一下ollama的接口是怎样响应的:

image.png

image.png

image.png

很可惜edge并不支持直接预览这个响应,只能看十六进制值以及对应的字符,而且和标准的event-source也不一样,不过好在我在apifox里面调试发现可以正常显示json

image.png

ndjson

从响应可以看到,响应的类型是 application/x-ndjson ,这是ds对他的介绍:

application/x-ndjson 是一种 MIME 类型,表示数据格式为 Newline Delimited JSON(简称 NDJSON)。它是一种基于文本的数据格式,用于存储或传输多个独立的 JSON 对象,每个对象占一行,并通过换行符(\n)分隔。NDJSON 在需要高效处理大规模或流式 JSON 数据的场景中非常实用。

这个内容说明了,这是一个二进制(application代表这是二进制数据)的多个json,而且每个json之间用换行符隔开,可以看apifox的响应预览,切到raw:

image.png

可以看到确实是每一行都是一个json字符串。

Transfer-Encoding: chunked

除了内容类型,还有个传输编码方式值得注意,就是响应头的:Transfer-Encoding: chunked。这是ds对这个头的说明:

Transfer-Encoding: chunked 是 HTTP 协议中的一种传输编码方式,允许服务器将响应数据分成多个“块”(chunk)逐步发送给客户端,无需预先知道数据的总长度。适用于动态生成内容或大文件传输的场景。

核心特点

无需预知总大小
不需要提前计算并设置 Content-Length 头,适合实时生成内容(如流媒体、动态 API 响应)。

分块传输
数据被拆分为多个独立块,每块包含:

-   **块大小**(十六进制数值,如 `1A` 表示 26 字节)。
-   **块内容**(实际数据)。
-   换行符(`\r\n`)。

结束标志
最后发送一个 0 长度的块(0\r\n\r\n),表示传输完成。

可以看到对比于普通的请求,这样的传输方式是一块一块的,不用提前知道整个请求的最终文本长度,比较咱们也没办法预算ai的回答有多长对吧,所有这种传输方法就非常合适。

在浏览器请求

讲解完这两个核心知识点之后,我们就已经大致掌握了流式请求的要义了,剩下的就是怎么发起和处理这些数据了,咱们先来看看JS是怎么请求的,咱们可以参考Page Assist,通过对源码的预览,我找到发起请求的源码:

https://github.com/n4ze3m/page-assist/blob/08b84e3918195b7cb8470fd67f60a80f94522005/src/models/utils/ollama.ts#L125-L192

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
async function* createOllamaStream(
url: string,
params: OllamaRequestParams,
options: OllamaCallOptions
) {
const response = await fetch(formattedUrl, {
method: "POST",
body: JSON.stringify(params),
})
if (!response.ok) {
// ....
}

const stream = IterableReadableStream.fromReadableStream(response.body)

const decoder = new TextDecoder()
let extra = ""
for await (const chunk of stream) {
const decoded = extra + decoder.decode(chunk)
const lines = decoded.split("\n")
extra = lines.pop() || ""
for (const line of lines) {
try {
yield JSON.parse(line)
} catch (e) {
console.warn(`Received a non-JSON parseable chunk: ${line}`)
}
}
}
}

其他那些有的没的代码就先删掉了,之间看核心部分代码。首先是使用fetch这个api发起请求,对于fetch这个api就不多说了,详情看MDN文档

为了方便迭代处理,这里将普通的可读流转成可迭代的可读的流,方便下面for await处理,这部分语法详情可以看mdn文档

因为前面说了,我们拿到的是二进制(也是十六进制)码,我们需要解码成文本,才能进行json反序列化,所有这里创建了一个文本解码类:TextDecoder,剩下就是调用decode方法对二进制进行解码。

解码出每一行json之后,就是大家都懂的json反序列化了。

最后我们修改一下代码,在控制台看看数据:

image.png

image.png

最终我们这样就能从流式请求接口拿到咱们的数据啦!

rust请求

因为最近还在学习rust,然后我也突发奇想使用rust调用试试,一开始问ai也写不出rust的代码,经过对各种rust群进行发问,有个群友发了个ollama的rust版客户端

详情看发起请求的代码:

https://github.com/pepperoni21/ollama-rs/blob/2409a5b584b50b83b361b229455e59fe0f156dc8/ollama-rs/src/generation/chat/mod.rs#L26-L70

然后是调用这个方法的example:

https://github.com/pepperoni21/ollama-rs/blob/2409a5b584b50b83b361b229455e59fe0f156dc8/ollama-rs/examples/chat_api_chatbot.rs

然后我们抄袭借鉴一下代码:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
use ollama_rs::generation::chat::ChatMessageResponse;
use reqwest::Client;
use serde::Serialize;
use std::io::{stdout, Write};

#[derive(Debug, Clone, Serialize)]
pub struct ChatMessageRequest {
model: String,
messages: Vec<ollama_rs::generation::chat::ChatMessage>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use tokio_stream::StreamExt;
let mut stdout = stdout();
let client = Client::new();
let builder = client.post("http://127.0.0.1:11434/api/chat");
let serialized = serde_json::to_string(&ChatMessageRequest {
model: "deepseek-r1:1.5b".to_string(),
messages: vec![ollama_rs::generation::chat::ChatMessage {
role: ollama_rs::generation::chat::MessageRole::User,
content: "你是谁".to_string(),
tool_calls: vec![],
images: None,
}],
})
.map_err(|e| e.to_string())?;
let res = builder.body(serialized).send().await?;
if !res.status().is_success() {
println!("请求不成功:{}", res.status());
return Ok(());
}
let mut stream = Box::new(res.bytes_stream().map(|res| match res {
Ok(bytes) => {
let res = serde_json::from_slice::<ChatMessageResponse>(&bytes);
match res {
Ok(res) => Ok(res),
Err(e) => {
eprintln!("Failed to deserialize response: {}", e);
Err(())
}
}
}
Err(e) => {
eprintln!("Failed to read response: {}", e);
Err(())
}
}));
while let Some(Ok(res)) = stream.next().await {
stdout.write_all(res.message.content.as_bytes())?;
stdout.flush()?;
}
Ok(())
}

代码和js类似,看代码是少了一步转字符串的,可以直接将字符串字节反序列化。

a7ddz-njehu.gif

总所周知,一般前后端项目都会使用nginx来部署前端项目,有些小伙伴可能会想到go的打包部署非常方便,那有没有一种可能直接把前端内容也打包进产物里面呢?(类似gitea)

有的兄弟,有的,这么简单的需求,只需要用goframe的cli将前端产物文件转译成类似base64编码的的方式,将文件转成一个go代码,然后通过fs读取即可,话不多说咱们直接开始。

gf-cli

首先我们会需要用到gf-cli的pack方法将整个前端产物转译,直接去github下载最新的gf-cli,https://github.com/gogf/gf/releases ,比如我的是windows系统就是下载gf_windows_amd64.exe

下载完直接改名gf.exe然后丢到C:\Windows\System32,或者其他在PATH里的目录即可。

image.png

在终端输入gf能正常打印说明安装成功。

创建项目然后打包

我们可以通过gf的init创建一些标准的后端目录结构,但是我这里因为演示的是只是怎么使用gf打包前端项目,这里我们使用最简单的目录结构,一个main.go和那种,我们直接使用goland来创建一个简单项目:

image.png

然后我们再找一个前端项目,我这里就选SoybeanAdmin吧,使用我自己的一个脚手架创建项目:

image.png

修改一下.env.prod加入:

VITE_BASE_URL=/web

然后build出来之后,将dist里的产物放到我们的go项目,然后使用我们的pack命令来打包一下

1
gf pack ./web packed/data.go -n=packed

image.png

最后修改一下我们的 main.go

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
package main

import (
"io/fs"
"net/http"

"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gres"

_ "awesomeProject1/packed"
)

type GFPackFS struct {
Fallback string
}

// Open GFPackFS实现FS接口
func (f *GFPackFS) Open(name string) (fs.File, error) {
file := gres.Get(name)
// file == nil的逻辑用来模仿nginx的try_files配置,如果没有对应的文件,则返回index.html
// 如果是访问首页,既/web路径的时候,也同样返回我们index.html
if file == nil || name == "web" {
fullback := gres.Get(f.Fallback)
return fullback, nil
}
return file, nil
}

func main() {
s := g.Server()
// 创建一个路由组,前缀是/web即我们的前端页面
s.Group("/web", func(group *ghttp.RouterGroup) {
group.ALL("/*", ghttp.WrapH(http.FileServer(http.FS(&GFPackFS{"web/index.html"}))))
})
s.SetPort(8199)
s.Run()
}

访问一下:http://localhost:8199/web/

image.png

配置下打包参数

众所周知go交叉编译非常方便,而使用gf之后更加方便,只需要在配置文件配置一下目标架构和系统,直接一个build命令即可打包出所有的标架构和系统可执行文件,这里直接创建个配置文件 config.yaml

1
2
3
4
5
6
gfcli:
build:
name: "gf"
arch: "amd64"
system: "linux,windows"
path: "./dist/"

然后直接一个 gf build

image.png

就这么简单,部署当然也就一个可执行文件丢上去就完事了!

当然这样只是为了搞个小程序玩玩,正经部署最好还是用nginx哦

相信很多小伙伴在vue转uniapp或者微信小程序的时候都会想过想简单的一个函数调用一些信息提示的功能,比如:

image.png

image.png

熟悉uniapp的小伙伴肯定知道有showToast这个方法,但是这个方法的限制很大,比如限制图标、限制文本字数等等。

当然,uniapp的一些组件库也提供了类似的功能,比如uview的Toast组件,提供了更丰富的样式和功能。但是像这种组件都必须在每个用到的页面都写上这么一个组件,那有没有什么办法像上面的element一样直接一个方法搞定呢?

众所周知小程序的模板不支持动态修改dom,也就不能像web一样往body里面append一个dom,那么怎么实现呢?

我的思路是像上面的naive-ui一样在页面内容外统一使用这个组件,至于怎么实现呢,我相信聪明的你肯定能想到布局,只需要每个页面都使用这个布局不就好了,这时候我们就需要使用我们的布局插件!

@uni-helper/vite-plugin-uni-layouts

这个插件为我们提供了页面布局的功能,详情使用方法可进去查看readme,安装好这个插件那么就能在默认模板里面加入我们想要的任何组件了。

但是我们如何在任意地方使用这个toast呢?

用过微信小程序的小伙伴都知道有个globalData的东西,能够整个app共用一个对象用于存储数据,而uniapp也为我们提供了这个功能,写h5、app的小伙伴也不用担心,uniapp为我们兼容了所有端。

那么我们只需要在进入每个页面的时候,把当前页面的Toast组件的ref存到globalData不就可以了吗?

image.png

需要留意的是,因为我们有很多个页面,所有在页面切换的时候,需要把当前页面布局的Toast组件的ref存下来,所以需要onShow这个生命周期钩子。

又因为onShow应该是要比组件mount的时机要早的,所以第一次执行onShow回调的时候,这里的uToast.value是undefined的,所以我们需要补一个onMounted的生命周期钩子,保证第一次加载完组件能拿到ref。

在其他地方使用

首先第一个是封装成一个方法并且标明入参类型提供其他页面去使用:

image.png

image.png

这样我们就能在任意地方使用我们在布局里的东西了,包括咱们的网络请求拦截器:

image.png

只能说,这个插件真的是,太裤辣

最后这是我的模板,详情代码可以看这个仓库: https://github.com/MatrixCross/Vue3-Uniapp-Starter

unjs是github上面一个挺多人关注的组织,组织下有很多很好用的工具包,搜了一下掘金好像并没有相关的详细介绍,然后就想着为他们介绍一下(水一篇文章)。

这里按照github上面的高星进行排名,下面开始咯。

阅读全文 »

虽然说很多人喜欢MacBookPro来做开发,但是对于很多人来说MBP的价格可不便宜,而MBA我觉得又是个电子垃圾,那么Windows的选择也很不错。我现在用的鸡哥的14X,机子本身3200,64G内存条1200,机子本身自带24G的两根内存条,如果需要加内存可以咸鱼300-400左右出掉,然后整机基本4000出头(上一个本子是ThinkBook14p 13500h 32g因为内存不够用所以换了)

image.png

基础美化

首先是Win11的任务栏默认居中,我不太习惯,可以右键任务栏-任务栏设置-任务栏对齐,选择靠左

image.png

然后是终端,终端推荐使用Terminal,然后安装oh-my-posh,主题我选择的是owl-night,字体是JetBrain Mono NF,记得字体需要下载NF字体,否则图标不能正常显示,当然Terminal还可以设置一些背景图片啥的,把二次元老婆/女神放上去也不错

image.png

安装方法如没有魔法的话可以从Windows的应用商店下载安装:

image.png

对于大部分开发者来说终端美化基本就够了,如果大家有什么好的建议可以评论区说一下。

基础开发环境

前端

因为我主要还是前端开发,所有这里重点说说前端我用到的一些软件,首先重中之重就是nvm-windows,下载地址是:https://github.com/coreybutler/nvm-windows/releases

然后我安装的时候会选择node下载到D盘,然后node的链接也是D盘:

image.png

image.png

安装完成之后可以设置下淘宝源下载node和npm:

nvm node_mirror https://npmmirror.com/mirrors/node/
nvm npm_mirror https://npmmirror.com/mirrors/npm/

装完node之后可以安装一些常用工具:npkill、tsx、taze、nrm

然后开发软件我喜欢WebStorm,WS已经在10月份公布免费非商用了(可以不用破解了),因为我喜欢JB全家桶,如果你也喜欢的话推荐使用JetBrainsToolbox

image.png

然后如果你的C盘比较少的话,推荐把全家桶安装在其他盘:

image.png

Java、Go等开发

对于这些我的推荐都是使用JB全家桶,因为他们都自带了对应的开发环境配置功能:

image.png

image.png

对应的java、go版本都可以直接在IDE里面去下载使用,不用自己跑到网上下载安装,而且这样安装我感觉是非常绿色的,洁癖狂喜!

对于rust开发的话可以用rustup安装,编译后端推荐使用msvc生成工具而不是VS:

https://visualstudio.microsoft.com/zh-hans/downloads/

image.png

其他的一些常用开发软件

ssh软件可以选择Xshell,自从6以后Xshell已经可以免费使用了,只需要邮箱校验一下就能一直用了。

全盘搜索我推荐Everything,如果你用Everything比较多的话可以安装个Everything Toolbar,将Toolbar加到任务栏会更加方便。

如果本地需要跑nginx、mysql、redis这些我常用的是小皮面板,安装方便只需要一个软件就给你把所有基础中间件跑起来,如果是新手的话强烈推荐!

image.png

虚拟机我更喜欢的是Vmware Workstation Pro而不是WSL/Hyper-V,这个软件也是从17开始就可以免费用了,不需要输入注册码了,然后Linux我可能比较喜欢Ubuntu、Deepin、Fedora这些开箱即用的。

Api调试、Api文档管理、Api自动化测试/压测:Apifox

其他的一些日常软件

办公三件套软件我推荐MS office,因为我对WPS的印象是很多东西需要收费而且有广告而且界面不好看所有推荐MS office,可以使用Office Plus Tools安装:https://otp.landian.vip/zh-cn/

解压缩软件推荐:7-Zip、Bindizip6.25版本(新版本有广告)

视频播放器:PotPlayer,官网:https://potplayer.daum.net/?lang=zh_CN

图片预览:HoneyView,官网:https://www.bandisoft.com/honeyview/

各种各样的工具集合:uTools

原文:How to Prerelease an npm Package

最近,我们彻底修改了共享的 ESLint 配置,在测试过程中,我需要发布一个 alpha 版本。我知道这是可能的,但我完全不知道该怎么做。幸运的是,一旦你知道怎么做,这其实很简单。

阅读全文 »

关注了这么久的装饰器提案终于进入到Stage3阶段了,掘金站内也有简单用法介绍版本:JS装饰器(Decorators)用法-Stage3(新),这篇就根据提案详情翻译一下。

装饰器(Decorators)

Stage: 3

装饰器是一项用于扩展 JavaScript 类的提案,在转译器环境中得到了开发者的广泛采用,并且在标准化方面引起了广泛兴趣。TC39 已经在装饰器提案上迭代了五年多。本文档描述了一个基于所有过去提案元素的新装饰器提案。

本 README 文件描述了当前的装饰器提案,该提案仍在进行中。有关该提案之前的迭代版本,请参阅本仓库的提交历史

阅读全文 »

系统工程与信息系统基础

系统工程

系统工程的概念

系统工程利用计算机作为工具,对系统的结构、元素、信息和反馈等进行分析,以达到最优规划、最优设计、最优管理和最优控制的目的。

从整体出发、从系统观念出发,以求整体最优。

阅读全文 »
0%