|
@@ -0,0 +1,376 @@
|
|
|
|
|
+# TG 协议号导入设计
|
|
|
|
|
+
|
|
|
|
|
+**日期:** 2026-04-20
|
|
|
|
|
+**作者:** Claude (opus-4-7[1m]) + 用户
|
|
|
|
|
+**状态:** 已批准,待实现
|
|
|
|
|
+
|
|
|
|
|
+## 背景与目标
|
|
|
|
|
+
|
|
|
|
|
+`D:\spider\tgs\` 目录下有 5 个中文 TG 圈常见的"协议号"文件夹,每个以手机号命名,内容为:
|
|
|
|
|
+
|
|
|
|
|
+- `<phone>.json` — 包含 `api_id` / `api_hash` / `phone` / 设备指纹 / `twoFA` 密码
|
|
|
|
|
+- `<phone>.session` — **Pyrogram/Telethon SQLite 格式**(28672 字节),存 `dc_id` + 256 字节 `auth_key`
|
|
|
|
|
+- `2fa_password.txt` — 2FA 明文
|
|
|
|
|
+- `tdata/` — Telegram Desktop 客户端数据目录(备用)
|
|
|
|
|
+
|
|
|
|
|
+当前 `tg_accounts` 管理后台只支持手填 `session_file` 路径 + `app_id` + `app_hash`,而且 `gotd/td` 的 `session.FileStorage` 只认 **gotd 自有 JSON 格式**,与协议号的 Pyrogram SQLite **不兼容**。本设计要把协议号接入管理后台,让群克隆、采集等现有 TG 功能能直接用这些账号。
|
|
|
|
|
+
|
|
|
|
|
+## 核心设计决策(用户已确认)
|
|
|
|
|
+
|
|
|
|
|
+| 决策点 | 选择 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 录入入口 | 后台表单"上传整个协议号文件夹" |
|
|
|
|
|
+| 上传方式 | HTML5 `<input webkitdirectory>` 选目录,前端拆成多文件 multipart |
|
|
|
|
|
+| Session 源 | 优先 `.session`(Pyrogram SQLite),失败则 fallback `tdata/` |
|
|
|
|
|
+| 2FA 处理 | 只存不用:AES-GCM 加密后入库,供未来重登/导出 |
|
|
|
|
|
+| 设备指纹 | 必须存并在 `Connect` 时带上,防 TG 风控踢号 |
|
|
|
|
|
+| 重复手机号 | 拒绝(409),不覆盖 |
|
|
|
|
|
+| 导入后行为 | 自动测试连接,失败标 `enabled=false` 不回滚 |
|
|
|
|
|
+
|
|
|
|
|
+## 架构概览
|
|
|
|
|
+
|
|
|
|
|
+**核心思路:一次性转换 + gotd 原生运行时。**
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+前端 TgAccounts 页面
|
|
|
|
|
+ │
|
|
|
|
|
+ ├─ "导入协议号"按钮
|
|
|
|
|
+ │ 用 <input webkitdirectory> 让用户选 D:\spider\tgs\<phone>\ 目录
|
|
|
|
|
+ │ 前端把目录下所有文件(*.json / *.session / 2fa_password.txt / tdata/**)
|
|
|
|
|
+ │ 逐个 append 到 FormData 上传
|
|
|
|
|
+ │
|
|
|
|
|
+ ▼
|
|
|
|
|
+POST /tg-accounts/import (multipart/form-data)
|
|
|
|
|
+ │
|
|
|
|
|
+ ├─ Step 1. 收件:解析 multipart,按文件名/扩展名分类到临时目录 tmp/<uuid>/
|
|
|
|
|
+ ├─ Step 2. 读 <phone>.json → phone, api_id, api_hash, twoFA, device fingerprint
|
|
|
|
|
+ ├─ Step 3. 重复校验:phone 在 tg_accounts 表 → 409
|
|
|
|
|
+ ├─ Step 4. 转换 session:
|
|
|
|
|
+ │ 4a. 优先尝试 Pyrogram SQLite(dc_id + auth_key → gotd session.Data → JSON)
|
|
|
|
|
+ │ 4b. 失败则 fallback 尝试 tdata(用 gotd/td/session/tdesktop 包)
|
|
|
|
|
+ │ 4c. 两者都失败 → 返回 422,原始文件保留在 tmp/ 方便排查
|
|
|
|
|
+ ├─ Step 5. 物化:
|
|
|
|
|
+ │ sessions/<phone>.json ← 转换后的 gotd session
|
|
|
|
|
+ │ sessions/<phone>.origin/ ← 原始 .session + tdata/ 备份
|
|
|
|
|
+ ├─ Step 6. 入库:TgAccount 记录(加密 2FA + 设备字段)
|
|
|
|
|
+ ├─ Step 7. 自动测试:用新记录构造 Client.Connect + GetMe
|
|
|
|
|
+ │ 失败 → enabled=false, status="dead", 返回告警但不回滚(账号还在库里)
|
|
|
|
|
+ └─ Step 8. reloadAccounts() → AccountManager 更新
|
|
|
|
|
+ ▼
|
|
|
|
|
+返回 { account, test_result }
|
|
|
|
|
+
|
|
|
|
|
+运行时:
|
|
|
|
|
+ AccountManager.Acquire → ManagedAccount.Client.Connect
|
|
|
|
|
+ Connect 读 sessions/<phone>.json(gotd 原生 FileStorage)
|
|
|
|
|
+ InitConnection 带上 DB 里的 device / app_version / sdk / lang_pack / lang_code
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**关键不变量:**
|
|
|
|
|
+- 运行时 `telegram.Client` 不感知 Pyrogram/tdata —— 只认 gotd session JSON
|
|
|
|
|
+- 所有元数据(设备指纹、加密 2FA)都在 `tg_accounts` 表,不依赖硬盘上的原始 `.json`
|
|
|
|
|
+- `sessions/<phone>.origin/` 仅用于人工排障和未来迁移,运行时不读
|
|
|
|
|
+
|
|
|
|
|
+## §1 数据模型变更
|
|
|
|
|
+
|
|
|
|
|
+**`tg_accounts` 表新增字段**(通过 GORM auto-migrate 加入):
|
|
|
|
|
+
|
|
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| `two_fa_enc` | `varbinary(255)` NULL | AES-GCM 加密后的 2FA 密码;`nil` 表示该账号无 2FA 或未上传 |
|
|
|
|
|
+| `device` | `varchar(100)` NULL | 如 "Xiaomi Mix 4" |
|
|
|
|
|
+| `app_version` | `varchar(50)` NULL | "12.0.0 (6163)" |
|
|
|
|
|
+| `sdk` | `varchar(100)` NULL | "Android 13 (33)"(映射到 gotd 的 `SystemVersion`) |
|
|
|
|
|
+| `lang_pack` | `varchar(50)` NULL | "android" |
|
|
|
|
|
+| `lang_code` | `varchar(20)` NULL | "en" |
|
|
|
|
|
+| `system_lang_code` | `varchar(20)` NULL | "en-US" |
|
|
|
|
|
+| `first_name` | `varchar(100)` NULL | UI 展示用 |
|
|
|
|
|
+| `last_name` | `varchar(100)` NULL | UI 展示用 |
|
|
|
|
|
+| `username` | `varchar(100)` NULL | UI 展示用 |
|
|
|
|
|
+| `origin_dir` | `varchar(500)` NULL | `sessions/<phone>.origin/` 绝对路径,排障用 |
|
|
|
|
|
+| `import_status` | `varchar(20)` NULL | `ok` / `session_invalid` / `dead`;nullable 以兼容旧记录 |
|
|
|
|
|
+| `source` | `varchar(20)` NULL | `protocol` / `manual`,区分两种录入方式;nullable 同上 |
|
|
|
|
|
+
|
|
|
|
|
+**不改:** `session_file` 保持语义(指向 `sessions/<phone>.json`),`app_id`/`app_hash` 继续复用。
|
|
|
|
|
+
|
|
|
|
|
+**对老记录:** 新字段全部 nullable,老记录仍可用;设备指纹走 gotd 默认值。
|
|
|
|
|
+
|
|
|
|
|
+## §2 文件布局与 Docker 卷
|
|
|
|
|
+
|
|
|
|
|
+容器内路径:
|
|
|
|
|
+```
|
|
|
|
|
+/app/sessions/ ← 宿主机挂载卷
|
|
|
|
|
+├── 13252753163.json ← gotd 原生格式(运行时读这个)
|
|
|
|
|
+├── 13252753163.origin/ ← 原始备份
|
|
|
|
|
+│ ├── 13252753163.json ← Pyrogram 元数据
|
|
|
|
|
+│ ├── 13252753163.session ← Pyrogram SQLite
|
|
|
|
|
+│ ├── 2fa_password.txt
|
|
|
|
|
+│ └── tdata/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**`docker-compose.yml` 改动**:确保 `sessions/` 目录是宿主机挂载卷;已存在则不动,没有就追加 `./data/sessions:/app/sessions`。
|
|
|
|
|
+
|
|
|
|
|
+**Config 新增**(`configs/config.yaml`):
|
|
|
|
|
+```yaml
|
|
|
|
|
+telegram:
|
|
|
|
|
+ sessions_dir: "/app/sessions"
|
|
|
|
|
+ secret_key: "${TG_SECRET_KEY}" # 32 字节 base64,AES-GCM 主密钥
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+启动时校验 `TG_SECRET_KEY` 存在且解码后为 32 字节,否则 fail-fast。
|
|
|
|
|
+
|
|
|
|
|
+## §3 Pyrogram SQLite → gotd Session 转换
|
|
|
|
|
+
|
|
|
|
|
+**新文件:** `internal/telegram/sessionimport/pyrogram.go`
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package sessionimport
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "crypto/sha1"
|
|
|
|
|
+ "database/sql"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ _ "modernc.org/sqlite"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/gotd/td/session"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// Pyrogram SQLite schema (sessions 表只有一行):
|
|
|
|
|
+// CREATE TABLE sessions (
|
|
|
|
|
+// dc_id INTEGER PRIMARY KEY,
|
|
|
|
|
+// api_id INTEGER, test_mode INTEGER,
|
|
|
|
|
+// auth_key BLOB, date INTEGER NOT NULL,
|
|
|
|
|
+// user_id INTEGER, is_bot INTEGER);
|
|
|
|
|
+func ConvertPyrogramSession(sqlitePath string) (*session.Data, error) {
|
|
|
|
|
+ db, err := sql.Open("sqlite", sqlitePath)
|
|
|
|
|
+ if err != nil { return nil, err }
|
|
|
|
|
+ defer db.Close()
|
|
|
|
|
+
|
|
|
|
|
+ var dcID int
|
|
|
|
|
+ var authKey []byte
|
|
|
|
|
+ err = db.QueryRow("SELECT dc_id, auth_key FROM sessions LIMIT 1").
|
|
|
|
|
+ Scan(&dcID, &authKey)
|
|
|
|
|
+ if err != nil { return nil, fmt.Errorf("read pyrogram sessions row: %w", err) }
|
|
|
|
|
+ if len(authKey) != 256 {
|
|
|
|
|
+ return nil, fmt.Errorf("invalid auth_key length: %d (expect 256)", len(authKey))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ addr, err := tgDCAddr(dcID)
|
|
|
|
|
+ if err != nil { return nil, err }
|
|
|
|
|
+
|
|
|
|
|
+ sum := sha1.Sum(authKey)
|
|
|
|
|
+ keyID := sum[12:20]
|
|
|
|
|
+
|
|
|
|
|
+ return &session.Data{
|
|
|
|
|
+ DC: dcID,
|
|
|
|
|
+ Addr: addr,
|
|
|
|
|
+ AuthKey: authKey,
|
|
|
|
|
+ AuthKeyID: keyID,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// tgDCAddr maps dc_id → production IP:port.
|
|
|
|
|
+// Source: https://core.telegram.org/api/datacenter
|
|
|
|
|
+func tgDCAddr(dc int) (string, error) {
|
|
|
|
|
+ switch dc {
|
|
|
|
|
+ case 1: return "149.154.175.53:443", nil
|
|
|
|
|
+ case 2: return "149.154.167.51:443", nil
|
|
|
|
|
+ case 3: return "149.154.175.100:443", nil
|
|
|
|
|
+ case 4: return "149.154.167.91:443", nil
|
|
|
|
|
+ case 5: return "91.108.56.130:443", nil
|
|
|
|
|
+ }
|
|
|
|
|
+ return "", fmt.Errorf("unknown dc_id: %d", dc)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**落盘:**
|
|
|
|
|
+```go
|
|
|
|
|
+storage := &session.FileStorage{Path: targetPath}
|
|
|
|
|
+// session.FileStorage.StoreSession 接受的是 session.Data 序列化后的 bytes;
|
|
|
|
|
+// 用 session.Loader{Storage: storage}.Save(ctx, data) 更稳:
|
|
|
|
|
+loader := session.Loader{Storage: storage}
|
|
|
|
|
+if err := loader.Save(ctx, data); err != nil { return err }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**tdata fallback:** 新文件 `internal/telegram/sessionimport/tdesktop.go`
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+import "github.com/gotd/td/session/tdesktop"
|
|
|
|
|
+
|
|
|
|
|
+func ConvertTdataSession(tdataDir, localKey string) (*session.Data, error) {
|
|
|
|
|
+ // localKey:若 tdata 有密码保护,用协议号的 2fa_password.txt 内容尝试
|
|
|
|
|
+ // 若无密码,localKey 传 ""
|
|
|
|
|
+ acc, err := tdesktop.Read(tdataDir, []byte(localKey))
|
|
|
|
|
+ if err != nil { return nil, err }
|
|
|
|
|
+ return session.TDesktopSession(acc)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**依赖新增**(`go.mod`):`modernc.org/sqlite`(纯 Go SQLite 驱动,无 CGO,Docker build 不受影响)。
|
|
|
|
|
+
|
|
|
|
|
+## §4 2FA 加密存储
|
|
|
|
|
+
|
|
|
|
|
+**新文件:** `internal/telegram/crypto.go`
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package telegram
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "crypto/aes"
|
|
|
|
|
+ "crypto/cipher"
|
|
|
|
|
+ "crypto/rand"
|
|
|
|
|
+ "encoding/base64"
|
|
|
|
|
+ "errors"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type Crypto struct { key []byte } // 32 字节
|
|
|
|
|
+
|
|
|
|
|
+func NewCrypto(b64Key string) (*Crypto, error) {
|
|
|
|
|
+ key, err := base64.StdEncoding.DecodeString(b64Key)
|
|
|
|
|
+ if err != nil { return nil, fmt.Errorf("decode secret key: %w", err) }
|
|
|
|
|
+ if len(key) != 32 {
|
|
|
|
|
+ return nil, fmt.Errorf("secret key must be 32 bytes, got %d", len(key))
|
|
|
|
|
+ }
|
|
|
|
|
+ return &Crypto{key: key}, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (c *Crypto) Encrypt(plain string) ([]byte, error) {
|
|
|
|
|
+ block, _ := aes.NewCipher(c.key)
|
|
|
|
|
+ gcm, _ := cipher.NewGCM(block)
|
|
|
|
|
+ nonce := make([]byte, gcm.NonceSize())
|
|
|
|
|
+ if _, err := rand.Read(nonce); err != nil { return nil, err }
|
|
|
|
|
+ ct := gcm.Seal(nonce, nonce, []byte(plain), nil) // [nonce || ciphertext || tag]
|
|
|
|
|
+ return ct, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (c *Crypto) Decrypt(blob []byte) (string, error) {
|
|
|
|
|
+ block, _ := aes.NewCipher(c.key)
|
|
|
|
|
+ gcm, _ := cipher.NewGCM(block)
|
|
|
|
|
+ ns := gcm.NonceSize()
|
|
|
|
|
+ if len(blob) < ns { return "", errors.New("ciphertext too short") }
|
|
|
|
|
+ nonce, ct := blob[:ns], blob[ns:]
|
|
|
|
|
+ pt, err := gcm.Open(nil, nonce, ct, nil)
|
|
|
|
|
+ if err != nil { return "", err }
|
|
|
|
|
+ return string(pt), nil
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**集成:**
|
|
|
|
|
+- `main.go` 启动时:从 `config.Telegram.SecretKey` 构造全局 `*telegram.Crypto`,注入到 `TgAccountHandler`
|
|
|
|
|
+- 导入时:`twoFA` 非空则 `enc := crypto.Encrypt(twoFA)`,存 `two_fa_enc`
|
|
|
|
|
+- **新接口** `POST /tg-accounts/:id/reveal-2fa`(admin-only):返回明文 2FA(用于重登流程,极少用,审计日志记录)
|
|
|
|
|
+
|
|
|
|
|
+## §5 Client 集成与设备指纹
|
|
|
|
|
+
|
|
|
|
|
+**`internal/telegram/types.go` 的 `Account` 扩展**:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+type Account struct {
|
|
|
|
|
+ Phone string
|
|
|
|
|
+ SessionFile string
|
|
|
|
|
+ AppID int
|
|
|
|
|
+ AppHash string
|
|
|
|
|
+ Device string // NEW: "Xiaomi Mix 4"
|
|
|
|
|
+ AppVersion string // NEW: "12.0.0 (6163)"
|
|
|
|
|
+ SystemVersion string // NEW: "Android 13 (33)"(来自 DB.sdk 字段)
|
|
|
|
|
+ LangPack string // NEW: "android"
|
|
|
|
|
+ LangCode string // NEW: "en"
|
|
|
|
|
+ SystemLangCode string // NEW: "en-US"
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**`internal/telegram/client.go` 的 `Connect` 改造**:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+opts := telegram.Options{
|
|
|
|
|
+ SessionStorage: storage,
|
|
|
|
|
+ NoUpdates: true,
|
|
|
|
|
+ Device: telegram.DeviceConfig{
|
|
|
|
|
+ DeviceModel: c.account.Device,
|
|
|
|
|
+ AppVersion: c.account.AppVersion,
|
|
|
|
|
+ SystemVersion: c.account.SystemVersion,
|
|
|
|
|
+ LangPack: c.account.LangPack,
|
|
|
|
|
+ SystemLangCode: c.account.SystemLangCode,
|
|
|
|
|
+ LangCode: c.account.LangCode,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**字段为空时**:gotd 使用默认设备画像(老账号保持现状,不受影响)。
|
|
|
|
|
+
|
|
|
|
|
+**`reloadAccounts`**(在 `tg_account.go` handler 中):从 DB 读这些新字段填到 `telegram.Account`。
|
|
|
|
|
+
|
|
|
|
|
+## §6 前端改造与 API
|
|
|
|
|
+
|
|
|
|
|
+### 前端 `web/src/pages/TgAccounts.tsx`
|
|
|
|
|
+
|
|
|
|
|
+- 顶部两个按钮:「**导入协议号**」(主)+「手填添加」(次)
|
|
|
|
|
+- 点「导入协议号」弹 Modal:
|
|
|
|
|
+ - 单个 `<input type="file" webkitdirectory directory multiple ref={...} />`
|
|
|
|
|
+ - 提示文字:「选择一个协议号文件夹,如 `D:\spider\tgs\13252753163\`」
|
|
|
|
|
+ - 提交时:`FileList` 全部 append 到 `FormData`,字段名统一用 `files`
|
|
|
|
|
+- 上传中 spinner;响应含 `test_result.status` 时根据值显示 toast:`ok` 绿色、`cooling` 黄色、`fail` 红色(仍刷新列表,因为记录已入库)
|
|
|
|
|
+
|
|
|
|
|
+**列表新列:**
|
|
|
|
|
+- **来源** — `source` 字段:`protocol`(紫色标签)/ `manual`(灰色标签)
|
|
|
|
|
+- **设备** — `device` 字段,截断 + tooltip 完整显示
|
|
|
|
|
+- **导入状态** — `import_status`:`ok` 绿 / `session_invalid` 红 / `dead` 橙
|
|
|
|
|
+
|
|
|
|
|
+### 后端新接口
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+POST /tg-accounts/import
|
|
|
|
|
+ Auth: admin only (GuardedRoute 校验)
|
|
|
|
|
+ Content-Type: multipart/form-data
|
|
|
|
|
+ Body:
|
|
|
|
|
+ files[]: 上传目录下所有文件(浏览器会把 .session / .json / tdata/** 拍平在 FormData 里)
|
|
|
|
|
+
|
|
|
|
|
+ Response 200:
|
|
|
|
|
+ { account: TgAccount, test_result: { status: "ok"|"cooling"|"fail", message: string } }
|
|
|
|
|
+
|
|
|
|
|
+ Response 400: 协议号文件夹结构不完整(缺 .json 或 .session + tdata 都缺)
|
|
|
|
|
+ Response 409: 手机号已存在
|
|
|
|
|
+ Response 422: session 解析失败,body 含 { pyrogram_err, tdata_err, tmp_dir }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**测试复用**:现有 `POST /tg-accounts/:id/test` 保留;导入流程内部也调同一个方法。
|
|
|
|
|
+
|
|
|
|
|
+## §7 错误处理与边界情况
|
|
|
|
|
+
|
|
|
|
|
+| 场景 | 处理 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 测试阶段 FloodWait | 导入成功,`status="cooling"`,`enabled=true`(FloodWait 是临时的) |
|
|
|
|
|
+| Pyrogram SQLite 损坏 | 日志 warn,尝试 tdata;都失败 → 422,临时目录 `tmp/<uuid>/` 保留 30 分钟 |
|
|
|
|
|
+| `<phone>.json` 缺 `twoFA` | `two_fa_enc` 存 NULL,不报错 |
|
|
|
|
|
+| tdata 加密且 2fa 不匹配 | 降级为"仅 Pyrogram 可用";若 Pyrogram 也烂,整体 422 |
|
|
|
|
|
+| 并发导入同一手机号 | DB 唯一索引兜底;第二个请求 duplicate key → 转 409 |
|
|
|
|
|
+| `auth_key` 长度 ≠ 256 | 422,明确提示"协议号文件可能被篡改或损坏" |
|
|
|
|
|
+| 未知 `dc_id` | 422,提示"不支持的 DC,dc_id=X" |
|
|
|
|
|
+| `/app/sessions/` 不可写 | 500,提示运维检查挂载卷权限 |
|
|
|
|
|
+| FormData 无 `.session` 也无 `tdata/` | 400,"协议号目录结构不完整" |
|
|
|
|
|
+
|
|
|
|
|
+**临时目录清理**:后台 goroutine 每 10 分钟扫一次 `tmp/`,删除 mtime > 30 分钟的子目录。
|
|
|
|
|
+
|
|
|
|
|
+**审计日志**:`import`、`reveal-2fa`、`delete` 都调 `LogAudit`。
|
|
|
|
|
+
|
|
|
|
|
+## 实施顺序建议
|
|
|
|
|
+
|
|
|
|
|
+1. **`internal/telegram/crypto.go`** — AES-GCM 封装 + 单测
|
|
|
|
|
+2. **`internal/telegram/sessionimport/pyrogram.go`** — Pyrogram SQLite 解析 + 单测(用 tgs/ 下真实文件)
|
|
|
|
|
+3. **`internal/telegram/sessionimport/tdesktop.go`** — tdata fallback
|
|
|
|
|
+4. **`internal/model/tg_account.go`**(文件不存在,在 `user.go` 里扩展 TgAccount) — 新字段 + auto-migrate
|
|
|
|
|
+5. **`internal/telegram/types.go`** — `Account` 结构体扩展
|
|
|
|
|
+6. **`internal/telegram/client.go`** — `Connect` 传 Device
|
|
|
|
|
+7. **`internal/handler/tg_account.go`** — 新 `Import` handler + `RevealTwoFA` + `reloadAccounts` 同步新字段
|
|
|
|
|
+8. **`configs/config.yaml` + `internal/config/config.go`** — `telegram.sessions_dir` / `secret_key`
|
|
|
|
|
+9. **`deploy/docker-compose.yml`** — 确认 sessions 卷挂载;`.env` 示例加 `TG_SECRET_KEY`
|
|
|
|
|
+10. **`web/src/pages/TgAccounts.tsx`** — UI 改造
|
|
|
|
|
+11. **端到端测试**:用 `D:\spider\tgs\13252753163\` 跑一遍完整流程
|
|
|
|
|
+
|
|
|
|
|
+## 非目标(明确不做)
|
|
|
|
|
+
|
|
|
|
|
+- 协议号**自动扫描** `tgs/` 目录导入 — 本期手动选目录上传
|
|
|
|
|
+- 重登流程(session 失效后用 2FA 自动重登) — 本期只存 2FA,不实现重登
|
|
|
|
|
+- Pyrogram session 格式**写回** — 运行时只读原始文件,gotd 写 session 只写 `sessions/<phone>.json`
|
|
|
|
|
+- tdata **反向导出** — 只导入不导出
|
|
|
|
|
+- 批量导入(一次选多个协议号目录) — 本期一次一个
|