ltcai 0.1.4 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/docs/OPERATIONS.md +149 -0
- package/knowledge_graph.py +802 -0
- package/ltcai_cli.py +45 -1
- package/package.json +15 -3
- package/requirements.txt +1 -0
- package/server.py +665 -28
- package/skills/SKILL_TEMPLATE.md +57 -0
- package/skills/code_review/SKILL.md +76 -0
- package/skills/data_analysis/SKILL.md +79 -0
- package/skills/file_edit/SKILL.md +68 -0
- package/skills/web_search/SKILL.md +74 -0
- package/static/account.html +14 -2
- package/static/admin.html +225 -6
- package/static/chat.html +644 -140
- package/static/graph.html +612 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/manifest.json +35 -0
- package/static/sw.js +51 -0
- package/telegram_bot.py +631 -217
- package/tests/__init__.py +0 -0
- package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/test_api.py +94 -0
- package/tests/unit/__init__.py +0 -0
- package/tests/unit/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/unit/__pycache__/test_tools.cpython-314-pytest-9.0.3.pyc +0 -0
- package/tests/unit/test_tools.py +127 -0
- package/tools.py +169 -13
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Skill: <name>
|
|
2
|
+
|
|
3
|
+
## 메타데이터
|
|
4
|
+
- **버전**: 0.1.0
|
|
5
|
+
- **카테고리**: [coding | data | document | web | system | analysis]
|
|
6
|
+
- **위험도**: [low | medium | high] <!-- low=읽기, medium=쓰기, high=실행/삭제 -->
|
|
7
|
+
- **필요 권한**: [none | local_read | local_write | exec | admin]
|
|
8
|
+
|
|
9
|
+
## 설명
|
|
10
|
+
한 줄 설명.
|
|
11
|
+
|
|
12
|
+
## 입력 스키마
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"required": ["field1"],
|
|
16
|
+
"optional": ["field2"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"field1": { "type": "string", "description": "..." },
|
|
19
|
+
"field2": { "type": "integer", "default": 10 }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 출력 스키마
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"success": true,
|
|
28
|
+
"result": "...",
|
|
29
|
+
"artifacts": []
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 실행 조건
|
|
34
|
+
- 사전 조건 (예: 모델 로드 필요, 파일 존재 여부)
|
|
35
|
+
- 후처리 조건
|
|
36
|
+
|
|
37
|
+
## 예제
|
|
38
|
+
|
|
39
|
+
### 성공 케이스
|
|
40
|
+
**입력**: `{ "field1": "예시값" }`
|
|
41
|
+
**출력**: `{ "success": true, "result": "..." }`
|
|
42
|
+
|
|
43
|
+
### 실패 케이스
|
|
44
|
+
**입력**: `{ "field1": "" }`
|
|
45
|
+
**출력**: `{ "success": false, "error": "field1 is required" }`
|
|
46
|
+
|
|
47
|
+
## 실패 처리
|
|
48
|
+
| 에러 코드 | 원인 | 처리 방법 |
|
|
49
|
+
|-----------|------|-----------|
|
|
50
|
+
| `INVALID_INPUT` | 필수 필드 누락 | 입력 검증 후 재시도 |
|
|
51
|
+
| `PERMISSION_DENIED` | 권한 없음 | 관리자에게 문의 |
|
|
52
|
+
| `TIMEOUT` | 실행 시간 초과 | 작업 분할 후 재시도 |
|
|
53
|
+
|
|
54
|
+
## 테스트 케이스
|
|
55
|
+
```python
|
|
56
|
+
# tests/skills/test_<name>.py 참조
|
|
57
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Skill: code_review
|
|
2
|
+
|
|
3
|
+
## 메타데이터
|
|
4
|
+
- **버전**: 0.1.0
|
|
5
|
+
- **카테고리**: coding
|
|
6
|
+
- **위험도**: low
|
|
7
|
+
- **필요 권한**: local_read
|
|
8
|
+
|
|
9
|
+
## 설명
|
|
10
|
+
파일 또는 코드 스니펫을 LLM에 전달해 버그, 보안 이슈, 성능, 스타일을 리뷰한다.
|
|
11
|
+
|
|
12
|
+
## 입력 스키마
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"required": ["target"],
|
|
16
|
+
"optional": ["focus", "lang", "max_lines"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"target": { "type": "string", "description": "절대 파일 경로 또는 코드 스니펫 문자열" },
|
|
19
|
+
"focus": { "type": "array", "items": { "type": "string", "enum": ["bug", "security", "performance", "style"] }, "default": ["bug", "security"], "description": "리뷰 초점 목록" },
|
|
20
|
+
"lang": { "type": "string", "description": "언어 힌트 (python, js, go …). 미입력시 자동 감지" },
|
|
21
|
+
"max_lines": { "type": "integer", "default": 500, "description": "분석할 최대 줄 수" }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 출력 스키마
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"success": true,
|
|
30
|
+
"result": {
|
|
31
|
+
"summary": "...",
|
|
32
|
+
"issues": [
|
|
33
|
+
{ "severity": "high|medium|low", "line": 42, "category": "security", "message": "..." }
|
|
34
|
+
],
|
|
35
|
+
"score": 85
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 실행 조건
|
|
41
|
+
- LLM 모델이 로드되어 있어야 함 (`/mode` 응답의 `model` 필드 비어있지 않음)
|
|
42
|
+
- 파일 대상인 경우 파일이 존재해야 함
|
|
43
|
+
|
|
44
|
+
## 예제
|
|
45
|
+
|
|
46
|
+
### 성공 케이스
|
|
47
|
+
**입력**: `{ "target": "~/project/server.py", "focus": ["security"] }`
|
|
48
|
+
**출력**:
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"success": true,
|
|
52
|
+
"result": {
|
|
53
|
+
"summary": "1개의 심각한 보안 이슈 발견",
|
|
54
|
+
"issues": [{ "severity": "high", "line": 102, "category": "security", "message": "SQL 쿼리에 사용자 입력이 직접 삽입됨" }],
|
|
55
|
+
"score": 60
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 실패 케이스
|
|
61
|
+
**입력**: `{ "target": "" }`
|
|
62
|
+
**출력**: `{ "success": false, "error": "INVALID_INPUT", "message": "target is required" }`
|
|
63
|
+
|
|
64
|
+
## 실패 처리
|
|
65
|
+
| 에러 코드 | 원인 | 처리 방법 |
|
|
66
|
+
|-----------|------|-----------|
|
|
67
|
+
| `INVALID_INPUT` | target 비어 있음 | 파일 경로 또는 코드 입력 |
|
|
68
|
+
| `FILE_NOT_FOUND` | 파일 경로 존재하지 않음 | 경로 확인 |
|
|
69
|
+
| `MODEL_NOT_LOADED` | LLM 미로드 | `/model` 명령으로 모델 선택 |
|
|
70
|
+
| `SIZE_LIMIT` | max_lines 초과 파일 | max_lines 값 조정 또는 파일 분할 |
|
|
71
|
+
|
|
72
|
+
## 테스트 케이스
|
|
73
|
+
```python
|
|
74
|
+
# tests/unit/test_tools.py::test_code_review_snippet
|
|
75
|
+
# tests/integration/test_api.py::test_agent_code_review_file
|
|
76
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Skill: data_analysis
|
|
2
|
+
|
|
3
|
+
## 메타데이터
|
|
4
|
+
- **버전**: 0.1.0
|
|
5
|
+
- **카테고리**: data
|
|
6
|
+
- **위험도**: low
|
|
7
|
+
- **필요 권한**: local_read
|
|
8
|
+
|
|
9
|
+
## 설명
|
|
10
|
+
CSV/Excel/JSON 파일을 읽어 기초 통계, 컬럼 요약, 이상치 탐지를 수행하고 인사이트를 제공한다.
|
|
11
|
+
|
|
12
|
+
## 입력 스키마
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"required": ["path"],
|
|
16
|
+
"optional": ["columns", "analysis_type", "max_rows"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"path": { "type": "string", "description": "분석할 파일 절대 경로 (.csv, .xlsx, .json)" },
|
|
19
|
+
"columns": { "type": "array", "items": { "type": "string" }, "description": "분석할 컬럼 목록. 미입력시 전체" },
|
|
20
|
+
"analysis_type": { "type": "array", "items": { "type": "string", "enum": ["summary", "outlier", "correlation", "trend"] }, "default": ["summary"], "description": "수행할 분석 유형" },
|
|
21
|
+
"max_rows": { "type": "integer", "default": 10000, "description": "처리할 최대 행 수" }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 출력 스키마
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"success": true,
|
|
30
|
+
"result": {
|
|
31
|
+
"shape": [100, 5],
|
|
32
|
+
"columns": ["col1", "col2"],
|
|
33
|
+
"summary": { "col1": { "mean": 42.0, "std": 3.1, "min": 10, "max": 99 } },
|
|
34
|
+
"outliers": { "col1": [99, 10] },
|
|
35
|
+
"insights": "..."
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 실행 조건
|
|
41
|
+
- pandas, openpyxl 패키지 설치 필요 (requirements.txt 포함)
|
|
42
|
+
- 파일이 존재해야 하며 .csv/.xlsx/.json 형식이어야 함
|
|
43
|
+
|
|
44
|
+
## 예제
|
|
45
|
+
|
|
46
|
+
### 성공 케이스
|
|
47
|
+
**입력**: `{ "path": "~/data/sales.csv", "analysis_type": ["summary", "outlier"] }`
|
|
48
|
+
**출력**:
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"success": true,
|
|
52
|
+
"result": {
|
|
53
|
+
"shape": [500, 4],
|
|
54
|
+
"columns": ["date", "revenue", "units", "region"],
|
|
55
|
+
"summary": { "revenue": { "mean": 15000.0, "std": 4200.0, "min": 200, "max": 89000 } },
|
|
56
|
+
"outliers": { "revenue": [89000] },
|
|
57
|
+
"insights": "revenue 컬럼에서 1개의 이상치(89000) 발견. 평균 대비 17.6 표준편차."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 실패 케이스
|
|
63
|
+
**입력**: `{ "path": "~/data/photo.png" }`
|
|
64
|
+
**출력**: `{ "success": false, "error": "UNSUPPORTED_FORMAT", "message": "Supported formats: csv, xlsx, json" }`
|
|
65
|
+
|
|
66
|
+
## 실패 처리
|
|
67
|
+
| 에러 코드 | 원인 | 처리 방법 |
|
|
68
|
+
|-----------|------|-----------|
|
|
69
|
+
| `INVALID_INPUT` | path 누락 | 파일 경로 입력 |
|
|
70
|
+
| `FILE_NOT_FOUND` | 경로에 파일 없음 | 경로 확인 |
|
|
71
|
+
| `UNSUPPORTED_FORMAT` | csv/xlsx/json 이외 형식 | 지원 형식으로 변환 후 재시도 |
|
|
72
|
+
| `PARSE_ERROR` | 파일 파싱 실패 | 파일 인코딩/형식 확인 |
|
|
73
|
+
| `SIZE_LIMIT` | max_rows 초과 | max_rows 값 조정 |
|
|
74
|
+
|
|
75
|
+
## 테스트 케이스
|
|
76
|
+
```python
|
|
77
|
+
# tests/unit/test_tools.py::test_data_analysis_csv_summary
|
|
78
|
+
# tests/unit/test_tools.py::test_data_analysis_unsupported_format
|
|
79
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Skill: file_edit
|
|
2
|
+
|
|
3
|
+
## 메타데이터
|
|
4
|
+
- **버전**: 0.1.0
|
|
5
|
+
- **카테고리**: system
|
|
6
|
+
- **위험도**: medium
|
|
7
|
+
- **필요 권한**: local_write
|
|
8
|
+
|
|
9
|
+
## 설명
|
|
10
|
+
로컬 파일을 읽고 특정 범위를 편집한 뒤 저장한다. 원본 백업을 `.bak` 파일로 생성한다.
|
|
11
|
+
|
|
12
|
+
## 입력 스키마
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"required": ["path", "new_content"],
|
|
16
|
+
"optional": ["start_line", "end_line", "backup"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"path": { "type": "string", "description": "절대 경로 또는 ~/로 시작하는 경로" },
|
|
19
|
+
"new_content": { "type": "string", "description": "교체할 새 내용" },
|
|
20
|
+
"start_line": { "type": "integer", "description": "편집 시작 줄 번호 (1-indexed). 없으면 전체 교체" },
|
|
21
|
+
"end_line": { "type": "integer", "description": "편집 종료 줄 번호 (포함). 없으면 start_line 단일 줄" },
|
|
22
|
+
"backup": { "type": "boolean", "default": true, "description": "false면 .bak 파일 생성 생략" }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 출력 스키마
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"success": true,
|
|
31
|
+
"result": {
|
|
32
|
+
"path": "...",
|
|
33
|
+
"lines_changed": 3,
|
|
34
|
+
"backup_path": "....bak"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 실행 조건
|
|
40
|
+
- 대상 파일이 존재해야 함
|
|
41
|
+
- BINARY_EXTS(png, jpg, zip 등) 파일은 거부
|
|
42
|
+
- 파일 크기 10 MB 이하
|
|
43
|
+
|
|
44
|
+
## 예제
|
|
45
|
+
|
|
46
|
+
### 성공 케이스
|
|
47
|
+
**입력**: `{ "path": "~/project/config.py", "new_content": "DEBUG = False\n", "start_line": 5, "end_line": 5 }`
|
|
48
|
+
**출력**: `{ "success": true, "result": { "path": "...", "lines_changed": 1, "backup_path": "...config.py.bak" } }`
|
|
49
|
+
|
|
50
|
+
### 실패 케이스
|
|
51
|
+
**입력**: `{ "path": "~/photo.png", "new_content": "..." }`
|
|
52
|
+
**출력**: `{ "success": false, "error": "BINARY_FILE", "message": "Binary files cannot be edited as text" }`
|
|
53
|
+
|
|
54
|
+
## 실패 처리
|
|
55
|
+
| 에러 코드 | 원인 | 처리 방법 |
|
|
56
|
+
|-----------|------|-----------|
|
|
57
|
+
| `INVALID_INPUT` | path 또는 new_content 누락 | 필드 확인 후 재시도 |
|
|
58
|
+
| `FILE_NOT_FOUND` | 경로에 파일 없음 | 경로 확인 |
|
|
59
|
+
| `BINARY_FILE` | 바이너리 파일 편집 시도 | 텍스트 파일만 지원 |
|
|
60
|
+
| `PERMISSION_DENIED` | 파일 쓰기 권한 없음 | 관리자 권한 확인 |
|
|
61
|
+
| `SIZE_LIMIT` | 파일 10 MB 초과 | 파일 분할 후 재시도 |
|
|
62
|
+
|
|
63
|
+
## 테스트 케이스
|
|
64
|
+
```python
|
|
65
|
+
# tests/unit/test_tools.py::test_file_edit_full_replace
|
|
66
|
+
# tests/unit/test_tools.py::test_file_edit_line_range
|
|
67
|
+
# tests/unit/test_tools.py::test_file_edit_binary_rejected
|
|
68
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Skill: web_search
|
|
2
|
+
|
|
3
|
+
## 메타데이터
|
|
4
|
+
- **버전**: 0.1.0
|
|
5
|
+
- **카테고리**: web
|
|
6
|
+
- **위험도**: low
|
|
7
|
+
- **필요 권한**: none
|
|
8
|
+
|
|
9
|
+
## 설명
|
|
10
|
+
외부 검색 엔진(DuckDuckGo/Brave)을 통해 웹 검색을 수행하고 상위 결과를 반환한다.
|
|
11
|
+
|
|
12
|
+
## 입력 스키마
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"required": ["query"],
|
|
16
|
+
"optional": ["num_results", "lang"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"query": { "type": "string", "description": "검색어" },
|
|
19
|
+
"num_results": { "type": "integer", "default": 5, "description": "반환할 결과 수 (1-20)" },
|
|
20
|
+
"lang": { "type": "string", "default": "ko-KR", "description": "검색 언어 로케일" }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 출력 스키마
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"success": true,
|
|
29
|
+
"result": {
|
|
30
|
+
"query": "...",
|
|
31
|
+
"results": [
|
|
32
|
+
{ "title": "...", "url": "...", "snippet": "..." }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 실행 조건
|
|
39
|
+
- 네트워크 연결 필요
|
|
40
|
+
- 외부 API 키 불필요 (DuckDuckGo instant answer API 사용)
|
|
41
|
+
|
|
42
|
+
## 예제
|
|
43
|
+
|
|
44
|
+
### 성공 케이스
|
|
45
|
+
**입력**: `{ "query": "FastAPI 비동기 처리", "num_results": 3 }`
|
|
46
|
+
**출력**:
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"success": true,
|
|
50
|
+
"result": {
|
|
51
|
+
"query": "FastAPI 비동기 처리",
|
|
52
|
+
"results": [
|
|
53
|
+
{ "title": "FastAPI - Async", "url": "https://fastapi.tiangolo.com/async/", "snippet": "..." }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 실패 케이스
|
|
60
|
+
**입력**: `{ "query": "" }`
|
|
61
|
+
**출력**: `{ "success": false, "error": "INVALID_INPUT", "message": "query is required" }`
|
|
62
|
+
|
|
63
|
+
## 실패 처리
|
|
64
|
+
| 에러 코드 | 원인 | 처리 방법 |
|
|
65
|
+
|-----------|------|-----------|
|
|
66
|
+
| `INVALID_INPUT` | query 비어 있음 | 검색어 입력 후 재시도 |
|
|
67
|
+
| `NETWORK_ERROR` | 외부 API 연결 실패 | 잠시 후 재시도 |
|
|
68
|
+
| `TIMEOUT` | 5초 초과 | 검색어 단순화 후 재시도 |
|
|
69
|
+
|
|
70
|
+
## 테스트 케이스
|
|
71
|
+
```python
|
|
72
|
+
# tests/unit/test_tools.py::test_web_search_returns_results
|
|
73
|
+
# tests/integration/test_api.py::test_agent_web_search
|
|
74
|
+
```
|
package/static/account.html
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
<html lang="ko">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
6
|
<title>Lattice AI</title>
|
|
7
|
+
<link rel="manifest" href="/manifest.json">
|
|
8
|
+
<meta name="theme-color" content="#2d5a3d">
|
|
9
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
10
|
+
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
11
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
|
|
7
12
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
|
|
8
13
|
<style>
|
|
9
14
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
|
@@ -300,7 +305,12 @@
|
|
|
300
305
|
|
|
301
306
|
<script>
|
|
302
307
|
const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825' : '';
|
|
303
|
-
function apiFetch(path, opts = {}) {
|
|
308
|
+
function apiFetch(path, opts = {}) {
|
|
309
|
+
const headers = { ...(opts.headers || {}) };
|
|
310
|
+
const token = localStorage.getItem('ltcai_session_token') || '';
|
|
311
|
+
if (token && !headers.Authorization) headers.Authorization = `Bearer ${token}`;
|
|
312
|
+
return fetch(API_BASE + path, { credentials: 'include', ...opts, headers });
|
|
313
|
+
}
|
|
304
314
|
|
|
305
315
|
// ── i18n ──────────────────────────────────────────────
|
|
306
316
|
const I18N = {
|
|
@@ -431,6 +441,7 @@
|
|
|
431
441
|
localStorage.setItem('ltcai_user_email', data.email);
|
|
432
442
|
localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
|
|
433
443
|
localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
|
|
444
|
+
if (data.token) localStorage.setItem('ltcai_session_token', data.token);
|
|
434
445
|
window.location.href = '/chat';
|
|
435
446
|
} else {
|
|
436
447
|
const data = await res.json().catch(() => ({}));
|
|
@@ -473,6 +484,7 @@
|
|
|
473
484
|
localStorage.setItem('ltcai_user_email', data.email);
|
|
474
485
|
localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
|
|
475
486
|
localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
|
|
487
|
+
if (data.token) localStorage.setItem('ltcai_session_token', data.token);
|
|
476
488
|
window.location.href = '/chat';
|
|
477
489
|
}
|
|
478
490
|
});
|