木头的博客

我是木头 有些想法 有点精力

注:本文不会涉及任何算法,只通俗地分享自己的感悟,若有事实错误请直接批评指正,感谢大家。

0. 序

最近深度体验了 AI 绘图 Stable Diffusion,结合正在发生的经济危机/裁员/通缩,我产生了一些思考:AI 大模型时代的到来,到底会淘汰哪些人?哪些领域会受到影响?AI 带来哪些隐私、道德和安全问题?作为受到直接影响的底层程序员/设计师/画师/写手,我们真的只能坐以待毙吗?

我的结论是:第三产业大部分领域都会受到影响 (实体产业影响较小)。

第一产业是指农、林、牧、渔业。

第二产业是指采矿业,制造业,电力、燃气及水的生产和供应业,建筑业。

第三产业是指除第一、二产业以外的其他行业。第三产业包括:交通运输、仓储和邮政业,信息传输、计算机服务和软件业,批发和零售业,住宿和餐饮业,金融业,房地产业,租赁和商务服务业,科学研究、技术服务和地质勘查业,水利、环境和公共设施管理业,居民服务和其他服务业,教育,卫生、社会保障和社会福利业,文化、体育和娱乐业,公共管理和社会组织,国际组织。

1. AI 绘图现在发展到什么程度了?

其实半年前(2023年3月左右)我就接触了 Stable Diffusion (一个开源的 AI 绘图项目),那时候基于扩散模型的 AI 绘图才刚发展起来,对我有一定的冲击,毕竟这种通过文字转化为图像的方式是革命性的。 但受限于显卡,只画了一些简单的内容,画出来的人也是比较挫。

早期的 AI 绘图模型 (来自 Stable Diffusion 1.5 大模型)

随着时间推移,各种 Chat 类应用,Code 补全类的 AI 工具层出不穷,我意识到 AI 时代已经来临,于是向老婆申请购买了一张二手的 3090 显卡,这张卡的优势在于有 24G 大显存,玩过 AI 绘图的小伙伴应该知道,显存越大,能画出来的原始图像分辨率就越高,训练时每一次迭代也可以同时训练更多图像。

拿到显卡后,我就陆续开始了对 AI 绘图的探索,先是沉溺与画各种漂亮妹子,然后绘制各种风格的图像,比如幻想风格、像素画风、游戏素材等图像。

真实系漂亮妹子
真实系风格 (来自 majicmix realistic 大模型)
奇幻风格 (来自 majicmix lux 大模型)
像素风格 (来自 Pixel Art LoRa 模型)
游戏素材 (左:Game Icon Research LoRa 模型 / 右:Character Design Sheet LoRa 模型)

后来不满足于画各种乱七八糟的东西,开始去各大网站搜罗 SD 应用方面的教程,陆续学习了照片高清放大、照片动漫化、AI 艺术字、光影重绘、换脸换装等各种应用。

AI 高清放大 (SD 高清重绘)
真人转动漫风格 (Disney 大模型重绘)
AI 艺术字 (ControlNet Depth 模型)
创意二维码 (ControlNet QRCodeMonster 模型)
光影重绘 (ControlNet Tile 模型)

再后来又用爱人的照片做数据集,训练她自己的真人模型,效果也相当不错,她本人看到都惊叹于其相似度。

阅读全文 »

环境变量

命令 作用
printenv 打印所有环境变量

输出

命令 作用
echo 输出

echo

输入

输入参数

读取参数时,使用 $0..$9 ${10} 等获取参数

其中 $0 是程序名称

add.sh
1
2
3
4
5
6
7
8
9
10
11
12
# !/bin/bash

name=`basename $0`

echo "the cmd is $0"
echo "the cmd basename is $name"
echo "The #1 param is $1"
echo "The #2 param is $2"

SUM=$[ $1 + $2 ]

echo "The result is $SUM"

输出

1
2
3
4
5
6
7
➜  ~ chmod +x add.sh
➜ ~ ./add.sh 1 2
the cmd is ./add.sh
the cmd basename is add.sh
The #1 param is 1
The #2 param is 2
The result is 3

特殊变量

还有几个特殊的变量可供使用

  • echo $# 输出参数的个数
  • echo $* 输出参数变量
  • echo $@ 输出参数列表(可用 for in 遍历)

下面的例子来判断变量参数是否符合要求

1
2
3
4
5
6
# !/bin/bash

