diff --git a/src/actions/actions-app.ts b/src/actions/actions-app.ts
new file mode 100644
index 0000000..8b6564e
--- /dev/null
+++ b/src/actions/actions-app.ts
@@ -0,0 +1,89 @@
+// 1. get the repo's contributors list
+// 2. generate svg
+// 3. save to local
+// 4. 根据仓库地址,会保存头像在本地,如果通过actions 触发后,通过对比,本地存在头像则不会再更新
+// 4.1 第4点中,bug:如果用户更新,则还是旧头像。出现用户头像,除非用头像生成 md5 存储作为指纹
+// import { http } from 'node:http'
+
+import {writeFile} from 'node:fs/promises';
+import {chunk} from 'lodash';
+import {svgStart, svgEnd, asyncHandleUsersSVG, asyncHandlerUserDefsSVG} from './svg';
+import {UserConfig, UserItem} from '../types';
+import {getOwnerRepo} from '../utils';
+
+import config from '../../config'
+import console from 'node:console';
+
+// global constants
+const SVG_WIDTH = 800;
+const SVG_HEIGHT = 370;
+const FONT_SIZE = 0
+const BASE_SIZE = 100
+
+export const generateUserListSVG = async (userList: UserItem[], config: UserConfig) => {
+
+ // split => two-dimensional array
+ let splitList: UserItem[] | UserItem[][] = userList
+
+ const svgWidth = config.width || SVG_WIDTH
+ const svgHeight = config.height || SVG_HEIGHT
+ const baseSize = config.size || BASE_SIZE
+ const fontSize = config.fontSize || FONT_SIZE
+
+ const isRadius = config.isRadius !== false // default need radius => 50%;
+ const outSize = fontSize + baseSize
+ const oneRowMax = Math.floor(svgWidth / outSize)
+
+ // split new array
+ if (userList.length > oneRowMax) {
+ splitList = chunk(userList, oneRowMax)
+ }
+
+ const svgConfig = {
+ baseSize,
+ fontSize,
+ oneRowMax,
+ outSize,
+ svgWidth,
+ svgHeight,
+ isRadius,
+ }
+
+ // radius
+ if (config.isRadius === undefined || config.isRadius === true) {
+ return `${svgStart(svgWidth, svgHeight)}
+
+ ${await asyncHandlerUserDefsSVG(splitList, svgConfig)}
+
+
+${await asyncHandleUsersSVG(splitList, svgConfig)}
+${svgEnd()}
+ `
+ } else if (!config.isRadius) {
+ const userBlockData = await asyncHandleUsersSVG(splitList, svgConfig)
+ return `${svgStart(svgWidth, svgHeight)}
+ ${userBlockData}
+${svgEnd()}
+ `
+ }
+ return ''
+}
+
+export const saveSVG = async (ownerRepo: string, userConfig: UserConfig, repoUserList: UserItem[]) => {
+ console.log('saveSVG=>', ownerRepo, repoUserList.length)
+ const {owner, repo} = getOwnerRepo && getOwnerRepo(ownerRepo) || {}
+ if (!owner || !repo) {
+ console.error('Invalid repo address:', ownerRepo)
+ return
+ }
+ try {
+ const svgStr = await generateUserListSVG(repoUserList, userConfig)
+ const filename = `./repos/${owner}/${repo}.svg`
+ await writeFile(filename, svgStr, {encoding: 'utf-8'})
+ } catch (error) {
+ console.error('err=>', error)
+ }
+}
+
+// saveSVG('veaba/contributors',listTen)
+// saveSVG('vuejs-translations/docs-zh-cn',listTen)
diff --git a/src/actions/actions-github.ts b/src/actions/actions-github.ts
new file mode 100644
index 0000000..a182a0d
--- /dev/null
+++ b/src/actions/actions-github.ts
@@ -0,0 +1,63 @@
+import axios from "axios"
+
+import { createWriteStream } from 'node:fs'
+import { resolve } from 'path'
+import { UserConfig, UserItem } from "../types"
+import { sortBy } from 'lodash'
+import { data } from '../../tests/mock'
+import { getTotalList, getOwnerRepo } from "../utils"
+
+
+
+
+/**
+ * get contributors avatar to public/avatars
+ *
+ * 1、TODO check local has been save ?
+ *
+ * 2、TODO 通过存在,则通过 md5 判断,一致则略过
+ *
+ * 3、TODO 不一致则拉取
+ *
+ * 4、TODO if not, get remote data and save to public/avatars
+ *
+ * 5、TODO 存储一份本地 avatars 映射的 md5 list
+ *
+*/
+export const getRepoData = async (repoKey: string, repoConfig: UserConfig): Promise => {
+ const { owner, repo } = getOwnerRepo(repoKey)
+
+ let repData = []
+ try {
+ const resp = await axios.get(`http://api.github.com/repos/${repoKey}/stats/contributors`)
+ if (resp?.data) {
+ repData = resp.data || []
+ }
+ } catch (err) {
+ console.error('get repo stats contributors err=>')
+ }
+ repData = getTotalList(data, repoConfig)
+
+ const sortList = sortBy(repData, (item) => -item.total)
+ return sortList
+}
+
+
+export const downloadAvatar = async (userItem: UserItem) => {
+ try {
+ const resp = await axios.get(`https://avatars.githubusercontent.com/u/${userItem.id}?v=4`, {
+ responseType: "stream"
+ })
+
+ if (resp?.status === 200) {
+ const writer = createWriteStream(resolve(__dirname, `../public/avatars/${userItem.id}.jpg`),)
+ resp.data.pipe(writer)
+ return new Promise((resolve, reject) => {
+ writer.on('finish', resolve)
+ writer.on('error', reject)
+ })
+ }
+ } catch (err) {
+ console.error('err=>', err)
+ }
+}
diff --git a/src/actions/actions-image.ts b/src/actions/actions-image.ts
new file mode 100644
index 0000000..df3f180
--- /dev/null
+++ b/src/actions/actions-image.ts
@@ -0,0 +1,5 @@
+import { readFile } from 'node:fs/promises'
+export const imagePathToBase64 = async (imagePath: string) => {
+ const imageBuffer = await readFile(imagePath)
+ return imageBuffer.toString('base64')
+}
diff --git a/src/actions/actions-index.ts b/src/actions/actions-index.ts
new file mode 100644
index 0000000..4b2bc6c
--- /dev/null
+++ b/src/actions/actions-index.ts
@@ -0,0 +1,47 @@
+// import config from '../../config'
+// import { resolve } from 'path'
+// import { saveSVG } from './app';
+// import { writeFile, access } from "node:fs/promises" // access
+// import { downloadAvatar, getRepoData } from './github';
+// import { MD5Item, UserItem } from '../types';
+// // import { isHasFile, readMD5 } from "../utils"
+//
+// import md5JSON from '../../public/avatars/avatarsMD5.json'
+//
+// const md5s: any = md5JSON
+//
+// interface TypesContributors {
+// repo: string;
+// }
+//
+// const updateAvatars = async (sortList: UserItem[]) => {
+// await Promise.all(sortList.map(async userItem => {
+//
+// const isHas = await isHasFile(resolve(__dirname, `../public/avatars/${userItem.id}.jpg`))
+// if (!isHas) {
+// return await downloadAvatar(userItem)
+// }
+// }))
+// }
+//
+// // export const serverStart = async () => {
+// // const now = Date.now()
+// // console.log('开始=>', new Date())
+// //
+// // console.time('task=>')
+// // const repos = Object.keys(config)
+// // // const reposConfigs = Object.values(config)
+// //
+// // await Promise.all(repos.map(async repo => {
+// // const repoConfig = config[repo]
+// // const repoList = await getRepoData(repo, repoConfig)
+// // await updateAvatars(repoList)
+// // await saveSVG(repo, repoList)
+// // }))
+// //
+// // console.timeEnd('task=>')
+// // console.log('结束=>', new Date(), Date.now() - now)
+// //
+// // }
+//
+// // serverStart()