Compare commits

..
Author SHA1 Message Date
cat-shark 435beaffbb refactor: 导图生成仅使用学习报告 2026-08-03 23:08:40 +08:00
cat-shark 3f504976ea fix: 流水线kubeconfig改用临时目录 2026-08-02 10:51:03 +08:00
cat-shark 81dfb4fc3e docs: 更新提交规范 2026-08-01 00:15:59 +08:00
cat-shark a71feea4fd docs: migrate .pi/SYSTEM.md to AGENTS.md 2026-08-01 00:13:08 +08:00
cat-shark 9f620e84f8 chore: remove k8s/ — migrated to lpt-infra 2026-07-26 21:35:51 +08:00
cat-shark a1cfb045ba feat: add deployment.yaml and service.yaml for lpt-ai K8s deployment 2026-07-26 20:52:03 +08:00
cat-shark e106a8bfdc docs: 补充Git提交规范 2026-07-23 23:03:21 +08:00
cat-shark e6bcf0db15 docs: add .pi/SYSTEM.md with project overview 2026-07-23 23:01:47 +08:00
cat-sharkandClaude Opus 4.8 943faf0ba9 fix(ci): 限制仅 main 部署并加固 IMAGE_TAG 传递
- lpt-ai 仅 main/master 可部署;dev/prod 靠 environment 区分 namespace
- IMAGE_TAG 写 env 文件 + 工作区文件兜底

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:47:13 +08:00
cat-sharkandClaude Opus 4.8 977a031393 Add shared Kubernetes configuration files
- Add k8s/configmap.yaml with shared environment variables
- Add k8s/lpt-secrets.yaml template (secrets not committed)
- Add k8s/regcred-secret.yaml template (managed by workflow)
- Add k8s/README.md with deployment instructions
- Remove incorrect deployment/service files from root k8s/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:27:50 +08:00
cat-sharkandClaude Opus 4.8 5497dfc290 Add Kubernetes deployment configurations
- Add deployment.yaml with imagePullSecrets and env configuration
- Add service.yaml with ClusterIP configuration
- Configure LLM API key from Secret and other env from ConfigMap
- Ensures proper image pull authentication from private registry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:24:09 +08:00
6 changed files with 204 additions and 37 deletions
+95 -25
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
environment:
description: '部署环境'
description: '部署环境(本仓库仅 main 分支;dev/prod 均从 main 部署)'
required: true
type: choice
options:
@@ -19,67 +19,137 @@ env:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
env:
KUBECONFIG: /tmp/kubeconfig
steps:
- name: Validate branch ↔ environment
run: |
set -euo pipefail
REF="${{ gitea.ref }}"
BRANCH="${REF#refs/heads/}"
if [[ "$REF" == refs/tags/* ]]; then
echo "ERROR: 请从分支触发部署,不要用 tag。当前 ref=$REF"
exit 1
fi
ENV="${{ inputs.environment }}"
echo "branch=$BRANCH environment=$ENV sha=${{ gitea.sha }}"
# lpt-ai 仅维护 maindev/prod 都从 main 出包,靠 environment 区分 namespace/镜像 tag
case "$BRANCH" in
main|master)
echo "OK: lpt-ai 从 $BRANCH 部署到 $ENV"
;;
*)
echo "ERROR: lpt-ai 只能从 main/master 部署,当前分支是 '$BRANCH'"
exit 1
;;
esac
case "$ENV" in
dev|prod) ;;
*)
echo "ERROR: 未知 environment=$ENV"
exit 1
;;
esac
- name: Checkout code
run: |
git clone $REPO_URL .
git checkout ${{ gitea.sha }}
set -euo pipefail
git clone "$REPO_URL" .
git checkout "${{ gitea.sha }}"
- name: Login to Docker Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login $REGISTRY -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
set -euo pipefail
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
- name: Build & Push Docker image
run: |
TAG=${{ gitea.sha }}-$(date +%s)
docker build -t $REGISTRY/$APP:$TAG -t $REGISTRY/$APP:${{ inputs.environment }} .
docker push $REGISTRY/$APP:$TAG
docker push $REGISTRY/$APP:${{ inputs.environment }}
echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV
set -euo pipefail
ENV="${{ inputs.environment }}"
TAG="${{ gitea.sha }}-$(date +%s)"
echo "Building $REGISTRY/$APP:$TAG (env tag=$ENV)"
docker build -t "$REGISTRY/$APP:$TAG" -t "$REGISTRY/$APP:$ENV" .
docker push "$REGISTRY/$APP:$TAG"
docker push "$REGISTRY/$APP:$ENV"
if [ -n "${GITHUB_ENV:-}" ]; then
echo "IMAGE_TAG=$TAG" >> "$GITHUB_ENV"
fi
if [ -n "${GITEA_ENV:-}" ]; then
echo "IMAGE_TAG=$TAG" >> "$GITEA_ENV"
fi
echo "$TAG" > image_tag.txt
if [ -n "${{ runner.temp }}" ]; then
echo "$TAG" > "${{ runner.temp }}/image_tag.txt" || true
fi
echo "IMAGE_TAG=$TAG"
- name: Setup kubectl
run: |
set -euo pipefail
curl -sLO "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > ~/.kube/config
echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > /tmp/kubeconfig
chmod 600 /tmp/kubeconfig
- name: Create/Update imagePullSecret
run: |
set -euo pipefail
kubectl create secret docker-registry regcred \
--docker-server=$REGISTRY \
--docker-server="$REGISTRY" \
--docker-username="${{ secrets.REGISTRY_USERNAME }}" \
--docker-password="${{ secrets.REGISTRY_PASSWORD }}" \
-n lpt-${{ inputs.environment }} \
-n "lpt-${{ inputs.environment }}" \
--dry-run=client -o yaml | kubectl apply -f -
- name: Deploy to K8s
run: |
kubectl set image deployment/$APP $APP=$REGISTRY/$APP:$IMAGE_TAG -n lpt-${{ inputs.environment }} --record
kubectl rollout status deployment/$APP -n lpt-${{ inputs.environment }} --timeout=5m
set -euo pipefail
ENV="${{ inputs.environment }}"
IMAGE_TAG="${IMAGE_TAG:-}"
if [ -z "$IMAGE_TAG" ] && [ -f image_tag.txt ]; then
IMAGE_TAG="$(cat image_tag.txt)"
fi
if [ -z "$IMAGE_TAG" ] && [ -f "${{ runner.temp }}/image_tag.txt" ]; then
IMAGE_TAG="$(cat "${{ runner.temp }}/image_tag.txt")"
fi
if [ -z "$IMAGE_TAG" ]; then
echo "ERROR: IMAGE_TAG 为空,无法部署"
exit 1
fi
echo "Deploying $REGISTRY/$APP:$IMAGE_TAG -> namespace lpt-$ENV"
kubectl set image "deployment/$APP" \
"$APP=$REGISTRY/$APP:$IMAGE_TAG" \
-n "lpt-$ENV" --record
kubectl rollout status "deployment/$APP" \
-n "lpt-$ENV" --timeout=5m
- name: Debug on failure
if: failure()
run: |
ENV="${{ inputs.environment }}"
echo "=== Deployment Status ==="
kubectl get deployment $APP -n lpt-${{ inputs.environment }}
kubectl get deployment "$APP" -n "lpt-$ENV" || true
echo ""
echo "=== Pod Status ==="
kubectl get pods -n lpt-${{ inputs.environment }} -l app=$APP
kubectl get pods -n "lpt-$ENV" -l "app=$APP" || true
echo ""
echo "=== Recent Events ==="
kubectl get events -n lpt-${{ inputs.environment }} --sort-by='.lastTimestamp' | tail -20
kubectl get events -n "lpt-$ENV" --sort-by='.lastTimestamp' | tail -20 || true
echo ""
echo "=== Pod Describe (latest) ==="
POD=$(kubectl get pods -n lpt-${{ inputs.environment }} -l app=$APP --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}')
kubectl describe pod $POD -n lpt-${{ inputs.environment }} || true
echo ""
echo "=== Pod Logs (latest) ==="
kubectl logs $POD -n lpt-${{ inputs.environment }} --tail=50 || true
POD=$(kubectl get pods -n "lpt-$ENV" -l "app=$APP" --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}' 2>/dev/null || true)
if [ -n "${POD:-}" ]; then
kubectl describe pod "$POD" -n "lpt-$ENV" || true
echo ""
echo "=== Pod Logs (latest) ==="
kubectl logs "$POD" -n "lpt-$ENV" --tail=50 || true
fi
- name: Rollback on failure
if: failure()
run: |
kubectl rollout undo deployment/$APP -n lpt-${{ inputs.environment }}
kubectl rollout status deployment/$APP -n lpt-${{ inputs.environment }}
set -euo pipefail
ENV="${{ inputs.environment }}"
kubectl rollout undo "deployment/$APP" -n "lpt-$ENV" || true
kubectl rollout status "deployment/$APP" -n "lpt-$ENV" || true
+104
View File
@@ -0,0 +1,104 @@
# LPT AI 服务(lpt-ai
> TypeScript + Fastify 独立 AI 服务,集成 SiliconFlow LLM,为后端提供异步 AI 任务。
## Git 提交规范
- 提交信息必须简短且使用中文,不要使用英文长句。
- 格式:`类型: 简述`,例如 `feat: 新增思维导图生成任务``fix: 修复任务队列TTL清理``docs: 补充环境变量说明`
## 技术栈
- 运行时:Node.js 20+
- 框架:Fastify 5
- LLMSiliconFlow OpenAI 兼容接口(Qwen/Qwen2.5-32B-Instruct
- 运行时依赖:除 fastify 外无其他外部依赖
## 项目结构
```text
lpt-ai/src/
├── index.ts → 服务入口(加载 .env,启动 Fastify
├── routes/ai.ts → AI 路由(health, tasks, fetch-title
├── llm/
│ ├── client.ts → LLM 客户端(SiliconFlow 接口)
│ ├── prompts.ts → Prompt 模板(3 种任务类型)
│ └── prompts.test.ts
├── task-queue.ts → 异步任务队列
└── admin/
├── index.ts → 管理面板路由
├── routes.ts
└── store.ts → 日志存储
```
## API 端点(5 个)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/health` | 健康检查(返回 LLM 可用状态和模型名) |
| POST | `/ai/tasks` | 提交异步任务(返回 taskId) |
| GET | `/ai/tasks/:taskId` | 轮询任务结果 |
| POST | `/fetch-title` | 抓取网页标题 |
| GET | `/admin` | 管理面板(查看最近 200 条日志) |
## 支持的任务类型
| 类型 | 说明 | 温度 | Max Tokens |
|------|------|------|------------|
| `aggregate-report` | 残片聚合为学习报告 | 0.3 | 2048 |
| `generate-mind-map` | 从报告/残片生成思维导图大纲 | 0.3 | 4096 |
| `compare-recall` | 用户回忆 vs 标准导图语义对比 | 0.2 | 4096 |
## 任务队列设计
- 异步任务模式:submit + poll,避免 LLM 长耗时(10-60s)超时。
- 单线程 Worker`setImmediate` 链,同一时刻只处理一个 LLM 调用,防止 API 限流。
- TTL 清理:5 分钟一次,移除完成超过 1 小时的记录。
- 异常隔离:单个任务失败不影响后续任务。
- 管理面板日志:复用 `logStore`,记录最近 200 条请求/响应。
## LLM 配置
```env
LLM_API_URL=https://api.siliconflow.cn/v1/chat/completions
LLM_API_KEY= # 必填,未配置时 /health 返回 llmAvailable: false
LLM_MODEL=Qwen/Qwen2.5-32B-Instruct
LLM_TIMEOUT_MS=60000 # 单次 LLM 调用超时
PORT=5199
```
## 降级策略
- LLM API Key 未配置 → `/health` 返回 `llmAvailable: false`
- 后端(lpt-be)检测到 AI 不可用 → 自动切换到内置规则引擎(`BuiltinMindMapGenerator`)。
- Java 后端是唯一调用方;本服务不直接暴露给前端,不做鉴权(部署在内网)。
## 启动命令
```bash
npm install
npm run dev # 开发模式(tsx watch,热重载)
npm run build # 编译 TypeScript
npm run start # 生产模式
npm run test # 测试
```
## Docker
- 多阶段构建:`node:20-alpine` 编译 → `node:20-alpine` 运行。
- 暴露端口:5199。
- 健康检查:`curl http://localhost:5199/health`
## 关联项目
| 项目 | 路径 | 端口 | 说明 |
|------|------|------|------|
| lpt-be | `../lpt-be/` | 5157 | Spring Boot 后端,通过 `lpt.ai-service.url` 调用本服务 |
| lpt-fe | `../lpt-fe/` | 5158 | Vue 3 前端,不直接调用本服务 |
## 调用关系
```text
lpt-be (5157) ──HTTP POST /ai/tasks──→ lpt-ai (5199)
lpt-be (5157) ──HTTP GET /ai/tasks/:id──→ lpt-ai (5199)
```
+1 -1
View File
@@ -85,7 +85,7 @@ curl http://localhost:5199/ai/tasks/550e8400-e29b-41d4-a716-446655440000
| type | 说明 | params 字段 |
|------|------|------------|
| `aggregate-report` | 聚合学习残片为报告 | `taskName`, `expectation`, `fragments[]` |
| `generate-mind-map` | 生成思维导图大纲 | `taskName`, `reports[]`, `fragments[]`, `applications[]` |
| `generate-mind-map` | 生成思维导图大纲 | `taskName`, `reports[]`, `applications[]` |
| `compare-recall` | 对比用户回忆与标准导图 | `taskName`, `standardOutline`, `recallOutline` |
## 架构位置
+2 -3
View File
@@ -32,15 +32,14 @@ describe("aggregateReportPrompt", () => {
});
describe("generateMindMapPrompt", () => {
it("要求输出缩进大纲且包含全部输入", () => {
it("要求输出缩进大纲且包含学习报告", () => {
const messages = generateMindMapPrompt(
"Java 并发",
"并发编程学习",
["报告一"],
["残片一", "残片二"],
);
expect(messages[0].content).toContain("缩进");
expect(messages[1].content).toContain("报告一");
expect(messages[1].content).toContain("残片");
expect(messages[1].content).not.toContain("学习残片");
});
});
+2 -6
View File
@@ -50,20 +50,19 @@ export function aggregateReportPrompt(
}
/**
* 从学习报告和残片生成标准思维导图(预留,用于替代 Java 端 BuiltinMindMapGenerator)。
* 从学习报告生成标准思维导图(预留,用于替代 Java 端 BuiltinMindMapGenerator)。
* 输出为缩进大纲文本,与后端 MindMapTreeTool.parseOutline 格式一致。
*/
export function generateMindMapPrompt(
taskName: string,
taskDescription: string,
reports: string[],
fragments: string[],
): ChatMessage[] {
return [
{
role: "system",
content: [
"你是一名知识结构整理助手。请阅读某个学习任务的全部学习报告和残片",
"你是一名知识结构整理助手。请阅读某个学习任务的全部学习报告,",
"将其中的知识点整理成一份思维导图大纲。要求:",
"1. 第一行是导图根标题(任务主题);",
"2. 之后每行一个节点,用两个空格的缩进表示层级;",
@@ -81,9 +80,6 @@ export function generateMindMapPrompt(
"",
"学习报告:",
...reports.map((r, i) => `${i + 1}. ${r}`),
"",
"学习残片:",
...fragments.map((f, i) => `${i + 1}. ${f}`),
]
.filter(Boolean)
.join("\n"),
-2
View File
@@ -160,12 +160,10 @@ function buildMessages(task: TaskRecord) {
}
case "generate-mind-map": {
const reports = p.reports as string[] | undefined;
const fragments = p.fragments as string[] | undefined;
return generateMindMapPrompt(
p.taskName as string,
(p.taskDescription as string) ?? "",
reports ?? [],
fragments ?? [],
);
}
case "compare-recall": {