if [ $# -lt 2 ] # $#获取输入个数,选项-lt表示less than
then
echo "please input at least 2 param"
fi

shift 移动参数位置

使用 shift 命令将参数队列的第一个参数移出 所有参数位置向前移动

sum.sh
1
2
3
4
5
6
7
8
9
10
11
# !/bin/bash

result=0

while [ -n "$1" ] # 通过循环,每次读取$1位置参数
do
result=$[ $result + $1 ] # $1位置参数与原参数和累加
shift # 把$2位置参数向前移动到$1,原$1位置参数不可用
done

echo "sum of result is $result"

read 读取键盘输入

read [-s] [-t timeout] [-n num] [-p prompt] [variables...]

  • -p prompt 提示语句
  • -s 静默输入(例如输入密码)
  • -t timeout 设置输入超时时间,如果超时则以非 0 状态退出
  • -n num 限制输入字数,如果设置为一则只需要按下一个字母,无需按回车键
  • variabels 变量名 可以制定多个, 如果不指定会将输入将存入REPLY变量中
1
2
3
4
5
6
# !/bin/bash

echo -n "Enter your name:" # 参数-n的作用是不换行,echo默认换行
read name # 把输入放入变量name
echo "hello $name,welcome to my program" # 显示输入信息
exit 0

交互式菜单

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
#!/bin/bash

echo "Please Select:
1.Display System Information
2.Display Disk Space
3.Display Home Space Utilization
0.Quit"
read -p -n 1 "Enter selection [0-3] >" num

if [[ $num =~ ^[0-3]$ ]]; then
if [[ $num == 0 ]]; then
echo "Program terminated"
exit;
fi

if [[ $num == 1 ]]; then
echo "Hostname :$HOSTNAME"
uptime
exit
fi

if [[ $num == 2 ]]; then
df -h
exit;
fi

if [[ $num == 3 ]]; then
if [[ $(id -u) -eq 0 ]]; then
echo "Home Space Utilization(All Users)"
du -sh /home/*
else
echo "Home Space Utilization($USER)"
du -sh $HOME
fi
fi
else
echo "Invalid entry." >&2
exit 1
fi

自动输入

我们在写 shell 脚本时会遇到需要进行手动输入的命令,如 sudo passwd 等需要等待输入密码,在 shell 脚本中如何进行输入呢?

passwd username 为例

1
2
3
4
5
6
#!/bin/bash

passwd username << EOF
123456
123456
EOF

条件

并且 (AND)

1
2
3
4
5
6
7
if [ c1 -a c2 ]; then
...
fi

if [ c1 ] && [ c2 ]; then
...
fi

或 (OR)

1
2
3
4
5
6
7
if [ c1 -o c2 ]; then
...
fi

if [ c1 ] || [ c2 ]; then
...
fi

利用 test 命令进行判断

参数 作用
-e file 判断 file 是否存在 (exist)
-f file 判断 file 是否存在并且是 file
-d file 判断 file 是否存在并且是 directory
-S file 判断 file 是否存在并且是 socket
-L file 判断 file 是否存在并且是 symbolic link
-r file 判断 file 是否存在并且可读 (readable)
-w file 判断 file 是否存在并且可写 (writeable)
-x file 判断 file 是否存在并且可执行 (executable)
-s file 判断 file 是否存在并且文件大小 (size) 非零
file1 -nt file2 判断 file1 存在并且比 file2 新 (newer than)
file1 -ot file2 判断 file1 存在并且比 file2 旧 (older than)
file1 -ef file2 判断 file1 和 file 2存在并且是同一个文件 (exist & same file)
string 判断 string 是空字符串
-n string 判断 string 长度为非零 (non-zero)
-z string 判断 string 长度为零 (zero)
s1 = s2 判断字符串 s1 和 s2 相同
s1 != s2 判断字符串 s1 和 s2 不同
s1 < s2 / s1 > s2 判断字符串 s1 的二进制值比 s2 小 / 大
! expiression 取表达式的非值
expiression1 -a expression2 判断表达式1 和(and) 表达式2 都为真
expiression1 -o expression2 判断表达式1 或(or) 表达式2 为真

注意:

  1. 如果文件是一个符号链接 (symbolic link),它将会指向链接的真实文件进行判断,除非指定了 -L-n 参数.
  2. -a-o 同时使用时,-a 的运算优先级比 -o 高.

利用 [] 判断符号进行判断

  • 判读符号 [] 的使用方法与 test 命令一致。
  • [] 方括号内的每个元件都要以空格分隔,这包括方括号本身。

    [ -z foo ] [ "$name" == "foo bar" ]

  • 如果需要引入变量,变量最好使用双引号包裹,否则变量中出现的空格将会作为判断符号的元件被分隔。

    试着体会 [ -z "foo bar" ][ -z foo bar ] 的区别

1
2
3
[ -z abc -a -w file] # => true
[ (-z abc) -a (-w file)] # => true
[ -z abc ] && [ -w file] # => false

利用 command 命令进行命令判断

参数 作用
-e 判断文件是否存在
-v 判断命令是否存在

判断语句 if, if [ ], if [[ ]] 有什么区别?

  1. if 这是shell的基本形式,要求命令返回0表示真,非0表示假,比较简单。例如:

    1
    2
    3
    if cmd; then
    echo "True"
    fi
  2. if [ ] 这是shell的扩展形式,使用 [] 进行条件测试。例如:

    1
    2
    3
    if [ $a -eq $b ]; then
    echo "a 等于 b"
    fi
  3. if [[ ]] 这是shell的新形式,使用 [[ ]] 进行条件测试。它有以下优点:

    • 支持正则表达式匹配
    • 不需要在条件表达式内部加空格
    • 支持 && 和 || 逻辑操作
    • 支持字符串比较

    例如:

    1
    2
    3
    4
    5
    6
    7
    if [[ $a = "b" ]]; then
    echo "a 等于 b"
    fi

    if [[ $a -eq $b ]] && [[ $c -gt $d ]]; then
    echo "条件满足"
    fi

综上,如果shell版本支持,推荐使用 if [[ ]] 形式进行条件判断,它有更多特性和优点。if [ ] 形式虽然也可用,但需要注意内部空格问题。if 命令形式最简单,但实用性较差。

函数

函数的定义格式如下

1
2
3
4
[function] funname [()] {
action;
[return int;]
}
1
2
3
4
5
6
7
8
9
function fn {
echo "hello world!"
return 0
}

say_hi() {
echo $1
}
say_hi jack
参数 说明
$1 $10 ${11} 1, 10, 11 个参数
$# 传递到脚本或函数的参数个数
$* 以一个单字符串显示所有向脚本传递的参数
$@ $* 相同,但是使用时加引号,并在引号中返回每个参数。
$$ 脚本运行的当前进程 ID 号
$! 后台运行的最后一个进程的 ID 号
$- 显示 Shell 使用的当前选项,与 set 命令功能相同。
$? 显示最后命令的退出状态。0 表示没有错误,其他任何值表明有错误。

小技巧

获取当前 Git 分支名

1
2
3
4
# 获取当前 ref 名
ref_name=$(git symbolic-ref -q HEAD) # => refs/heads/master
# 利用替换命令
branch_name=${ref_name##refs/heads/} # => master

参考资料

标题党,勿全信;
这是一篇关于前端单元测试的优化心得,讲述了一个中型团队坎坷的痛与泪的故事;
长文预警,建议当短篇小说看;
干货在末尾。

就在前几天,我拿到了心心念念的 MacBook M1 Pro,上手第一件事就是跑一下项目上的前端测试,期待着久仰大名的 M1 处理器一显神威。

为什么这么着急用 M1 跑测试?这个故事要从半年前说起。

1. 越来越痛的测试

小半年前,项目规模越来越大,随着十几轮冲刺后,我们有两个前端库的代码量和测试也越来越多,于是出现了上面的对话

不知从什么时候开始,项目代码里的测试成了我们的痛点,推代码时会触发全量的测试,而在这推代码的10多分钟里,我是什么都做不了的,电脑卡的要命,不得不站起来去接杯水或上个厕所。

从某种角度来说,也挺好,能时不时的提醒我站起来活动活动,放松一下肩颈和眼睛,看看博客摸摸鱼(不是)。

但问题是 Pipeline 上的速度也是稳定慢,从开始 Build 到 Deploy to Dev,更是达到了离谱的 30 分钟左右,Pipeline 甚至还触发了客户设定的 300 小时/月的限流。

2. 法外狂徒

git push --no-verify 成了我们项目的常态。

阅读全文 »

最近把家里的网络折腾了一下,整体用下来感觉还不错,这里记录下来分享给大家,大家一起学习交流~

Infrastructure

痛点:

  • 科学上网。现在家里的网络总是觉得有些不尽人意,本人最大的爱好就是周末宅在家里搞搞开源、打打游戏。遇到问题了 Google / StackOverflow 效率是比 Baidu 高,而每个设备自己科学上网比较繁琐,需要各自管理白名单,太麻烦…
  • NAS 访问慢。和爱人出去玩偶尔会拍一些照片和 Vlog,这些都是美好珍贵的记忆,目前存在自己的私有云上,想看了拿出来看看,照片列表加载缓慢,视频进度拖拉卡顿…
  • WIFI 信号。之前路由器是放在客厅,信号到卧室床头比较曲折,有时关门后甚至断网,网络体验极差…

折腾后:

  • 全屋设备科学上网。在软路由里根据 IP 地址分流,国外 IP 走代理,国内直连。终端无需单独开软件,无需在意是否国内外网站。
  • 广告过滤。同上。
  • 内网全千兆访问。访问 NAS 资源、各设备之间文件互传速度大幅提升,堪比本地访问。
  • Docker。开发时如果需要容器资源,内外网都可使用路由器的 Docker,节约开发机资源,目前资源是 4C8G。
  • 数据安全。之前 NAS 是直接暴露在公网的,且没有开启 HTTP,现在公网访问内网设备需要 VPN,访问 NAS 强制 HTTPS。
  • 扩大 WIFI 信号覆盖。未来搬去新家后智能设备增多,WIFI6 是必不可少的。出于成本考虑,WIFI6 AP 放在客厅,现有垃圾 AP 放在卧室。

硬件改造成本共 1135 元

  • 软路由: J4105,8G DDR4 + 64G SATA固态 咸鱼 750 元
  • 交换机: 水星SG108C 塑料8口 JD 69元
  • AP: 华为AX3Pro 咸鱼 295 元
  • 网线: 绿联6类线 0.5m x3 JD 21元

哭,现在设备各种涨价, ax3pro 暴涨 150 元,蹲了二周才蹲到低于 300 元的 AP,J4105 也涨了 100 多,不知道是不是受了这一波矿潮影响…

总之,想把网络环境做好,最大的变化就是没有变化,让用户感觉不到就成功了,只需要默默地提供稳定的服务就好~

阅读全文 »

Vite2 发布有一段时间了,其内部使用了 ESBuild 进行打包,ESBuild 有着相对 Webpack 惊人的打包速度。

esbuild benchmark 数据来自 github.com/evanw/esbuild

React 用户表示令人羡慕,不过 Vite 野心比较大,不止可以用来开发 Vue 项目,类似 Rollup,Vite2 可以使用插件来支持 React 项目。

这就巧了,手上刚好有一个 React 练习项目,使用 Create React App 创建并且进行过 Eject。接下来我们就用这个 React 项目来练练手吧,享受一下飞一般的构建速度!

阅读全文 »

变量

字符串

字符串可以用单引号,也可以用双引号,也可以不用引号。

单引号

单引号里任何字符都会原样输出,变量也是无效的,也无法对单引号进行转义。

1
str='this is a string'

双引号和字符串的拼接

双引号就可以随意的进行转义啦!

1
2
3
4
5
name="mutoe"
greeting="hello, $name !" # => hello, mutoe !
greeting="hello, "$name" !" # => hello, mutoe !
greeting="hello, ${name}!" # => hello, mutoe!
quote_greeting="hello, \"$name\" !" # => hello, "mutoe" !

获取字符串长度

1
2
str='abcd'
echo ${#abcd} # => 4

获取子字符串

1
2
str='abcdefg'
echo ${string:1:4} # => bcde

分割字符串

1
2
3
4
5
6
str="a,b,c,d"
arr=$(echo $str | tr ";" "\n")
for item in $arr
do
echo $item
done

数组

数组使用小括号定义

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
# 声明数组
array=(1 2 a b "foo")
array[10]=bar

# 读取数组
echo ${array[3]} # => b
echo ${array[9]} # =>
echo ${array[10]} # => bar
echo ${array[*]} # => 1 2 a b foo bar
echo ${array[@]} # => 1 2 a b foo bar

# 读取数组长度
echo ${#array[*]} # => 6

# 读取字符串元素长度
echo ${#array[10]} # => 3

# 删除元素
unset array[1]
echo ${array[*]} # => 1 a b foo bar
echo ${#array[*]} # => 5

# 拼接数组
new_arr=(0 ${array[*]} z)
echo ${new_arr[*]} # => 0 1 a b foo bar z
echo ${new_arr[6]} # => z

# 删除数组
unset new_arr
echo $new_arr # =>

关联数组

Bash shell 中可以使用字符串作为数组的下标,类似 Map 对象

1
2
3
4
5
6
declare -A color
color["red"]="#ff0000"
color["green"]="#00ff00"
color["blue"]="#0000ff"

echo $color

条件

if

1
2
3
4
5
6
7
8
9
if condition
then
...
elif condition2
then
...
else
...
fi

实际上,if 检测的是一条命令的退出状态。

Example
1
2
3
4
5
a=$[2*3]
b=$[1+5]
if [ $a == $b ]; then
echo "a == b"
fi

case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case $var in
1)
command1
command2
;; # break (required)
$const|2) # or
command1
command2
;;
*) # default
command1
command2
;;
esac

注释

# 开始的部分就是注释, sh里没有多行注释,只能每行加一个 #

参考资料

第二章我们简单的介绍了下如何连接测试数据库,这篇我们将结合 TDD 来完整的实现注册和登录功能。(如果接下来我写的内容你已经做过,可以跳过该步)

1. 编写第一个集成测试

按照 Nest 提供的示例 E2E 测试, 位于 test/app.e2e-spec.ts 应该长这样

test/app.e2e-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Test, TestingModule } from '@nestjs/testing'
import * as request from 'supertest'
import { AppModule } from './../src/app.module'

describe('AppController (e2e)', () => {
let app

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile()

app = moduleFixture.createNestApplication()
await app.init()
})

it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!')
})
})

现在是运行不了的,不过没关系,我们稍稍改造一下

首先我们测试一个用于检查健康的接口 /hello, 无需导入整个 AppModule, 只需导入 AppController ,改造为下列的样子

app.e2e-spec.ts
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
import { Test, TestingModule } from '@nestjs/testing'
import { AppController } from 'app.controller'
import * as request from 'supertest'

describe('app module', () => {
let app

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
controllers: [AppController],
}).compile()

app = moduleFixture.createNestApplication()
await app.init()
})

afterAll(async () => {
await app.close()
})

it('/hello (GET)', () => {
return request(app.getHttpServer())
.get('/hello?name=world')
.expect(200)
.expect('Hello world!')
})
})

然后执行

1
yarn test:e2e

yarn e2e test

阅读全文 »

这一节, 我们引入 Swagger 来自动根据代码里的注脚来生成接口文档。

Nest 为我们提供了一个专用的模块来搭配 Swagger 来使用

1. 安装依赖

1
yarn add @nestjs/swagger swagger-ui-express

2. 初始化 Swagger 模块

在我们的应用入口文件 main.ts 中添加一个 createSwagger 方法, 并在 bootstrap 方法中初始化它

main.ts
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
import { INestApplication, ValidationPipe } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
import { AppModule } from 'app.module'

function createSwagger(app: INestApplication) {
const version = require('../package.json').version || ''

const options = new DocumentBuilder()
.setTitle('Nestjs Realworld Example App')
.setVersion(version)
.addBearerAuth()
.build()

const document = SwaggerModule.createDocument(app, options)
SwaggerModule.setup('/docs', app, document)
}

async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe())

if (process.env.SWAGGER_ENABLE && process.env.SWAGGER_ENABLE === 'true') {
createSwagger(app)
}

await app.listen(3000)
}

bootstrap().catch((err) => console.error(err))

createSwagger 中, 我们首先读取了来自 package.json 中的版本号来作为接口的版本

然后我们设置了 Title 和 Bearer 鉴权认证入口, 我们还设置了 /docs 为我们文档的入口

最后,我们判断环境变量中的 SWAGGER_ENABLE 是否打开, 如果打开我们就初始化 Swagger 文档系统。

.env.env.template 中增加 SWAGGER_ENABLE=true, 然后启动服务器

访问 http://localhost:3000/docs 就能看见我们的接口文档创建好啦!

swagger preview

阅读全文 »

这里记录了一些个人学习 Flutter 时遇到的一些问题, 可以作为避免踩坑和速查手册. 如有疑问欢迎留言.

阅读全文 »

上一章中, 我们完成了鉴权功能, 也就是 Auth 模块, 顺便简单实现了下注册和登录功能. 这一章我们就来正式的将登录和实现功能做完, 含有完整的数据校验和转化.

在正式的开始完善功能之前, 我们现清理重构一下已有的代码.

1. 重构代码

1.1 整理路由

我们上一张的登录和注册是写在 app.controller.ts 中的, 如果所有的路由都写在这里的话就会比较杂乱, 所以我们将它移动到 Auth 模块下.

首先生成 AuthController

1
nest g controller auth

然后将 /auth/register/auth/login 移到 AuthController 下

auth.controller.ts
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
import { Body, Controller, Post, Request, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { UserService } from '../user/user.service'
import { AuthService } from './auth.service'

@Controller('auth')
export class AuthController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {}

@Post('/register')
async register(
@Body() requestBody: { email: string; username: string; password: string },
) {
const user = await this.userService.createUser(requestBody)
const token = this.authService.generateToken(user.id, user.username)
return {
user: { ...user, token },
}
}

@UseGuards(AuthGuard('local'))
@Post('/login')
async login(@Request() req) {
const { user } = req
const token = this.authService.generateToken(user.id, user.username)
return {
user: { ...user, token },
}
}
}

顺便也移动下测试代码, 这里就不再赘述了.

阅读全文 »
0%