Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sunls24 committed Oct 26, 2023
0 parents commit e2ba5d9
Show file tree
Hide file tree
Showing 61 changed files with 10,321 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "prettier"]
}
45 changes: 45 additions & 0 deletions .github/workflows/docker.yml
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
37 changes: 37 additions & 0 deletions .gitignore
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
57 changes: 57 additions & 0 deletions Dockerfile
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"]
36 changes: 36 additions & 0 deletions README.md
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)
147 changes: 147 additions & 0 deletions app/api/chat/functions.ts
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);
}
Loading

0 comments on commit e2ba5d9

Please sign in to comment.