Spaces:
Sleeping
Sleeping
Commit ·
32455d6
0
Parent(s):
init: eino siliconflow demo
Browse files- .dockerignore +7 -0
- .env.example +12 -0
- .gitignore +7 -0
- Dockerfile +26 -0
- README.md +128 -0
- go.mod +34 -0
- go.sum +137 -0
- internal/app/handlers.go +66 -0
- internal/app/timeouts.go +13 -0
- internal/app/worker.go +57 -0
- internal/einoopenai/chat_model.go +207 -0
- main.go +49 -0
- web/index.html +299 -0
.dockerignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
.git
|
| 3 |
+
.idea
|
| 4 |
+
.vscode
|
| 5 |
+
bin
|
| 6 |
+
dist
|
| 7 |
+
|
.env.example
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 优先使用硅基流动(可选)。不填则回退到 OPENAI_*,都不填则自动走 Mock。
|
| 2 |
+
SILICONFLOW_API_KEY=
|
| 3 |
+
SILICONFLOW_MODEL=Qwen/Qwen2.5-7B-Instruct
|
| 4 |
+
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1
|
| 5 |
+
|
| 6 |
+
# OpenAI 兼容(可选)
|
| 7 |
+
OPENAI_API_KEY=
|
| 8 |
+
OPENAI_MODEL=
|
| 9 |
+
OPENAI_BASE_URL=
|
| 10 |
+
|
| 11 |
+
# HTTP 服务端口
|
| 12 |
+
PORT=8080
|
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
.idea/
|
| 3 |
+
.vscode/
|
| 4 |
+
.env
|
| 5 |
+
dist/
|
| 6 |
+
bin/
|
| 7 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM golang:1.22-bookworm AS build
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY go.mod go.sum ./
|
| 6 |
+
RUN go mod download
|
| 7 |
+
|
| 8 |
+
COPY . ./
|
| 9 |
+
|
| 10 |
+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server .
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
FROM gcr.io/distroless/static-debian12:nonroot
|
| 14 |
+
|
| 15 |
+
WORKDIR /app
|
| 16 |
+
|
| 17 |
+
COPY --from=build /out/server /app/server
|
| 18 |
+
COPY --from=build /app/web /app/web
|
| 19 |
+
|
| 20 |
+
ENV PORT=7860
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
|
| 23 |
+
USER nonroot:nonroot
|
| 24 |
+
|
| 25 |
+
ENTRYPOINT ["/app/server"]
|
| 26 |
+
|
README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Eino + 硅基流动 Qwen2.5-7B-Instruct 展示
|
| 3 |
+
short_description: Eino+硅基流动 Qwen2.5-7B-Instruct 展示
|
| 4 |
+
sdk: docker
|
| 5 |
+
app_port: 7860
|
| 6 |
+
colorFrom: blue
|
| 7 |
+
colorTo: green
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Eino 闭环最小项目(Go)
|
| 11 |
+
|
| 12 |
+
这是一个“能跑起来”的最小闭环示例:启动一个 HTTP 服务,提供 `/chat` 接口,并自带一个 Web 展示页。
|
| 13 |
+
|
| 14 |
+
- 有 `SILICONFLOW_API_KEY` 时:默认调用硅基流动的 `Qwen/Qwen2.5-7B-Instruct`
|
| 15 |
+
- 有 `OPENAI_API_KEY` 时:通过 CloudWeGo Eino(`BaseChatModel` 接口)+ OpenAI 兼容接口真实调用模型
|
| 16 |
+
- 没有 Key 时:自动进入 Mock 模式,保证你也能一键跑通闭环
|
| 17 |
+
|
| 18 |
+
## 目录结构
|
| 19 |
+
|
| 20 |
+
- `main.go`:HTTP 入口
|
| 21 |
+
- `internal/app`:路由与业务封装
|
| 22 |
+
- `web/index.html`:Web 展示页
|
| 23 |
+
|
| 24 |
+
## 快速开始
|
| 25 |
+
|
| 26 |
+
### 1) 准备环境
|
| 27 |
+
|
| 28 |
+
需要本机已安装 Go(建议 1.21+)。
|
| 29 |
+
|
| 30 |
+
### 2) 启动(Mock 模式,默认可跑)
|
| 31 |
+
|
| 32 |
+
在项目根目录执行:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
go run .
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
然后测试:
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
curl -s http://localhost:8080/healthz
|
| 42 |
+
curl -s http://localhost:8080/chat \
|
| 43 |
+
-H 'content-type: application/json' \
|
| 44 |
+
-d '{"input":"用一句话解释 Eino 是什么"}'
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
打开展示页:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
open http://localhost:8080
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### 3) 启动(真实调用:硅基流动 Qwen2.5-7B-Instruct)
|
| 54 |
+
|
| 55 |
+
把环境变量配上即可:
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
export SILICONFLOW_API_KEY="你的 key"
|
| 59 |
+
export SILICONFLOW_MODEL="Qwen/Qwen2.5-7B-Instruct"
|
| 60 |
+
export SILICONFLOW_BASE_URL="https://api.siliconflow.cn/v1"
|
| 61 |
+
|
| 62 |
+
go run .
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
然后打开展示页:
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
open http://localhost:8080
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
Hugging Face Spaces 会自动监听 `7860` 端口,不需要你手动设置 `PORT`。
|
| 72 |
+
|
| 73 |
+
### 4) 启动(真实调用:其他 OpenAI 兼容接口)
|
| 74 |
+
|
| 75 |
+
把环境变量配上即可:
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
export OPENAI_API_KEY="你的 key"
|
| 79 |
+
export OPENAI_MODEL="gpt-4o-mini"
|
| 80 |
+
# 可选:如果你用的是第三方/自建 OpenAI 兼容网关
|
| 81 |
+
# export OPENAI_BASE_URL="https://你的网关/v1"
|
| 82 |
+
|
| 83 |
+
go run .
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
然后:
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
curl -s http://localhost:8080/chat \
|
| 90 |
+
-H 'content-type: application/json' \
|
| 91 |
+
-d '{"input":"用两句话解释:什么是闭环?"}'
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## 接口说明
|
| 95 |
+
|
| 96 |
+
- `GET /healthz`:健康检查
|
| 97 |
+
- `POST /chat`:请求体 JSON
|
| 98 |
+
|
| 99 |
+
请求体:
|
| 100 |
+
|
| 101 |
+
```json
|
| 102 |
+
{ "input": "你好" }
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
响应体(示例):
|
| 106 |
+
|
| 107 |
+
```json
|
| 108 |
+
{
|
| 109 |
+
"ok": true,
|
| 110 |
+
"mock": true,
|
| 111 |
+
"model": "gpt-4o-mini",
|
| 112 |
+
"output": "(Mock)你输入了:你好"
|
| 113 |
+
}
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## 常见问题
|
| 117 |
+
|
| 118 |
+
- 如果你要走真实模型,但没有配置 `OPENAI_API_KEY`,会自动回退到 Mock。
|
| 119 |
+
- 如果 `go mod tidy` 卡住或报 `proxy.golang.org` 超时:可以用国内代理拉依赖,例如:
|
| 120 |
+
|
| 121 |
+
```bash
|
| 122 |
+
export GOPROXY=https://goproxy.cn,direct
|
| 123 |
+
export GOSUMDB=sum.golang.google.cn
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
- 这个项目没有依赖 `eino-ext`(目前 `eino-ext` 仓库还不包含 OpenAI 适配代码)。这里用 Eino 的 `components/model.BaseChatModel` 自己实现了一个最小 OpenAI 兼容适配器,代码在 `internal/einoopenai`。
|
| 127 |
+
|
| 128 |
+
- 出于安全原因,`SILICONFLOW_API_KEY` 不会写进仓库;请在 Hugging Face Spaces 的 Secrets 中配置它。
|
go.mod
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module eino-closedloop-cn
|
| 2 |
+
|
| 3 |
+
go 1.21
|
| 4 |
+
|
| 5 |
+
require github.com/cloudwego/eino v0.8.5
|
| 6 |
+
|
| 7 |
+
require (
|
| 8 |
+
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
| 9 |
+
github.com/buger/jsonparser v1.1.1 // indirect
|
| 10 |
+
github.com/bytedance/gopkg v0.1.3 // indirect
|
| 11 |
+
github.com/bytedance/sonic v1.15.0 // indirect
|
| 12 |
+
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
| 13 |
+
github.com/cloudwego/base64x v0.1.6 // indirect
|
| 14 |
+
github.com/dustin/go-humanize v1.0.1 // indirect
|
| 15 |
+
github.com/eino-contrib/jsonschema v1.0.3 // indirect
|
| 16 |
+
github.com/goph/emperror v0.17.2 // indirect
|
| 17 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
| 18 |
+
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
| 19 |
+
github.com/mailru/easyjson v0.7.7 // indirect
|
| 20 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 21 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 22 |
+
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
| 23 |
+
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
| 24 |
+
github.com/pkg/errors v0.9.1 // indirect
|
| 25 |
+
github.com/sirupsen/logrus v1.9.3 // indirect
|
| 26 |
+
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
|
| 27 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 28 |
+
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
| 29 |
+
github.com/yargevad/filepathx v1.0.0 // indirect
|
| 30 |
+
golang.org/x/arch v0.11.0 // indirect
|
| 31 |
+
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
|
| 32 |
+
golang.org/x/sys v0.26.0 // indirect
|
| 33 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 34 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
|
| 2 |
+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
| 3 |
+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
| 4 |
+
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
| 5 |
+
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
| 6 |
+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
| 7 |
+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
| 8 |
+
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
| 9 |
+
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
| 10 |
+
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
| 11 |
+
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
| 12 |
+
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
| 13 |
+
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
| 14 |
+
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
| 15 |
+
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
| 16 |
+
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
|
| 17 |
+
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
| 18 |
+
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
| 19 |
+
github.com/cloudwego/eino v0.8.5 h1:ZNRJBiOW8eEOxMKjR4KbxW9Px9+DtETi8k7Yk1cuzv8=
|
| 20 |
+
github.com/cloudwego/eino v0.8.5/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU=
|
| 21 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 22 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 23 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 24 |
+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
| 25 |
+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
| 26 |
+
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
| 27 |
+
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
|
| 28 |
+
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
| 29 |
+
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
| 30 |
+
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
|
| 31 |
+
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
| 32 |
+
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
| 33 |
+
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
| 34 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 35 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 36 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 37 |
+
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
|
| 38 |
+
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
|
| 39 |
+
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
| 40 |
+
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
| 41 |
+
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
| 42 |
+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
| 43 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 44 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 45 |
+
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
| 46 |
+
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
| 47 |
+
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
| 48 |
+
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
| 49 |
+
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
| 50 |
+
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
| 51 |
+
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
| 52 |
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
| 53 |
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
| 54 |
+
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
| 55 |
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
| 56 |
+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
| 57 |
+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
| 58 |
+
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
| 59 |
+
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
| 60 |
+
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
| 61 |
+
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
| 62 |
+
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
| 63 |
+
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
| 64 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 65 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 66 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 67 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 68 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 69 |
+
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
|
| 70 |
+
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
|
| 71 |
+
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
| 72 |
+
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
| 73 |
+
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
| 74 |
+
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
| 75 |
+
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
| 76 |
+
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
| 77 |
+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
| 78 |
+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
| 79 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 80 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 81 |
+
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
|
| 82 |
+
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
| 83 |
+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
| 84 |
+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
| 85 |
+
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
|
| 86 |
+
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
|
| 87 |
+
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
|
| 88 |
+
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
| 89 |
+
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
| 90 |
+
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
| 91 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 92 |
+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 93 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 94 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 95 |
+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
| 96 |
+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
| 97 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 98 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 99 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 100 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 101 |
+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
| 102 |
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
| 103 |
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
| 104 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 105 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 106 |
+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
| 107 |
+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
| 108 |
+
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
| 109 |
+
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
| 110 |
+
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
|
| 111 |
+
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
|
| 112 |
+
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
|
| 113 |
+
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
| 114 |
+
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
| 115 |
+
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
| 116 |
+
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
| 117 |
+
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
| 118 |
+
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
| 119 |
+
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
| 120 |
+
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 121 |
+
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 122 |
+
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 123 |
+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 124 |
+
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
| 125 |
+
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
| 126 |
+
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
| 127 |
+
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
| 128 |
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
| 129 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 130 |
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
| 131 |
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 132 |
+
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
| 133 |
+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
| 134 |
+
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 135 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 136 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 137 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
internal/app/handlers.go
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package app
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"net/http"
|
| 6 |
+
"strings"
|
| 7 |
+
"time"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type chatRequest struct {
|
| 11 |
+
Input string `json:"input"`
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
type chatResponse struct {
|
| 15 |
+
OK bool `json:"ok"`
|
| 16 |
+
Mock bool `json:"mock"`
|
| 17 |
+
Model string `json:"model"`
|
| 18 |
+
Output string `json:"output"`
|
| 19 |
+
Error string `json:"error,omitempty"`
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
func RegisterRoutes(mux *http.ServeMux) {
|
| 23 |
+
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
| 24 |
+
w.Header().Set("content-type", "application/json; charset=utf-8")
|
| 25 |
+
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
|
| 26 |
+
})
|
| 27 |
+
|
| 28 |
+
mux.HandleFunc("/chat", func(w http.ResponseWriter, r *http.Request) {
|
| 29 |
+
w.Header().Set("content-type", "application/json; charset=utf-8")
|
| 30 |
+
|
| 31 |
+
if r.Method != http.MethodPost {
|
| 32 |
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
| 33 |
+
_ = json.NewEncoder(w).Encode(chatResponse{OK: false, Error: "仅支持 POST"})
|
| 34 |
+
return
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
var req chatRequest
|
| 38 |
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
| 39 |
+
w.WriteHeader(http.StatusBadRequest)
|
| 40 |
+
_ = json.NewEncoder(w).Encode(chatResponse{OK: false, Error: "JSON 解析失败"})
|
| 41 |
+
return
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
req.Input = strings.TrimSpace(req.Input)
|
| 45 |
+
if req.Input == "" {
|
| 46 |
+
w.WriteHeader(http.StatusBadRequest)
|
| 47 |
+
_ = json.NewEncoder(w).Encode(chatResponse{OK: false, Error: "input 不能为空"})
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
ctx, cancel := withTimeout(r.Context(), 25*time.Second)
|
| 52 |
+
defer cancel()
|
| 53 |
+
|
| 54 |
+
out, meta, err := ChatOnce(ctx, req.Input)
|
| 55 |
+
if err != nil {
|
| 56 |
+
w.WriteHeader(http.StatusBadGateway)
|
| 57 |
+
_ = json.NewEncoder(w).Encode(chatResponse{OK: false, Error: err.Error(), Mock: meta.Mock, Model: meta.Model})
|
| 58 |
+
return
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
_ = json.NewEncoder(w).Encode(chatResponse{OK: true, Mock: meta.Mock, Model: meta.Model, Output: out})
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
fs := http.FileServer(http.Dir("web"))
|
| 65 |
+
mux.Handle("/", fs)
|
| 66 |
+
}
|
internal/app/timeouts.go
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package app
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"time"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
func withTimeout(parent context.Context, d time.Duration) (context.Context, context.CancelFunc) {
|
| 9 |
+
if parent == nil {
|
| 10 |
+
parent = context.Background()
|
| 11 |
+
}
|
| 12 |
+
return context.WithTimeout(parent, d)
|
| 13 |
+
}
|
internal/app/worker.go
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package app
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"strings"
|
| 8 |
+
|
| 9 |
+
"eino-closedloop-cn/internal/einoopenai"
|
| 10 |
+
|
| 11 |
+
"github.com/cloudwego/eino/components/model"
|
| 12 |
+
"github.com/cloudwego/eino/schema"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
type chatMeta struct {
|
| 16 |
+
Mock bool
|
| 17 |
+
Model string
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func ChatOnce(ctx context.Context, input string) (string, chatMeta, error) {
|
| 21 |
+
apiKey := strings.TrimSpace(os.Getenv("SILICONFLOW_API_KEY"))
|
| 22 |
+
modelName := strings.TrimSpace(os.Getenv("SILICONFLOW_MODEL"))
|
| 23 |
+
if apiKey == "" {
|
| 24 |
+
apiKey = strings.TrimSpace(os.Getenv("OPENAI_API_KEY"))
|
| 25 |
+
modelName = strings.TrimSpace(os.Getenv("OPENAI_MODEL"))
|
| 26 |
+
}
|
| 27 |
+
if modelName == "" {
|
| 28 |
+
modelName = "Qwen/Qwen2.5-7B-Instruct"
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
if apiKey == "" {
|
| 32 |
+
return fmt.Sprintf("(Mock)你输入了:%s", input), chatMeta{Mock: true, Model: modelName}, nil
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
baseURL := strings.TrimSpace(os.Getenv("SILICONFLOW_BASE_URL"))
|
| 36 |
+
if baseURL == "" {
|
| 37 |
+
baseURL = strings.TrimSpace(os.Getenv("OPENAI_BASE_URL"))
|
| 38 |
+
}
|
| 39 |
+
if baseURL == "" && strings.TrimSpace(os.Getenv("SILICONFLOW_API_KEY")) != "" {
|
| 40 |
+
baseURL = "https://api.siliconflow.cn/v1"
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
chatModel := einoopenai.NewChatModel(einoopenai.ChatModelConfig{
|
| 44 |
+
APIKey: apiKey,
|
| 45 |
+
BaseURL: baseURL,
|
| 46 |
+
Model: modelName,
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
resp, err := chatModel.Generate(ctx, []*schema.Message{
|
| 50 |
+
schema.UserMessage(input),
|
| 51 |
+
}, model.WithModel(modelName))
|
| 52 |
+
if err != nil {
|
| 53 |
+
return "", chatMeta{Mock: false, Model: modelName}, err
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
return strings.TrimSpace(resp.Content), chatMeta{Mock: false, Model: modelName}, nil
|
| 57 |
+
}
|
internal/einoopenai/chat_model.go
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package einoopenai
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"errors"
|
| 8 |
+
"fmt"
|
| 9 |
+
"io"
|
| 10 |
+
"net/http"
|
| 11 |
+
"net/url"
|
| 12 |
+
"strings"
|
| 13 |
+
"time"
|
| 14 |
+
|
| 15 |
+
"github.com/cloudwego/eino/components/model"
|
| 16 |
+
"github.com/cloudwego/eino/schema"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
type ChatModelConfig struct {
|
| 20 |
+
APIKey string
|
| 21 |
+
BaseURL string
|
| 22 |
+
Model string
|
| 23 |
+
|
| 24 |
+
HTTPClient *http.Client
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
type ChatModel struct {
|
| 28 |
+
apiKey string
|
| 29 |
+
baseURL string
|
| 30 |
+
model string
|
| 31 |
+
client *http.Client
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func NewChatModel(cfg ChatModelConfig) *ChatModel {
|
| 35 |
+
baseURL := strings.TrimSpace(cfg.BaseURL)
|
| 36 |
+
if baseURL == "" {
|
| 37 |
+
baseURL = "https://api.openai.com/v1"
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
modelName := strings.TrimSpace(cfg.Model)
|
| 41 |
+
if modelName == "" {
|
| 42 |
+
modelName = "gpt-4o-mini"
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
client := cfg.HTTPClient
|
| 46 |
+
if client == nil {
|
| 47 |
+
client = &http.Client{Timeout: 60 * time.Second}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return &ChatModel{
|
| 51 |
+
apiKey: strings.TrimSpace(cfg.APIKey),
|
| 52 |
+
baseURL: baseURL,
|
| 53 |
+
model: modelName,
|
| 54 |
+
client: client,
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
func (m *ChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
|
| 59 |
+
if strings.TrimSpace(m.apiKey) == "" {
|
| 60 |
+
return nil, errors.New("OPENAI_API_KEY 为空")
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
common := model.GetCommonOptions(&model.Options{Model: &m.model}, opts...)
|
| 64 |
+
modelName := m.model
|
| 65 |
+
if common.Model != nil && strings.TrimSpace(*common.Model) != "" {
|
| 66 |
+
modelName = strings.TrimSpace(*common.Model)
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
msgs := make([]openAIMessage, 0, len(input))
|
| 70 |
+
for _, msg := range input {
|
| 71 |
+
if msg == nil {
|
| 72 |
+
continue
|
| 73 |
+
}
|
| 74 |
+
content := strings.TrimSpace(msg.Content)
|
| 75 |
+
if content == "" {
|
| 76 |
+
continue
|
| 77 |
+
}
|
| 78 |
+
msgs = append(msgs, openAIMessage{Role: string(msg.Role), Content: content})
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if len(msgs) == 0 {
|
| 82 |
+
return nil, errors.New("输入消息为空")
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
reqBody := openAIChatCompletionRequest{
|
| 86 |
+
Model: modelName,
|
| 87 |
+
Messages: msgs,
|
| 88 |
+
Temperature: common.Temperature,
|
| 89 |
+
MaxTokens: common.MaxTokens,
|
| 90 |
+
TopP: common.TopP,
|
| 91 |
+
Stop: common.Stop,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
respBody, status, err := m.doJSON(ctx, http.MethodPost, "/chat/completions", reqBody)
|
| 95 |
+
if err != nil {
|
| 96 |
+
return nil, err
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
if status >= 400 {
|
| 100 |
+
msg := parseOpenAIError(respBody)
|
| 101 |
+
if msg == "" {
|
| 102 |
+
msg = string(respBody)
|
| 103 |
+
}
|
| 104 |
+
return nil, fmt.Errorf("OpenAI 请求失败(HTTP %d):%s", status, msg)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
var out openAIChatCompletionResponse
|
| 108 |
+
if err := json.Unmarshal(respBody, &out); err != nil {
|
| 109 |
+
return nil, fmt.Errorf("OpenAI 响应解析失败:%w", err)
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
if len(out.Choices) == 0 {
|
| 113 |
+
return nil, errors.New("OpenAI 响应 choices 为空")
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
content := strings.TrimSpace(out.Choices[0].Message.Content)
|
| 117 |
+
return schema.AssistantMessage(content, nil), nil
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
func (m *ChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
| 121 |
+
sr, sw := schema.Pipe[*schema.Message](1)
|
| 122 |
+
go func() {
|
| 123 |
+
defer sw.Close()
|
| 124 |
+
msg, err := m.Generate(ctx, input, opts...)
|
| 125 |
+
if err != nil {
|
| 126 |
+
sw.Send(nil, err)
|
| 127 |
+
return
|
| 128 |
+
}
|
| 129 |
+
sw.Send(msg, nil)
|
| 130 |
+
}()
|
| 131 |
+
return sr, nil
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
func (m *ChatModel) doJSON(ctx context.Context, method, path string, body any) ([]byte, int, error) {
|
| 135 |
+
endpoint, err := joinURL(m.baseURL, path)
|
| 136 |
+
if err != nil {
|
| 137 |
+
return nil, 0, err
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
buf, err := json.Marshal(body)
|
| 141 |
+
if err != nil {
|
| 142 |
+
return nil, 0, err
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
req, err := http.NewRequestWithContext(ctx, method, endpoint, bytes.NewReader(buf))
|
| 146 |
+
if err != nil {
|
| 147 |
+
return nil, 0, err
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
req.Header.Set("content-type", "application/json")
|
| 151 |
+
req.Header.Set("authorization", "Bearer "+m.apiKey)
|
| 152 |
+
|
| 153 |
+
resp, err := m.client.Do(req)
|
| 154 |
+
if err != nil {
|
| 155 |
+
return nil, 0, err
|
| 156 |
+
}
|
| 157 |
+
defer resp.Body.Close()
|
| 158 |
+
|
| 159 |
+
respBytes, _ := io.ReadAll(resp.Body)
|
| 160 |
+
return respBytes, resp.StatusCode, nil
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
func joinURL(base, path string) (string, error) {
|
| 164 |
+
u, err := url.Parse(strings.TrimRight(strings.TrimSpace(base), "/"))
|
| 165 |
+
if err != nil {
|
| 166 |
+
return "", err
|
| 167 |
+
}
|
| 168 |
+
ref, err := url.Parse(path)
|
| 169 |
+
if err != nil {
|
| 170 |
+
return "", err
|
| 171 |
+
}
|
| 172 |
+
return u.ResolveReference(ref).String(), nil
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
type openAIMessage struct {
|
| 176 |
+
Role string `json:"role"`
|
| 177 |
+
Content string `json:"content"`
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
type openAIChatCompletionRequest struct {
|
| 181 |
+
Model string `json:"model"`
|
| 182 |
+
Messages []openAIMessage `json:"messages"`
|
| 183 |
+
Temperature *float32 `json:"temperature,omitempty"`
|
| 184 |
+
MaxTokens *int `json:"max_tokens,omitempty"`
|
| 185 |
+
TopP *float32 `json:"top_p,omitempty"`
|
| 186 |
+
Stop []string `json:"stop,omitempty"`
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
type openAIChatCompletionResponse struct {
|
| 190 |
+
Choices []struct {
|
| 191 |
+
Message openAIMessage `json:"message"`
|
| 192 |
+
} `json:"choices"`
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
type openAIErrorResponse struct {
|
| 196 |
+
Error struct {
|
| 197 |
+
Message string `json:"message"`
|
| 198 |
+
} `json:"error"`
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
func parseOpenAIError(b []byte) string {
|
| 202 |
+
var out openAIErrorResponse
|
| 203 |
+
if err := json.Unmarshal(b, &out); err != nil {
|
| 204 |
+
return ""
|
| 205 |
+
}
|
| 206 |
+
return strings.TrimSpace(out.Error.Message)
|
| 207 |
+
}
|
main.go
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"log"
|
| 6 |
+
"net/http"
|
| 7 |
+
"os"
|
| 8 |
+
"os/signal"
|
| 9 |
+
"syscall"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
"eino-closedloop-cn/internal/app"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
func main() {
|
| 16 |
+
port := os.Getenv("PORT")
|
| 17 |
+
if port == "" {
|
| 18 |
+
port = "8080"
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
mux := http.NewServeMux()
|
| 22 |
+
app.RegisterRoutes(mux)
|
| 23 |
+
|
| 24 |
+
srv := &http.Server{
|
| 25 |
+
Addr: ":" + port,
|
| 26 |
+
Handler: mux,
|
| 27 |
+
ReadHeaderTimeout: 5 * time.Second,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
errCh := make(chan error, 1)
|
| 31 |
+
go func() {
|
| 32 |
+
log.Printf("HTTP 服务已启动:http://localhost:%s", port)
|
| 33 |
+
errCh <- srv.ListenAndServe()
|
| 34 |
+
}()
|
| 35 |
+
|
| 36 |
+
stop := make(chan os.Signal, 1)
|
| 37 |
+
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
| 38 |
+
|
| 39 |
+
select {
|
| 40 |
+
case <-stop:
|
| 41 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 42 |
+
defer cancel()
|
| 43 |
+
_ = srv.Shutdown(ctx)
|
| 44 |
+
case err := <-errCh:
|
| 45 |
+
if err != nil && err != http.ErrServerClosed {
|
| 46 |
+
log.Fatalf("服务启动失败:%v", err)
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
web/index.html
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Eino + 硅基流动 Qwen2.5-7B-Instruct 展示</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
color-scheme: light;
|
| 10 |
+
}
|
| 11 |
+
body {
|
| 12 |
+
margin: 0;
|
| 13 |
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,
|
| 14 |
+
"Apple Color Emoji", "Segoe UI Emoji";
|
| 15 |
+
background: #ffffff;
|
| 16 |
+
color: #111827;
|
| 17 |
+
}
|
| 18 |
+
.wrap {
|
| 19 |
+
max-width: 980px;
|
| 20 |
+
margin: 0 auto;
|
| 21 |
+
padding: 28px 16px 48px;
|
| 22 |
+
}
|
| 23 |
+
.title {
|
| 24 |
+
display: flex;
|
| 25 |
+
align-items: baseline;
|
| 26 |
+
gap: 12px;
|
| 27 |
+
margin: 0 0 12px;
|
| 28 |
+
}
|
| 29 |
+
.title h1 {
|
| 30 |
+
font-size: 20px;
|
| 31 |
+
margin: 0;
|
| 32 |
+
font-weight: 700;
|
| 33 |
+
letter-spacing: 0.2px;
|
| 34 |
+
}
|
| 35 |
+
.badge {
|
| 36 |
+
font-size: 12px;
|
| 37 |
+
padding: 3px 10px;
|
| 38 |
+
border-radius: 999px;
|
| 39 |
+
border: 1px solid #e5e7eb;
|
| 40 |
+
background: #f9fafb;
|
| 41 |
+
color: #111827;
|
| 42 |
+
}
|
| 43 |
+
.sub {
|
| 44 |
+
margin: 0 0 18px;
|
| 45 |
+
color: #374151;
|
| 46 |
+
font-size: 13px;
|
| 47 |
+
line-height: 1.55;
|
| 48 |
+
}
|
| 49 |
+
.grid {
|
| 50 |
+
display: grid;
|
| 51 |
+
grid-template-columns: 1fr;
|
| 52 |
+
gap: 14px;
|
| 53 |
+
}
|
| 54 |
+
.card {
|
| 55 |
+
border: 1px solid #e5e7eb;
|
| 56 |
+
border-radius: 12px;
|
| 57 |
+
overflow: hidden;
|
| 58 |
+
background: #ffffff;
|
| 59 |
+
}
|
| 60 |
+
.cardHead {
|
| 61 |
+
padding: 12px 14px;
|
| 62 |
+
display: flex;
|
| 63 |
+
align-items: center;
|
| 64 |
+
justify-content: space-between;
|
| 65 |
+
gap: 12px;
|
| 66 |
+
border-bottom: 1px solid #e5e7eb;
|
| 67 |
+
background: #fafafa;
|
| 68 |
+
}
|
| 69 |
+
.cardHead strong {
|
| 70 |
+
font-size: 13px;
|
| 71 |
+
}
|
| 72 |
+
.right {
|
| 73 |
+
display: flex;
|
| 74 |
+
align-items: center;
|
| 75 |
+
gap: 10px;
|
| 76 |
+
flex-wrap: wrap;
|
| 77 |
+
}
|
| 78 |
+
.kv {
|
| 79 |
+
display: inline-flex;
|
| 80 |
+
gap: 6px;
|
| 81 |
+
align-items: baseline;
|
| 82 |
+
font-size: 12px;
|
| 83 |
+
color: #374151;
|
| 84 |
+
}
|
| 85 |
+
.kv code {
|
| 86 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
| 87 |
+
"Courier New", monospace;
|
| 88 |
+
background: #f3f4f6;
|
| 89 |
+
border: 1px solid #e5e7eb;
|
| 90 |
+
padding: 1px 6px;
|
| 91 |
+
border-radius: 8px;
|
| 92 |
+
color: #111827;
|
| 93 |
+
}
|
| 94 |
+
.cardBody {
|
| 95 |
+
padding: 14px;
|
| 96 |
+
}
|
| 97 |
+
textarea {
|
| 98 |
+
width: 100%;
|
| 99 |
+
box-sizing: border-box;
|
| 100 |
+
resize: vertical;
|
| 101 |
+
min-height: 92px;
|
| 102 |
+
border-radius: 10px;
|
| 103 |
+
border: 1px solid #d1d5db;
|
| 104 |
+
padding: 10px 12px;
|
| 105 |
+
font-size: 14px;
|
| 106 |
+
line-height: 1.5;
|
| 107 |
+
outline: none;
|
| 108 |
+
}
|
| 109 |
+
textarea:focus {
|
| 110 |
+
border-color: #2563eb;
|
| 111 |
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
| 112 |
+
}
|
| 113 |
+
.actions {
|
| 114 |
+
display: flex;
|
| 115 |
+
gap: 10px;
|
| 116 |
+
margin-top: 10px;
|
| 117 |
+
flex-wrap: wrap;
|
| 118 |
+
}
|
| 119 |
+
button {
|
| 120 |
+
appearance: none;
|
| 121 |
+
border: 1px solid #d1d5db;
|
| 122 |
+
background: #ffffff;
|
| 123 |
+
color: #111827;
|
| 124 |
+
border-radius: 10px;
|
| 125 |
+
padding: 9px 12px;
|
| 126 |
+
font-size: 13px;
|
| 127 |
+
font-weight: 600;
|
| 128 |
+
cursor: pointer;
|
| 129 |
+
}
|
| 130 |
+
button.primary {
|
| 131 |
+
border-color: #2563eb;
|
| 132 |
+
background: #2563eb;
|
| 133 |
+
color: #ffffff;
|
| 134 |
+
}
|
| 135 |
+
button:disabled {
|
| 136 |
+
opacity: 0.6;
|
| 137 |
+
cursor: not-allowed;
|
| 138 |
+
}
|
| 139 |
+
.out {
|
| 140 |
+
white-space: pre-wrap;
|
| 141 |
+
word-break: break-word;
|
| 142 |
+
font-size: 14px;
|
| 143 |
+
line-height: 1.65;
|
| 144 |
+
color: #111827;
|
| 145 |
+
}
|
| 146 |
+
.muted {
|
| 147 |
+
color: #6b7280;
|
| 148 |
+
font-size: 12px;
|
| 149 |
+
}
|
| 150 |
+
.err {
|
| 151 |
+
color: #b91c1c;
|
| 152 |
+
}
|
| 153 |
+
@media (min-width: 920px) {
|
| 154 |
+
.grid {
|
| 155 |
+
grid-template-columns: 1fr 1fr;
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
</style>
|
| 159 |
+
</head>
|
| 160 |
+
<body>
|
| 161 |
+
<div class="wrap">
|
| 162 |
+
<div class="title">
|
| 163 |
+
<h1>Eino + 硅基流动(OpenAI 兼容)展示</h1>
|
| 164 |
+
<span class="badge" id="statusBadge">未连接</span>
|
| 165 |
+
</div>
|
| 166 |
+
<p class="sub">
|
| 167 |
+
这个页面只负责“看效果”。API Key 不会出现在浏览器里,浏览器只请求本地的 Go 服务 `/chat`。
|
| 168 |
+
</p>
|
| 169 |
+
|
| 170 |
+
<div class="grid">
|
| 171 |
+
<div class="card">
|
| 172 |
+
<div class="cardHead">
|
| 173 |
+
<strong>输入</strong>
|
| 174 |
+
<div class="right">
|
| 175 |
+
<span class="kv">接口 <code>POST /chat</code></span>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="cardBody">
|
| 179 |
+
<textarea id="input" placeholder="输入你要问的问题…"></textarea>
|
| 180 |
+
<div class="actions">
|
| 181 |
+
<button class="primary" id="sendBtn">发送</button>
|
| 182 |
+
<button id="fillBtn">填入示例</button>
|
| 183 |
+
<button id="clearBtn">清空输出</button>
|
| 184 |
+
<span class="muted" id="hint"></span>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div class="card">
|
| 190 |
+
<div class="cardHead">
|
| 191 |
+
<strong>输出</strong>
|
| 192 |
+
<div class="right">
|
| 193 |
+
<span class="kv">模式 <code id="mode">-</code></span>
|
| 194 |
+
<span class="kv">模型 <code id="model">-</code></span>
|
| 195 |
+
<span class="kv">耗时 <code id="ms">-</code></span>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
<div class="cardBody">
|
| 199 |
+
<div class="out" id="output"></div>
|
| 200 |
+
<div class="muted" id="error"></div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<script>
|
| 207 |
+
const $ = (id) => document.getElementById(id)
|
| 208 |
+
|
| 209 |
+
const inputEl = $("input")
|
| 210 |
+
const outputEl = $("output")
|
| 211 |
+
const errorEl = $("error")
|
| 212 |
+
const sendBtn = $("sendBtn")
|
| 213 |
+
const fillBtn = $("fillBtn")
|
| 214 |
+
const clearBtn = $("clearBtn")
|
| 215 |
+
const hintEl = $("hint")
|
| 216 |
+
const statusBadge = $("statusBadge")
|
| 217 |
+
const modeEl = $("mode")
|
| 218 |
+
const modelEl = $("model")
|
| 219 |
+
const msEl = $("ms")
|
| 220 |
+
|
| 221 |
+
function setBusy(busy) {
|
| 222 |
+
sendBtn.disabled = busy
|
| 223 |
+
fillBtn.disabled = busy
|
| 224 |
+
clearBtn.disabled = busy
|
| 225 |
+
hintEl.textContent = busy ? "请求中…" : ""
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function setStatus(ok) {
|
| 229 |
+
statusBadge.textContent = ok ? "已连接" : "未连接"
|
| 230 |
+
statusBadge.style.background = ok ? "#ecfeff" : "#f9fafb"
|
| 231 |
+
statusBadge.style.borderColor = ok ? "#a5f3fc" : "#e5e7eb"
|
| 232 |
+
statusBadge.style.color = ok ? "#0e7490" : "#111827"
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
async function ping() {
|
| 236 |
+
try {
|
| 237 |
+
const r = await fetch("/healthz")
|
| 238 |
+
setStatus(r.ok)
|
| 239 |
+
} catch {
|
| 240 |
+
setStatus(false)
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
fillBtn.addEventListener("click", () => {
|
| 245 |
+
inputEl.value = "请用 3 条要点解释:Qwen2.5-7B-Instruct 适合做什么?"
|
| 246 |
+
inputEl.focus()
|
| 247 |
+
})
|
| 248 |
+
|
| 249 |
+
clearBtn.addEventListener("click", () => {
|
| 250 |
+
outputEl.textContent = ""
|
| 251 |
+
errorEl.textContent = ""
|
| 252 |
+
modeEl.textContent = "-"
|
| 253 |
+
modelEl.textContent = "-"
|
| 254 |
+
msEl.textContent = "-"
|
| 255 |
+
})
|
| 256 |
+
|
| 257 |
+
sendBtn.addEventListener("click", async () => {
|
| 258 |
+
const text = (inputEl.value || "").trim()
|
| 259 |
+
if (!text) return
|
| 260 |
+
|
| 261 |
+
setBusy(true)
|
| 262 |
+
errorEl.textContent = ""
|
| 263 |
+
errorEl.className = "muted"
|
| 264 |
+
|
| 265 |
+
const t0 = performance.now()
|
| 266 |
+
try {
|
| 267 |
+
const r = await fetch("/chat", {
|
| 268 |
+
method: "POST",
|
| 269 |
+
headers: { "content-type": "application/json" },
|
| 270 |
+
body: JSON.stringify({ input: text }),
|
| 271 |
+
})
|
| 272 |
+
const data = await r.json().catch(() => ({}))
|
| 273 |
+
const t1 = performance.now()
|
| 274 |
+
msEl.textContent = `${Math.round(t1 - t0)}ms`
|
| 275 |
+
|
| 276 |
+
if (!r.ok || !data.ok) {
|
| 277 |
+
const msg = data.error || `请求失败(HTTP ${r.status})`
|
| 278 |
+
errorEl.textContent = msg
|
| 279 |
+
errorEl.className = "muted err"
|
| 280 |
+
return
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
modeEl.textContent = data.mock ? "mock" : "real"
|
| 284 |
+
modelEl.textContent = data.model || "-"
|
| 285 |
+
outputEl.textContent = data.output || ""
|
| 286 |
+
} catch (e) {
|
| 287 |
+
errorEl.textContent = e?.message || "请求异常"
|
| 288 |
+
errorEl.className = "muted err"
|
| 289 |
+
} finally {
|
| 290 |
+
setBusy(false)
|
| 291 |
+
ping()
|
| 292 |
+
}
|
| 293 |
+
})
|
| 294 |
+
|
| 295 |
+
ping()
|
| 296 |
+
</script>
|
| 297 |
+
</body>
|
| 298 |
+
</html>
|
| 299 |
+
|