This commit is contained in:
parent
f06f341d12
commit
644e1339fb
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
; https://editorconfig.org/
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[{*.yml,*.yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
57
.github/workflows/build.yaml
vendored
57
.github/workflows/build.yaml
vendored
@ -1,13 +1,6 @@
|
|||||||
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
@ -17,6 +10,9 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
checks: write
|
checks: write
|
||||||
|
env:
|
||||||
|
DOCKER_REGISTRY: devops.lty.name
|
||||||
|
DOCKER_IMAGE: ${{ github.repository }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@ -26,36 +22,33 @@ jobs:
|
|||||||
go-version: '1.23'
|
go-version: '1.23'
|
||||||
- uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8
|
- uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8
|
||||||
with:
|
with:
|
||||||
version: v1.60
|
version: v1.63
|
||||||
- name: Build
|
- name: build
|
||||||
run: make -j8 all && tree bin
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: builds-${{ github.sha }}
|
|
||||||
path: |
|
|
||||||
bin/*.zip
|
|
||||||
bin/*.tar.gz
|
|
||||||
bin/*.sha256sum
|
|
||||||
- name: Upload Release Asset
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
run: |
|
run: |
|
||||||
gh release create ${GIT_TAG} bin/*.zip bin/*.tar.gz bin/*.sha256sum
|
make build
|
||||||
|
cp -r bin ${DISTNAME}
|
||||||
|
cp -r agents ${DISTNAME}
|
||||||
|
tar --zstd -cvf ${DISTNAME}.tar.zst ${DISTNAME}
|
||||||
env:
|
env:
|
||||||
GIT_TAG: ${{ github.ref_name }}
|
DISTNAME: chatai-linux-amd64-${{ github.ref_name }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
- if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: https://gitea.com/actions/release-action@21a5938ff2548f6472d89ba13b3cdd8af4c67068
|
||||||
|
with:
|
||||||
|
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: |
|
||||||
|
*.tar.zst
|
||||||
- uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5
|
- uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5
|
||||||
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
|
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ${{ env.DOCKER_REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.PACKAGES_PUBLISH_TOKEN }}
|
||||||
- uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355
|
- if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
|
||||||
|
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/386
|
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:${{ startsWith(github.ref, 'refs/tags/') && 'latest' || 'dev' }}
|
${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ startsWith(github.ref, 'refs/tags/') && 'latest' || 'dev' }}
|
||||||
ghcr.io/${{ github.repository }}:${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.sha }}
|
${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.sha }}
|
||||||
|
20
Dockerfile
20
Dockerfile
@ -1,13 +1,7 @@
|
|||||||
FROM --platform=$BUILDPLATFORM alpine:3.21 AS builder
|
FROM alpine:3.21
|
||||||
RUN apk add --no-cache tzdata ca-certificates
|
COPY ./bin/chatai /app/chatai
|
||||||
|
COPY ./agents /app/agents
|
||||||
FROM alpine:3.21
|
RUN apk add --no-cache tzdata ca-certificates
|
||||||
ARG TARGETPLATFORM
|
WORKDIR /app
|
||||||
COPY bin/arch/${TARGETPLATFORM}/ai-agent /app/ai-agent
|
EXPOSE 7120
|
||||||
COPY agents /app/agents
|
CMD ["/app/chatai"]
|
||||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
|
||||||
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 7120
|
|
||||||
CMD ["/app/ai-agent"]
|
|
||||||
|
61
Makefile
61
Makefile
@ -1,30 +1,10 @@
|
|||||||
#!/usr/bin/env make -f
|
#!/usr/bin/env make -f
|
||||||
AGENTS_DIR = agents
|
OUTPUT := ./bin/
|
||||||
DIST_DIRECTORY = ./bin
|
VERSION := $(shell git describe --tags 2>/dev/null || echo "git/$(shell git rev-parse --short HEAD 2>/dev/null || echo 'none')")
|
||||||
DIST_APPNAME = ai-agent
|
CGO_ENABLED := 0
|
||||||
DIST_EXENAME = ai-agent
|
BUILDINFO_CLASS := devops.lty.name/luo/chatai/internal/config
|
||||||
BUILDINFO_CLASS = github.com/ltylab/ai-agent/internal/conf
|
|
||||||
BUILDINFO_VERSION := $(shell git describe --tags 2>/dev/null || echo "git/$(shell git rev-parse --short HEAD 2>/dev/null || echo 'none')")
|
|
||||||
CROSS_BUILD_TRIPLES = darwin/amd64 \
|
|
||||||
darwin/arm64 \
|
|
||||||
windows/386 \
|
|
||||||
windows/amd64 \
|
|
||||||
windows/arm64 \
|
|
||||||
linux/386 \
|
|
||||||
linux/amd64 \
|
|
||||||
linux/arm64 \
|
|
||||||
linux/loong64 \
|
|
||||||
linux/mips \
|
|
||||||
linux/mips64 \
|
|
||||||
linux/mips64le \
|
|
||||||
linux/mipsle \
|
|
||||||
linux/riscv64 \
|
|
||||||
freebsd/386 \
|
|
||||||
freebsd/amd64 \
|
|
||||||
freebsd/arm64 \
|
|
||||||
freebsd/riscv64
|
|
||||||
LDFLAGS = -s -w
|
LDFLAGS = -s -w
|
||||||
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildVersion=$(BUILDINFO_VERSION)"
|
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildVersion=$(VERSION)"
|
||||||
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildTime=$(shell date '+%Y-%m-%d %H:%M:%S %Z')"
|
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildTime=$(shell date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||||
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildMode=release"
|
LDFLAGS += -X "$(BUILDINFO_CLASS).BuildMode=release"
|
||||||
|
|
||||||
@ -36,7 +16,12 @@ build_debug: build
|
|||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: clean install
|
build: clean install
|
||||||
CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o $(DIST_DIRECTORY)/$(DIST_EXENAME)
|
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags '$(LDFLAGS)' -o $(OUTPUT)
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run: install
|
||||||
|
go run .
|
||||||
|
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
@ -46,26 +31,4 @@ install:
|
|||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rvf $(DIST_DIRECTORY)
|
rm -rvf $(OUTPUT)
|
||||||
|
|
||||||
|
|
||||||
.PHONY: all $(CROSS_BUILD_TRIPLES)
|
|
||||||
all: clean install $(CROSS_BUILD_TRIPLES)
|
|
||||||
$(CROSS_BUILD_TRIPLES): GOOS = $(word 1,$(subst /, ,$@))
|
|
||||||
$(CROSS_BUILD_TRIPLES): GOARCH = $(word 2,$(subst /, ,$@))
|
|
||||||
$(CROSS_BUILD_TRIPLES): CROSS_DIST_DIRNAME = $(DIST_APPNAME)-$(subst /,.,$(BUILDINFO_VERSION)-$(GOOS)-$(GOARCH))
|
|
||||||
$(CROSS_BUILD_TRIPLES): CROSS_DIST_EXENAME = $(DIST_EXENAME)$(if $(filter $(GOOS),windows),.exe,)
|
|
||||||
$(CROSS_BUILD_TRIPLES): CROSS_DIST_ARCNAME = $(CROSS_DIST_DIRNAME).$(if $(filter $(GOOS),windows),zip,tar.gz)
|
|
||||||
$(CROSS_BUILD_TRIPLES):
|
|
||||||
$(MAKE) build GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
|
||||||
DIST_DIRECTORY=$(DIST_DIRECTORY)/$(CROSS_DIST_DIRNAME) \
|
|
||||||
DIST_EXENAME=$(CROSS_DIST_EXENAME) && \
|
|
||||||
cp -r $(AGENTS_DIR) $(DIST_DIRECTORY)/$(CROSS_DIST_DIRNAME)/ && \
|
|
||||||
mkdir -p $(DIST_DIRECTORY)/arch/$(GOOS)/$(GOARCH) && \
|
|
||||||
cp $(DIST_DIRECTORY)/$(CROSS_DIST_DIRNAME)/$(CROSS_DIST_EXENAME) $(DIST_DIRECTORY)/arch/$(GOOS)/$(GOARCH)/$(CROSS_DIST_EXENAME) && \
|
|
||||||
if [ "$(GOOS)" = "windows" ]; then \
|
|
||||||
cd $(DIST_DIRECTORY) && zip -r $(CROSS_DIST_ARCNAME) $(CROSS_DIST_DIRNAME) && cd -; \
|
|
||||||
else \
|
|
||||||
tar -cvzf $(DIST_DIRECTORY)/$(CROSS_DIST_ARCNAME) -C $(DIST_DIRECTORY) $(CROSS_DIST_DIRNAME); \
|
|
||||||
fi && \
|
|
||||||
sh -c "cd $(DIST_DIRECTORY) && sha256sum $(CROSS_DIST_ARCNAME) > $(CROSS_DIST_ARCNAME).sha256sum";
|
|
||||||
|
15
README.md
15
README.md
@ -1,35 +1,34 @@
|
|||||||
# Luo Tianyi Codelabs AI Agent
|
# Chat AI
|
||||||
|
|
||||||
提供类似 QQ 界面的公共 AI 聊天机器人,基于 Google Gemini 模型。
|
提供类似 QQ 界面的公共 AI 聊天机器人,基于 Google Gemini 模型。
|
||||||
|
|
||||||
**注意**
|
**注意**
|
||||||
- 不支持视频、图片、音频等多模态输入。
|
- 不支持视频、图片、音频等多模态输入。
|
||||||
- 完全公开、无频率限制与身份认证。建议使用免费的 API Key 并禁用结算账号。
|
- 完全公开、无频率限制与身份认证。建议使用免费的 API Key 并禁用结算账号。
|
||||||
- 暂时没有列出所有模型的功能。
|
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
从 GitHub Release 下载最新版本的压缩包,解压缩。你可以使用 `-h` 参数查看帮助信息:
|
从「发行版」下载最新版本的压缩包,解压缩。你可以使用 `-h` 参数查看帮助信息:
|
||||||
```bash
|
```bash
|
||||||
./ai-agent-web -h
|
./chatai -h
|
||||||
```
|
```
|
||||||
|
|
||||||
要正常启动服务器,您需要设置环境变量 `GOOGLE_AI_KEY`。您可以在 [Google AI Studio](https://aistudio.google.com) 免费获得一个(需要 Google 账号)。
|
要正常启动服务器,您需要设置环境变量 `GOOGLE_AI_KEY`。您可以在 [Google AI Studio](https://aistudio.google.com) 免费获得一个(需要 Google 账号)。
|
||||||
```bash
|
```bash
|
||||||
export GOOGLE_AI_KEY=your-key
|
export GOOGLE_AI_KEY=your-key
|
||||||
./ai-agent-web
|
./chatai
|
||||||
```
|
```
|
||||||
|
|
||||||
您也可以使用 Docker 运行:
|
您也可以使用 Docker 运行:
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 57120:7120 -e GOOGLE_AI_KEY=your-key \
|
docker run -d -p 57120:7120 -e GOOGLE_AI_KEY=your-key \
|
||||||
--name ai-agent-web ghcr.io/github.com/ltylab/ai-agent
|
--name chatai lty.name/chatai
|
||||||
```
|
```
|
||||||
|
|
||||||
如果需要自定义 AI Agent,请将您的 AI Agent 配置文件映射到 `/app/agents` 目录:
|
如果需要自定义 AI Agent,请将您的 AI Agent 配置文件映射到 `/app/agents` 目录:
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 57120:7120 -e GOOGLE_AI_KEY=your-key \
|
docker run -d -p 57120:7120 -e GOOGLE_AI_KEY=your-key \
|
||||||
-v /path/to/your/agents:/app/agents \
|
-v /path/to/your/agents:/app/agents \
|
||||||
--name ai-agent-web ghcr.io/github.com/ltylab/ai-agent
|
--name chatai lty.name/chatai
|
||||||
```
|
```
|
||||||
|
|
||||||
可以通过 `-model` 参数修改模型,推荐使用下面两个免费模型:
|
可以通过 `-model` 参数修改模型,推荐使用下面两个免费模型:
|
||||||
@ -55,6 +54,7 @@ agents
|
|||||||
"id": "luo",
|
"id": "luo",
|
||||||
"name": "洛天依AI",
|
"name": "洛天依AI",
|
||||||
"desc": "世界第一的 ∞ 吃货殿下~",
|
"desc": "世界第一的 ∞ 吃货殿下~",
|
||||||
|
"footer": "此 AI 并非洛天依官方提供,请勿用于商业用途。",
|
||||||
"primaryColor": "#6cb6df",
|
"primaryColor": "#6cb6df",
|
||||||
"secondaryColor": "#66ccff",
|
"secondaryColor": "#66ccff",
|
||||||
"accentColor": "#a62f35"
|
"accentColor": "#a62f35"
|
||||||
@ -65,6 +65,7 @@ agents
|
|||||||
- `id` 是 Agent ID,必须和文件夹名称一致。
|
- `id` 是 Agent ID,必须和文件夹名称一致。
|
||||||
- `name` 是 Agent 名称,将显示在聊天界面。
|
- `name` 是 Agent 名称,将显示在聊天界面。
|
||||||
- `desc` 是 Agent 描述,将显示在聊天界面。
|
- `desc` 是 Agent 描述,将显示在聊天界面。
|
||||||
|
- `footer` 是 Agent 页脚,用于显示版权信息、免责声明等。
|
||||||
- `primaryColor` 是主色调,将用于聊天界面的背景色。可选,如果未设置则使用默认值。
|
- `primaryColor` 是主色调,将用于聊天界面的背景色。可选,如果未设置则使用默认值。
|
||||||
- `secondaryColor` 是次色调,将用于聊天界面的背景色。可选,如果未设置则使用默认值。
|
- `secondaryColor` 是次色调,将用于聊天界面的背景色。可选,如果未设置则使用默认值。
|
||||||
- `accentColor` 是强调色,将用于聊天界面的背景色。可选,如果未设置则使用默认值。
|
- `accentColor` 是强调色,将用于聊天界面的背景色。可选,如果未设置则使用默认值。
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "ldk",
|
"id": "ldk",
|
||||||
"name": "李迪克AI",
|
"name": "李迪克AI",
|
||||||
"desc": "我跟你讲!我家星尘宝宝,可爱(*´▽`*)~"
|
"desc": "我跟你讲!我家星尘宝宝,可爱(*´▽`*)~",
|
||||||
|
"footer": "AI生成的内容不代表 <a href=\"https://space.bilibili.com/882467\" target=\"_blank\" rel=\"noopener noreferrer\">李迪克</a> 的观点 | 数据来源: <a href=\"https://cyberldk.com\" target=\"_blank\" rel=\"noopener noreferrer\">CyberLDK</a>"
|
||||||
}
|
}
|
@ -2,6 +2,7 @@
|
|||||||
"id": "luo",
|
"id": "luo",
|
||||||
"name": "洛天依AI",
|
"name": "洛天依AI",
|
||||||
"desc": "世界第一的 ∞ 吃货殿下~",
|
"desc": "世界第一的 ∞ 吃货殿下~",
|
||||||
|
"footer": "此 AI 并非官方制作 | 请勿用于不当或商业用途",
|
||||||
"primaryColor": "#6cb6df",
|
"primaryColor": "#6cb6df",
|
||||||
"secondaryColor": "#66ccff",
|
"secondaryColor": "#66ccff",
|
||||||
"accentColor": "#a62f35"
|
"accentColor": "#a62f35"
|
||||||
|
2
go.mod
2
go.mod
@ -1,4 +1,4 @@
|
|||||||
module github.com/ltylab/ai-agent
|
module devops.lty.name/luo/chatai
|
||||||
|
|
||||||
go 1.23.3
|
go 1.23.3
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/ltylab/ai-agent/internal/conf"
|
"devops.lty.name/luo/chatai/internal/config"
|
||||||
"github.com/ltylab/ai-agent/internal/log"
|
"devops.lty.name/luo/chatai/internal/log"
|
||||||
"github.com/ltylab/ai-agent/web/assets"
|
"devops.lty.name/luo/chatai/web/assets"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@ -24,6 +24,9 @@ type AgentConf struct {
|
|||||||
// AgentDescription is a short description of the agent.
|
// AgentDescription is a short description of the agent.
|
||||||
AgentDescription string `json:"desc"`
|
AgentDescription string `json:"desc"`
|
||||||
|
|
||||||
|
// FooterHTML is the HTML content to be displayed in the footer of the agent chat page.
|
||||||
|
FooterHTML string `json:"footer"`
|
||||||
|
|
||||||
// PrimaryColor is the primary color for the agent chat page.
|
// PrimaryColor is the primary color for the agent chat page.
|
||||||
PrimaryColor string `json:"primaryColor"`
|
PrimaryColor string `json:"primaryColor"`
|
||||||
|
|
||||||
@ -78,11 +81,11 @@ func (a *AgentConf) ReadAgent() *Agent {
|
|||||||
func LoadAgentFromFile(agentJSONPath string) (*AgentConf, error) {
|
func LoadAgentFromFile(agentJSONPath string) (*AgentConf, error) {
|
||||||
conf := &AgentConf{
|
conf := &AgentConf{
|
||||||
AgentID: uuid.New().String(),
|
AgentID: uuid.New().String(),
|
||||||
AgentName: conf.DefaultAgentName,
|
AgentName: config.DefaultAgentName,
|
||||||
AgentDescription: conf.DefaultAgentDescription,
|
AgentDescription: config.DefaultAgentDescription,
|
||||||
PrimaryColor: conf.DefaultAgentPrimaryColor,
|
PrimaryColor: config.DefaultAgentPrimaryColor,
|
||||||
SecondaryColor: conf.DefaultAgentSecondaryColor,
|
SecondaryColor: config.DefaultAgentSecondaryColor,
|
||||||
AccentColor: conf.DefaultAgentAccentColor,
|
AccentColor: config.DefaultAgentAccentColor,
|
||||||
Dir: path.Dir(agentJSONPath),
|
Dir: path.Dir(agentJSONPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
var (
|
|
||||||
BuildVersion = "dev"
|
|
||||||
BuildTime = "<unknown>"
|
|
||||||
BuildMode = "development"
|
|
||||||
PackageLicense = "Unlicense"
|
|
||||||
PackageCopyright = "This is an unlicensed software (under public domain) by Luo Tianyi Codelabs <https://lty.name/>."
|
|
||||||
|
|
||||||
DefaultListen = ":7120"
|
|
||||||
DefaultAgentsDir = "agents"
|
|
||||||
DefaultLogLevel = "dbg"
|
|
||||||
DefaultMsgLogPath = "logs"
|
|
||||||
|
|
||||||
DefaultAgentName = "未名"
|
|
||||||
DefaultAgentDescription = "这个 Agent 没有名字"
|
|
||||||
DefaultAgentPrimaryColor = "#444e8d"
|
|
||||||
DefaultAgentSecondaryColor = "#9f9ff5"
|
|
||||||
DefaultAgentAccentColor = "#eeaf5b"
|
|
||||||
|
|
||||||
DefaultGoogleAIModel = "gemini-1.5-flash"
|
|
||||||
)
|
|
21
internal/config/conf.go
Normal file
21
internal/config/conf.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
var (
|
||||||
|
BuildVersion = "dev"
|
||||||
|
BuildTime = "<unknown>"
|
||||||
|
BuildMode = "development"
|
||||||
|
Copyright = "This is an unlicensed software (under public domain) by Luo Tianyi Codelabs <https://lty.name/>."
|
||||||
|
|
||||||
|
FlagListen = ":7120"
|
||||||
|
FlagAgentsDir = "agents"
|
||||||
|
FlagLogLevel = "dbg"
|
||||||
|
FlagMsgLogPath = "logs"
|
||||||
|
|
||||||
|
DefaultAgentName = "未名"
|
||||||
|
DefaultAgentDescription = "这个 Agent 没有名字"
|
||||||
|
DefaultAgentPrimaryColor = "#444e8d"
|
||||||
|
DefaultAgentSecondaryColor = "#9f9ff5"
|
||||||
|
DefaultAgentAccentColor = "#eeaf5b"
|
||||||
|
|
||||||
|
DefaultGoogleAIModel = "gemini-1.5-flash"
|
||||||
|
)
|
@ -8,8 +8,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ltylab/ai-agent/internal/agent"
|
"devops.lty.name/luo/chatai/internal/agent"
|
||||||
"github.com/ltylab/ai-agent/internal/log"
|
"devops.lty.name/luo/chatai/internal/log"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/generative-ai-go/genai"
|
"github.com/google/generative-ai-go/genai"
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"text/template"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/ltylab/ai-agent/internal/agent"
|
"devops.lty.name/luo/chatai/internal/agent"
|
||||||
"github.com/ltylab/ai-agent/internal/log"
|
"devops.lty.name/luo/chatai/internal/log"
|
||||||
"github.com/ltylab/ai-agent/web"
|
"devops.lty.name/luo/chatai/web"
|
||||||
"github.com/ltylab/ai-agent/web/assets"
|
"devops.lty.name/luo/chatai/web/assets"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -38,11 +38,32 @@ func Setup(e *gin.Engine, agentsDir string, msgLogPath string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.StaticFS("/assets", http.FS(assets.AssetsFS))
|
e.StaticFS("/assets", http.FS(assets.AssetsFS))
|
||||||
|
e.GET("", s.handleHomepageWithRendenedTemplate)
|
||||||
e.GET("/:agentID", s.handleWithRendenedTemplate)
|
e.GET("/:agentID", s.handleWithRendenedTemplate)
|
||||||
e.GET("/:agentID/avatar.webp", s.handleAvatar)
|
e.GET("/:agentID/avatar.webp", s.handleAvatar)
|
||||||
e.POST("/:agentID/chat", s.handleChat)
|
e.POST("/:agentID/chat", s.handleChat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleHomepageWithRendenedTemplate(c *gin.Context) {
|
||||||
|
tmpl, err := template.New("home").Parse(web.HomeLayout)
|
||||||
|
if err != nil {
|
||||||
|
log.T("server/tmpl").Errf("Failed to parse template: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "server error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tmpl.Execute(c.Writer, s.agents)
|
||||||
|
if err != nil {
|
||||||
|
log.T("server/tmpl").Errf("Failed to render template: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "server error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
c.Writer.Flush()
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleWithRendenedTemplate(c *gin.Context) {
|
func (s *Server) handleWithRendenedTemplate(c *gin.Context) {
|
||||||
agentID := c.Param("agentID")
|
agentID := c.Param("agentID")
|
||||||
agentConf, ok := s.agents[agentID]
|
agentConf, ok := s.agents[agentID]
|
||||||
@ -51,7 +72,7 @@ func (s *Server) handleWithRendenedTemplate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := template.New("tmpl.html").Parse(web.Template)
|
tmpl, err := template.New("agent").Parse(web.AgentLayout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.T("server/tmpl").Errf("Failed to parse template: %v", err)
|
log.T("server/tmpl").Errf("Failed to parse template: %v", err)
|
||||||
c.String(http.StatusInternalServerError, "server error")
|
c.String(http.StatusInternalServerError, "server error")
|
||||||
|
22
main.go
22
main.go
@ -4,19 +4,19 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/ltylab/ai-agent/internal/conf"
|
"devops.lty.name/luo/chatai/internal/config"
|
||||||
"github.com/ltylab/ai-agent/internal/log"
|
"devops.lty.name/luo/chatai/internal/log"
|
||||||
"github.com/ltylab/ai-agent/internal/server"
|
"devops.lty.name/luo/chatai/internal/server"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
listen := flag.String("listen", conf.DefaultListen, "http listen address")
|
listen := flag.String("listen", config.FlagListen, "http listen address")
|
||||||
agentsDir := flag.String("agents", conf.DefaultAgentsDir, "directory to store agent configurations")
|
agentsDir := flag.String("agents", config.FlagAgentsDir, "directory to store agent configurations")
|
||||||
logLevel := flag.String("level", conf.DefaultLogLevel, "log level (dbg, inf, wrn, err)")
|
logLevel := flag.String("level", config.FlagLogLevel, "log level (dbg, inf, wrn, err)")
|
||||||
msgLogPath := flag.String("msglog", conf.DefaultMsgLogPath, "audit log path")
|
msgLogPath := flag.String("msglog", config.FlagMsgLogPath, "audit log path")
|
||||||
model := flag.String("model", conf.DefaultGoogleAIModel, "Google AI model (gemini-1.5-flash, gemini-2.0-flash-exp)")
|
model := flag.String("model", config.DefaultGoogleAIModel, "Google AI model (gemini-1.5-flash, gemini-2.0-flash-exp)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
server.GoogleAIKey = os.Getenv("GOOGLE_AI_KEY")
|
server.GoogleAIKey = os.Getenv("GOOGLE_AI_KEY")
|
||||||
@ -27,11 +27,11 @@ func main() {
|
|||||||
server.GoogleAIModel = *model
|
server.GoogleAIModel = *model
|
||||||
|
|
||||||
log.Setup(log.LogLevel(log.ParseLogLevel(*logLevel)))
|
log.Setup(log.LogLevel(log.ParseLogLevel(*logLevel)))
|
||||||
log.T("main").Inff("AI-Agent version %s, %s mode (build %s)", conf.BuildVersion, conf.BuildMode, conf.BuildTime)
|
log.T("main").Inff("AI-Agent version %s, %s mode (build %s)", config.BuildVersion, config.BuildMode, config.BuildTime)
|
||||||
log.T("main").Inff("%s", conf.PackageCopyright)
|
log.T("main").Inff("%s", config.Copyright)
|
||||||
|
|
||||||
log.SetupGin1()
|
log.SetupGin1()
|
||||||
if conf.BuildMode == "release" {
|
if config.BuildMode == "release" {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +248,7 @@
|
|||||||
<aside>
|
<aside>
|
||||||
<button>_</button>
|
<button>_</button>
|
||||||
<button>□</button>
|
<button>□</button>
|
||||||
<button>×</button>
|
<button onclick="location.href = '/'">×</button>
|
||||||
</aside>
|
</aside>
|
||||||
</header>
|
</header>
|
||||||
<article>
|
<article>
|
||||||
@ -264,7 +264,8 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
<div class="copy">
|
<div class="copy">
|
||||||
以上内容为 AI 生成,请注意辨别。<br>为了安全和审计需要,我们可能记录您与 AI 的对话内容和您的 IP 地址。请勿向模型分享敏感信息。
|
{{.FooterHTML}}<br>
|
||||||
|
以上内容为 AI 生成,请注意辨别。<br>为了安全和审计需要,我们可能记录您与 AI 的对话内容和您的 IP 地址。请勿向 AI 分享敏感信息。
|
||||||
</div>
|
</div>
|
||||||
<template id="chat-record">
|
<template id="chat-record">
|
||||||
<section>
|
<section>
|
@ -4,5 +4,8 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed tmpl.html
|
//go:embed home.html
|
||||||
var Template string
|
var HomeLayout string
|
||||||
|
|
||||||
|
//go:embed agent.html
|
||||||
|
var AgentLayout string
|
||||||
|
150
web/home.html
Normal file
150
web/home.html
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="renderer" content="webkit">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-siteapp">
|
||||||
|
<meta name="google" content="notranslate">
|
||||||
|
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||||
|
<title>选择一个 AI Agent 开始聊天</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans',
|
||||||
|
'PingFang SC', 'Hiragino Sans GB', 'Source Han Sans CN', 'Source Han Sans SC', 'Microsoft YaHei', 'Wenquanyi Micro Hei', 'WenQuanYi Zen Hei',
|
||||||
|
'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #3c3c3c;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
height: 40%;
|
||||||
|
min-height: 400px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .container {
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
gap: 2.5rem;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
height: 100%;
|
||||||
|
background: #fcfcfc;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
section img {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section span {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
justify-self: flex-end;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
{{range .}}
|
||||||
|
.chat-btn__{{.AgentID}} {
|
||||||
|
background: {{.PrimaryColor}};
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-btn__{{.AgentID}}:hover {
|
||||||
|
background: {{.SecondaryColor}};
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>选择一个 AI Agent 开始聊天</h1>
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
{{range .}}
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<img src="/{{.AgentID}}/avatar.webp" alt="{{.AgentName}} 头像">
|
||||||
|
<h2>{{.AgentName}}</h2>
|
||||||
|
<span>{{.AgentDescription}}</span>
|
||||||
|
</div>
|
||||||
|
<a href="/{{.AgentID}}" class="chat-btn__{{.AgentID}}">开始聊天</a>
|
||||||
|
</section>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
window.addEventListener("wheel", function (e) {
|
||||||
|
const container = document.querySelector(".container");
|
||||||
|
if (e.deltaY > 0) container.scrollLeft += 100;
|
||||||
|
else container.scrollLeft -= 100;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user