-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e2ba5d9
Showing
61 changed files
with
10,321 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": ["next/core-web-vitals", "prettier"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
name: Publish Docker image | ||
|
||
on: | ||
workflow_dispatch: | ||
release: | ||
types: [published] | ||
|
||
jobs: | ||
push_to_registry: | ||
name: Push Docker image to Docker Hub | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Check out the repo | ||
uses: actions/checkout@v3 | ||
- name: Log in to Docker Hub | ||
uses: docker/login-action@v2 | ||
with: | ||
username: ${{ secrets.DOCKER_USERNAME }} | ||
password: ${{ secrets.DOCKER_PASSWORD }} | ||
|
||
- name: Extract metadata (tags, labels) for Docker | ||
id: meta | ||
uses: docker/metadata-action@v4 | ||
with: | ||
images: sunls24/chat-ai | ||
tags: | | ||
type=raw,value=latest | ||
type=ref,event=tag | ||
- name: Set up QEMU | ||
uses: docker/setup-qemu-action@v2 | ||
|
||
- name: Set up Docker Buildx | ||
uses: docker/setup-buildx-action@v2 | ||
|
||
- name: Build and push Docker image | ||
uses: docker/build-push-action@v4 | ||
with: | ||
context: . | ||
platforms: linux/amd64,linux/arm64 | ||
push: true | ||
tags: ${{ steps.meta.outputs.tags }} | ||
labels: ${{ steps.meta.outputs.labels }} | ||
cache-from: type=gha | ||
cache-to: type=gha,mode=max |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts | ||
|
||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
FROM node:18-alpine AS base | ||
|
||
# Install dependencies only when needed | ||
FROM base AS deps | ||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. | ||
RUN apk add --no-cache libc6-compat && \ | ||
wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh - | ||
WORKDIR /app | ||
|
||
# Install dependencies based on the preferred package manager | ||
COPY package.json pnpm-lock.yaml ./ | ||
RUN source /root/.shrc; \ | ||
if [ -f pnpm-lock.yaml ]; then pnpm i --frozen-lockfile; \ | ||
else echo "Lockfile not found." && exit 1; \ | ||
fi | ||
|
||
# Rebuild the source code only when needed | ||
FROM base AS builder | ||
WORKDIR /app | ||
COPY --from=deps /app/node_modules ./node_modules | ||
COPY . . | ||
|
||
# Next.js collects completely anonymous telemetry data about general usage. | ||
# Learn more here: https://nextjs.org/telemetry | ||
# Uncomment the following line in case you want to disable telemetry during the build. | ||
ENV NEXT_TELEMETRY_DISABLED 1 | ||
ENV OPENAI_API_KEY "" | ||
|
||
RUN wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh - && \ | ||
source /root/.shrc && \ | ||
pnpm run build | ||
|
||
# Production image, copy all the files and run next | ||
FROM base AS runner | ||
WORKDIR /app | ||
|
||
ENV NODE_ENV production | ||
# Uncomment the following line in case you want to disable telemetry during runtime. | ||
ENV NEXT_TELEMETRY_DISABLED 1 | ||
|
||
RUN addgroup --system --gid 1001 nodejs | ||
RUN adduser --system --uid 1001 nextjs | ||
|
||
# Automatically leverage output traces to reduce image size | ||
# https://nextjs.org/docs/advanced-features/output-file-tracing | ||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ | ||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static | ||
|
||
USER nextjs | ||
|
||
EXPOSE 3002 | ||
|
||
ENV PORT 3002 | ||
# set hostname to localhost | ||
ENV HOSTNAME "127.0.0.1" | ||
|
||
CMD ["node", "server.js"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# 📌 概述 | ||
|
||
**Chat AI:** 一个简单而优雅的 AI 聊天程序 | ||
|
||
## ⚙️ 设置 | ||
|
||
#### 环境变量 | ||
|
||
- `OPENAI_API_KEY`:不必多说,懂的都懂 | ||
- `GOOGLE_API_KEY`:用于 Google 搜索插件(可选) | ||
- `GOOGLE_ENGINE_ID`:用于 Google 搜索插件(可选) | ||
- `AMAP_KEY`:用于查询天气插件,_(使用高德开放平台)_(可选) | ||
|
||
## 🚀 本地运行 | ||
|
||
1. 克隆仓库: | ||
|
||
```sh | ||
git clone https://github.com/sunls24/chat-ai | ||
``` | ||
|
||
2. 安装依赖项: | ||
|
||
```bash | ||
pnpm install | ||
``` | ||
|
||
3. 开发模式: | ||
|
||
```bash | ||
pnpm run dev | ||
``` | ||
|
||
## ☁️ 使用 Vercel 部署 | ||
|
||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgit.luolix.top%2Fsunls24%2Fchat-ai&env=OPENAI_API_KEY,GOOGLE_API_KEY,GOOGLE_ENGINE_ID,AMAP_KEY) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { ChatCompletionCreateParams } from "openai/resources/chat/completions"; | ||
import { NodeHtmlMarkdown } from "node-html-markdown"; | ||
|
||
interface FunctionCall { | ||
function: ChatCompletionCreateParams.Function; | ||
call: (name: string, args: Record<string, unknown>) => Promise<any>; | ||
} | ||
|
||
const functionMap: Record<string, FunctionCall> = { | ||
googleSearch: { | ||
function: { | ||
name: "googleSearch", | ||
description: "Search the web for information using Google", | ||
parameters: { | ||
type: "object", | ||
properties: { | ||
keyword: { | ||
type: "string", | ||
description: "Keywords for searching", | ||
}, | ||
}, | ||
required: ["keyword"], | ||
}, | ||
}, | ||
call: async (name, args) => { | ||
const nothing = "nothing"; | ||
const cfg = args.config as any; | ||
const apiKey = process.env.GOOGLE_API_KEY ?? cfg.apiKey; | ||
const engineId = process.env.GOOGLE_ENGINE_ID ?? cfg.engineId; | ||
if (!apiKey || !engineId) { | ||
console.log(`- ${name} apiKey or engineId is empty`); | ||
return nothing; | ||
} | ||
try { | ||
const res = await fetch( | ||
`https://www.googleapis.com/customsearch/v1?&fields=items(title,link,snippet,pagemap/metatags(og:description))&key=${apiKey}&cx=${engineId}&q=${args.keyword}`, | ||
); | ||
const result = await res.json(); | ||
return result.items ?? nothing; | ||
} catch (err: any) { | ||
console.log(`- ${name} ${args.keyword} ${err.cause ?? err}`); | ||
return nothing; | ||
} | ||
}, | ||
}, | ||
browseWebsite: { | ||
function: { | ||
name: "browseWebsite", | ||
description: "Get website content through via given url", | ||
parameters: { | ||
type: "object", | ||
properties: { | ||
url: { | ||
type: "string", | ||
description: "Website address", | ||
}, | ||
}, | ||
required: ["url"], | ||
}, | ||
}, | ||
call: async (name, args) => { | ||
const cfg = args.config as any; | ||
const url = args.url as string; | ||
try { | ||
const res = await fetch(url); | ||
let result = await res.text(); | ||
result = NodeHtmlMarkdown.translate(result); | ||
result = result.replace(/^\s+|\s+$/gm, "\n").replace(/(\n{2,})/g, "\n"); | ||
if (result.length > cfg.maxLength) { | ||
console.log(`- ${name} ${url} Cut off!!!`); | ||
result = result.substring(0, cfg.maxLength); | ||
} | ||
return result; | ||
} catch (err: any) { | ||
console.log(`- ${name} ${url} ${err.cause ?? err}`); | ||
return "Unable to access this website"; | ||
} | ||
}, | ||
}, | ||
weatherInfo: { | ||
function: { | ||
name: "weatherInfo", | ||
description: "Get online weather information", | ||
parameters: { | ||
type: "object", | ||
properties: { | ||
location: { | ||
type: "string", | ||
description: "Query location", | ||
}, | ||
type: { | ||
type: "string", | ||
description: | ||
"base: return live weather, all: return forecast weather", | ||
}, | ||
}, | ||
required: ["location", "type"], | ||
}, | ||
}, | ||
call: async (name, args) => { | ||
const nothing = "Unable to get weather information"; | ||
const cfg = args.config as any; | ||
const key = cfg.amapKey ? cfg.amapKey : process.env.AMAP_KEY; | ||
if (!key) { | ||
console.log(`- ${name} amapKey is empty`); | ||
return nothing; | ||
} | ||
try { | ||
let res = await fetch( | ||
`https://restapi.amap.com/v3/geocode/geo?address=${args.location}&key=${key}`, | ||
); | ||
let result = await res.json(); | ||
if (result.status != 1 || result.count < 1) { | ||
console.log(`- ${name} ${args.location} ${result.info}`); | ||
return nothing; | ||
} | ||
res = await fetch( | ||
`https://restapi.amap.com/v3/weather/weatherInfo?city=${result.geocodes[0].adcode}&key=${key}&extensions=${args.type}`, | ||
); | ||
result = await res.json(); | ||
if (result.status != 1 || result.count < 1) { | ||
console.log(`- ${name} ${args.location} ${result.info}`); | ||
return nothing; | ||
} | ||
return result; | ||
} catch (err: any) { | ||
console.log(`- ${name} ${args.location} ${err.cause ?? err}`); | ||
return nothing; | ||
} | ||
}, | ||
}, | ||
}; | ||
|
||
export const functions: ChatCompletionCreateParams.Function[] = Object.values( | ||
functionMap, | ||
).map((value) => value.function); | ||
|
||
export async function onFunctionCall( | ||
name: string, | ||
args: Record<string, unknown>, | ||
): Promise<any> { | ||
console.log("- onFunctionCall", name, args); | ||
if (!functionMap.hasOwnProperty(name)) { | ||
return `${name} function not found`; | ||
} | ||
return functionMap[name].call(name, args); | ||
} |
Oops, something went wrong.