Compare commits
2
Commits
6b77cdcf3a
...
e89578fa82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e89578fa82 | ||
|
|
65cfaf97a1 |
No files matched your search
@@ -18,25 +18,24 @@
|
|||||||
│ waf │ │ gateway │ │ static │ │ dnfs │ │ db │ │ ai_review │
|
│ waf │ │ gateway │ │ static │ │ dnfs │ │ db │ │ ai_review │
|
||||||
│ WAF 网关 │ │ 应用层网关 │ │前端托管│ │ 树状菜单 │ │ 通用数据库 │ │ AI内容审核 │
|
│ WAF 网关 │ │ 应用层网关 │ │前端托管│ │ 树状菜单 │ │ 通用数据库 │ │ AI内容审核 │
|
||||||
│ :443 (入口) │ │ :8443 (内部) │ │ :9200 │ │ :8006 │ │ :8005 │ │ :8007 │
|
│ :443 (入口) │ │ :8443 (内部) │ │ :9200 │ │ :8006 │ │ :8005 │ │ :8007 │
|
||||||
│ coraza+反代 │ │ 鉴权 + 反代 │ │静态/媒体│ │ 节点CRUD │ │ SQL+日志收集 │ │ DeepSeek+RAG │
|
│ coraza+反代 │ │ 鉴权 + 反代 │ │静态/媒体│ │ 节点CRUD │ │ pymsg/TCP │ │ DeepSeek+RAG │
|
||||||
└──────┬───────┘ └──────┬───────┘ └────────┘ └──────┬───────┘ └──────────────┘ └──────────────┘
|
└──────┬───────┘ └──────┬───────┘ └───┬────┘ └──────┬───────┘ └──────▲───────┘ └──────▲───────┘
|
||||||
│ HTTPS终止 │ 反向代理 │ 调用
|
│ HTTPS终止 │ 反向代理 │ 代理笔记请求 │ pymsg │ pymsg │ HTTP
|
||||||
└──────────────────►│ ├──────────► db:8005
|
└──────────────────►│ ├─────────────►│─────────────────┘ │
|
||||||
├────────────────► static:9200
|
├────────────►│ └─────── HTTP /review ──────────────┘
|
||||||
└────────────────► dnfs:8006 ──► ai_review:8007
|
└────────────►│
|
||||||
|
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox ──(log_client, pymsg)──► db:8005 (Logs 表)
|
||||||
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox ──(log_client)──► db:8005/log/ingest
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 请求链路
|
### 请求链路
|
||||||
|
|
||||||
1. 客户端所有请求先打到 `waf:443`(HTTPS),WAF 终止 TLS、运行 coraza 规则匹配,命中则直接返回 403。
|
1. 客户端所有请求先打到 `waf:443`(HTTPS),WAF 终止 TLS、运行 coraza 规则匹配,命中则直接返回 403。
|
||||||
2. 未命中规则的请求由 WAF 以明文 HTTP 转发到 `gateway:8443`,并设置 `X-Forwarded-For` / `X-Forwarded-Proto` 头。
|
2. 未命中规则的请求由 WAF 以明文 HTTP 转发到 `gateway:8443`,并设置 `X-Forwarded-For` / `X-Forwarded-Proto` 头。
|
||||||
3. `gateway` 对 `/login`、`/register` 做账号鉴权(SQLite + PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制)。
|
3. `gateway` 对 `/login`、`/register` 做账号鉴权(PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制),用户数据通过 pymsg/TCP 调用 `db:8005` 存取。
|
||||||
4. 其余请求携带 Token,经 `gateway` 校验后反向代理到 `Proxy.opened` 中放开的内部服务(`static:9200` / `dnfs:8006`)。
|
4. 其余请求携带 Token,经 `gateway` 校验后反向代理到 `Proxy.opened` 中放开的内部服务(`static:9200` / `dnfs:8006`)。
|
||||||
5. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部服务间调用,不对外暴露。
|
5. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部服务间调用,不对外暴露。
|
||||||
6. `dnfs` 处理节点增删改查时,通过 HTTP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
|
6. `dnfs` 处理节点增删改查时,通过 pymsg/TCP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
|
||||||
7. 各服务通过 `log_client.py` SDK 异步批量推送运行日志到 `db:8005/log/ingest`,实现集中式日志收集(fail-safe,db 不可用时静默丢弃)。
|
7. 各服务通过 `log_client.py` SDK 异步批量推送运行日志(pymsg/TCP 写入 db 的 `Logs` 表),实现集中式日志收集(fail-safe,db 不可用时静默丢弃)。
|
||||||
|
|
||||||
## 各服务说明
|
## 各服务说明
|
||||||
|
|
||||||
@@ -47,15 +46,15 @@
|
|||||||
| [gateway](gateway/) | C++20 | 8443 | 应用层网关(内部 HTTP),用户注册/登录鉴权、Token 签发与校验、反向代理到内部服务 |
|
| [gateway](gateway/) | C++20 | 8443 | 应用层网关(内部 HTTP),用户注册/登录鉴权、Token 签发与校验、反向代理到内部服务 |
|
||||||
| [static](static/) | Python(Flask) | 9200 | 前端托管服务,社区首页、静态页面与文件浏览、视频/音频流媒体播放、路径遍历防护 |
|
| [static](static/) | Python(Flask) | 9200 | 前端托管服务,社区首页、静态页面与文件浏览、视频/音频流媒体播放、路径遍历防护 |
|
||||||
| [dnfs](dnfs/) | Python(Flask) | 8006 | 树状网络文件系统(Dendrachy Network File-System),基于父子 pid 的节点树增删改查,集成 AI 内容审核 |
|
| [dnfs](dnfs/) | Python(Flask) | 8006 | 树状网络文件系统(Dendrachy Network File-System),基于父子 pid 的节点树增删改查,集成 AI 内容审核 |
|
||||||
| [db](db/) | Python(Flask) | 8005 | 通用数据库服务,基于 SQLAlchemy Core 的 SQL-over-HTTP,支持 select/insert/update/delete,参数化查询防注入;内置集中式日志收集(/log/* 路由) |
|
| [db](db/) | Python(pymsg) | 8005 | 通用数据库服务,基于 pymsg 协议(TCP 长度前缀帧 + JSON)的 SQL-over-TCP,SQLAlchemy Core 参数化查询防注入;内置集中式日志收集(Logs 表 + 自动清理) |
|
||||||
| [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 |
|
| [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 |
|
||||||
| [captcha](captcha/) | Python(Flask) | 8009 | 验证码服务,生成图形验证码(5 分钟过期、一次性),供 gateway 登录/注册可选启用 CAPTCHA 校验 |
|
| [captcha](captcha/) | Python(stdlib+PIL) | 8009 | 验证码服务,生成图形验证码(5 分钟过期、一次性),供 gateway 登录/注册可选启用 CAPTCHA 校验 |
|
||||||
| [smtp](smtp/) | Python(Flask) | 8008 | 邮件服务,发送/校验邮箱验证码(/send_email、/verify_email),注册流程的邮件验证码后端,SMTP 账号配置在 .env |
|
| [smtp](smtp/) | Python(Flask) | 8008 | 邮件服务,发送/校验邮箱验证码(/send_email、/verify_email),注册流程的邮件验证码后端,SMTP 账号配置在 .env |
|
||||||
| [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 |
|
| [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 |
|
||||||
|
|
||||||
### core(核心模块 · 进程托管服务)
|
### core(核心模块 · 进程托管服务)
|
||||||
|
|
||||||
- 可执行文件 `nidus.out`,作为 systemd 服务(`Nidus.service`)常驻运行。
|
- 可执行文件 `nidus.out`,作为 systemd 服务(`Nidus.service`)常驻运行(崩溃守护由 systemd 负责,core 自身不含自动重启逻辑)。
|
||||||
- 支持两种工作模式:命令行子系统调用(`--send/--exec/--list/--kill`)与 HTTP 伺服器模式(`--serve <port>`)。
|
- 支持两种工作模式:命令行子系统调用(`--send/--exec/--list/--kill`)与 HTTP 伺服器模式(`--serve <port>`)。
|
||||||
- 通过命名管道(Pipe)与消息队列(Mq)实现进程间通信。
|
- 通过命名管道(Pipe)与消息队列(Mq)实现进程间通信。
|
||||||
- 读取 `init.txt` 自启动列表,按行拉起各业务服务(ai_review / db / dnfs / smtp / static / waf / gateway / captcha / aibox)。
|
- 读取 `init.txt` 自启动列表,按行拉起各业务服务(ai_review / db / dnfs / smtp / static / waf / gateway / captcha / aibox)。
|
||||||
@@ -74,7 +73,8 @@
|
|||||||
- 内部 HTTP 服务,默认监听 127.0.0.1:8443,不再承载 TLS(TLS 已由 WAF 终止)。
|
- 内部 HTTP 服务,默认监听 127.0.0.1:8443,不再承载 TLS(TLS 已由 WAF 终止)。
|
||||||
- 通过 `Server.tls = on/off` 配置切换 HTTPS/HTTP 模式([example.ini](gateway/example.ini)),独立部署时仍可启用 TLS。
|
- 通过 `Server.tls = on/off` 配置切换 HTTPS/HTTP 模式([example.ini](gateway/example.ini)),独立部署时仍可启用 TLS。
|
||||||
- **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),登录时自动迁移旧 SHA256 哈希,常数时间比较防 timing attack。
|
- **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),登录时自动迁移旧 SHA256 哈希,常数时间比较防 timing attack。
|
||||||
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行。
|
- **用户存储**:用户数据通过 pymsg/TCP 调用 `db:8005`(`DbC::call_msg`)存取,不再直连本地 SQLite。
|
||||||
|
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行;RSA 密钥对启动时内存生成,重启后旧 Token 全部失效。
|
||||||
- **CAPTCHA**:`/captcha` 接口生成验证码(5 分钟过期、一次性使用),登录与注册均可选启用。
|
- **CAPTCHA**:`/captcha` 接口生成验证码(5 分钟过期、一次性使用),登录与注册均可选启用。
|
||||||
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟,按 `X-Forwarded-For`(WAF 注入)客户端 IP 计数。
|
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟,按 `X-Forwarded-For`(WAF 注入)客户端 IP 计数。
|
||||||
- **反向代理**:`Target-Url` 必须在 `Proxy.opened` 白名单中(`closed` 仅记录用途,实际隔离靠"不在 opened 中即拒绝"),缺省走 `Proxy.default`(不需 Token,服务前端静态资源)。
|
- **反向代理**:`Target-Url` 必须在 `Proxy.opened` 白名单中(`closed` 仅记录用途,实际隔离靠"不在 opened 中即拒绝"),缺省走 `Proxy.default`(不需 Token,服务前端静态资源)。
|
||||||
@@ -84,48 +84,50 @@
|
|||||||
### static(前端托管服务)
|
### static(前端托管服务)
|
||||||
|
|
||||||
- 托管 `public/` 下的前端页面:社区首页(`index.html`)、登录/注册、树状笔记(`dnfs.html`)、文件浏览(`__view.html`)等。
|
- 托管 `public/` 下的前端页面:社区首页(`index.html`)、登录/注册、树状笔记(`dnfs.html`)、文件浏览(`__view.html`)等。
|
||||||
- 支持文件浏览、视频/音频在线播放(HTTP Range 分段请求)、一键下载。
|
- 支持文件浏览、视频/音频在线播放(HTTP Range 分段请求,返回 206/416)、一键下载。
|
||||||
- 路径遍历防护,仅允许访问 `directory.root` 之内。
|
- 路径遍历防护(`safe_path_join`:realpath 前缀校验),仅允许访问 `directory.root` 之内。
|
||||||
|
- **dnfs 代理**:`/get_notes`、`/add_note`、`/delete_note`、`/update_note` 透传 Authorization 转发到 `dnfs:8006`(解决浏览器跨域)。
|
||||||
|
- **账号旁路**:`/api/check_user`、`/api/reset_password` 等接口直接读写 `gateway/sqlite3.db` 的 Users 表;`/yhdet_callback`、`/api/yhdet_*` 对接 yhdet.top OAuth 登录。
|
||||||
- 统一品牌"树状菜单",支持深色模式(`css/theme.css`)。
|
- 统一品牌"树状菜单",支持深色模式(`css/theme.css`)。
|
||||||
- 可使用 Nuitka 编译为独立二进制(`fsys.bin`)。
|
- 可使用 Nuitka 编译为独立二进制(`fsys.bin`)。
|
||||||
- 配置见 [static/config.ini](static/config.ini)。
|
- 配置见 [static/config.ini](static/config.ini)(注:CORS 白名单硬编码于 `fsys.py`,config.ini 的 cors 段未生效)。
|
||||||
|
|
||||||
### dnfs(树状菜单服务)
|
### dnfs(树状菜单服务)
|
||||||
|
|
||||||
- 基于 `pid`(父节点 id)组织的树状节点,节点含 `title` 与 `content`。
|
- 基于 `pid`(父节点 id)组织的树状节点,节点含 `title` 与 `content`。
|
||||||
- 提供 `/get_notes`、`/add_note`、`/delete_note`、`/update_note` 四个接口。
|
- 提供 `/get_notes`、`/add_note`、`/delete_note`、`/update_note` 四个接口。
|
||||||
- 数据持久化通过 HTTP 调用 `db` 服务(`http://127.0.0.1:8005/execute`),不再直接操作数据库。
|
- 数据持久化通过 pymsg/TCP 调用 `db` 服务(`127.0.0.1:8005`,execute 路由),不再直接操作数据库。
|
||||||
- 新增节点前调用 `ai_review` 服务(`http://127.0.0.1:8007/review`)进行内容审核,审核不通过则拒绝写入。
|
- 新增节点前调用 `ai_review` 服务(`http://127.0.0.1:8007/review`)进行内容审核,审核不通过则拒绝写入。
|
||||||
|
|
||||||
### db(通用数据库服务)
|
### db(通用数据库服务)
|
||||||
|
|
||||||
- 单一接口 `POST /execute`,接收结构化 JSON(action + table + where + values),翻译为 SQL 执行。
|
- 基于自研 [pymsg](pymsg/) 协议(TCP 长度前缀帧 + JSON),单一路由 `_route: "execute"`,接收结构化 JSON(action + table + where + values),翻译为 SQL 执行。
|
||||||
- 支持 select / insert / update / delete,运算符包括 eq、ne、gt、lt、ge、le、like、in,支持排序与分页。
|
- 支持 select / insert / update / delete,运算符包括 eq、ne、gt、lt、ge、le、like、in,支持排序与分页。
|
||||||
- 基于 SQLAlchemy Core 表反射,参数化查询,零 SQL 注入风险。
|
- 基于 SQLAlchemy Core 表反射,参数化查询,零 SQL 注入风险。
|
||||||
- 仅监听 127.0.0.1,单线程,自动回滚,不暴露 DDL 操作。
|
- 仅监听 127.0.0.1,自动提交/回滚,不暴露 DDL 操作。
|
||||||
- 默认使用 SQLite(`database.db`),可通过 `DATABASE_URL` 环境变量切换至 PostgreSQL / MySQL。
|
- 默认使用 SQLite(`database.db`),可通过 `DATABASE_URL` 环境变量切换至 PostgreSQL / MySQL。
|
||||||
- **内置日志子模块**([log_module.py](db/log_module.py)):
|
- **内置日志子模块**:
|
||||||
- `log_internal()` — db 进程内部直接写 Logs 表,无 HTTP 往返,防递归设计。
|
- 启动时自动建 `Logs` 表(service / level / message / trace_id / extra + 4 个索引)。
|
||||||
- `/log/ingest` — 接收外部服务批量推送日志(单次上限 100 条,校验 service / level / message)。
|
- 外部服务通过 `log_client.py` 以 pymsg 调 execute 路由逐条 INSERT 写入日志。
|
||||||
- `/log/query` — 结构化查询(service / level / trace_id / 时间区间 / 关键字模糊搜索 / 排序分页)。
|
- `_log_internal()` — db 进程内部直接写 Logs 表,无网络往返,防递归设计。
|
||||||
- `/log/stats` — 按 service / level 分组统计。
|
|
||||||
- `/log/purge` — 手动清理指定时间之前的日志。
|
|
||||||
- `/log/health` — 健康检查。
|
|
||||||
- 后台守护线程每小时自动清理:保留 30 天 / 上限 100 万条。
|
- 后台守护线程每小时自动清理:保留 30 天 / 上限 100 万条。
|
||||||
- **统一日志客户端 SDK**([log_client.py](db/log_client.py)):各服务复制即用,纯标准库实现(urllib),异步非阻塞(内存队列 + 后台线程,50 条/2 秒批量 flush),fail-safe 设计(db 不可用时静默丢弃,不影响业务)。
|
- 注:`db/log_module.py`(Flask `/log/*` Blueprint)为旧版 HTTP 接口的遗留文件,当前 dbv2 未挂载。
|
||||||
|
- **统一日志客户端 SDK**(各服务目录下的 `log_client.py`,如 [dnfs/log_client.py](dnfs/log_client.py)):各服务复制即用,纯标准库实现,异步非阻塞(内存队列 + 后台线程,50 条/2 秒批量 flush),fail-safe 设计(db 不可用时静默丢弃,不影响业务)。
|
||||||
|
|
||||||
### ai_review(AI 内容审核服务)
|
### ai_review(AI 内容审核服务)
|
||||||
|
|
||||||
- 基于 DeepSeek LLM + ChromaDB 向量检索的 RAG 审核架构。
|
- 基于 DeepSeek LLM + ChromaDB 向量检索的 RAG 审核架构。
|
||||||
- 审核类别:色情、违禁药品、赌博、诈骗、仇恨/暴力/恐怖主义,灰色地带内容从严判定。
|
- 审核类别:色情、违禁药品、赌博、诈骗、仇恨/暴力/恐怖主义,灰色地带内容从严判定。
|
||||||
- 接口:`POST /review`(提交审核)、`POST /rag/feedback`(人工纠偏)、`GET /rag/list`(知识库列表)、`GET /health`(健康检查)。
|
- 接口:`POST /review`(提交审核)、`POST /rag/feedback`(人工纠偏)、`GET /rag/list`(知识库列表)、`GET /health`(健康检查)。
|
||||||
- RAG 检索 top-3 相似已标注样本注入 prompt,MD5 去重,API 异常时 fail-open。
|
- RAG 检索 top-3 相似已标注样本注入 prompt,MD5 去重。
|
||||||
|
- **fail-closed 设计**:API 异常或响应解析失败时一律返回 `{"invalid": "true"}`(拦截),宁可误拦不可漏放。
|
||||||
- 配置通过 `.env` 文件(已 gitignore),含 DeepSeek API 密钥、模型参数、监听地址。
|
- 配置通过 `.env` 文件(已 gitignore),含 DeepSeek API 密钥、模型参数、监听地址。
|
||||||
|
|
||||||
### captcha(验证码服务)
|
### captcha(验证码服务)
|
||||||
|
|
||||||
- 生成图形验证码,Token 一次性使用、5 分钟过期。
|
- 生成图形验证码,Token 一次性使用、5 分钟过期;纯标准库实现(http.server + PIL,无 Flask)。
|
||||||
- 接口:`GET /captcha`(生成)、`POST /verify`(校验)、`POST /new`(刷新);答案错误返回 `200 {valid:false}`(Token 照常消费),仅参数 / Token 错误返回 `400`。
|
- 接口:`GET /captcha/image`(生成 PNG,Token 在 `X-Captcha-Token` 响应头)、`POST /captcha/verify`(校验,body 含 token/answer)、`GET /health`。
|
||||||
|
- 答案错误返回 `200 {valid:false}`(Token 照常消费),仅参数 / Token 错误返回 `400`;比较采用 XOR 防时序攻击。
|
||||||
- 由 `gateway` 在 `/login`、`/register` 中可选启用 CAPTCHA 校验(字段 `captcha_token` / `captcha_answer`)。
|
- 由 `gateway` 在 `/login`、`/register` 中可选启用 CAPTCHA 校验(字段 `captcha_token` / `captcha_answer`)。
|
||||||
- 内存存储,不支持多实例水平扩展(需换 Redis);详见 [captcha/README.md](captcha/README.md)。
|
- 内存存储,不支持多实例水平扩展(需换 Redis);详见 [captcha/README.md](captcha/README.md)。
|
||||||
|
|
||||||
@@ -154,8 +156,9 @@
|
|||||||
|
|
||||||
| 文件 | 说明 |
|
| 文件 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [log_client.py](db/log_client.py) | 统一日志客户端 SDK,各服务复制即用(纯标准库,异步批量推送) |
|
| [pymsg](pymsg/) | 自研 RPC 基础库(setuptools 包),协议为 `[4字节大端长度][UTF-8 JSON]`,提供 `send/recv/MsgClient/MsgServer`;db 服务端与各 log_client 的基础依赖,requirements 以 `../pymsg` 本地路径引用 |
|
||||||
| [security_middleware.py](db/security_middleware.py) | Flask 安全响应头中间件(CSP / X-Frame-Options / nosniff 等),各服务各持一份副本 |
|
| log_client.py | 统一日志客户端 SDK,各服务目录各持一份副本(如 [dnfs/log_client.py](dnfs/log_client.py)),纯标准库 + pymsg,异步批量推送 |
|
||||||
|
| security_middleware.py | Flask 安全响应头中间件(CSP / X-Frame-Options / nosniff 等),各服务各持一份副本 |
|
||||||
|
|
||||||
## 端口一览
|
## 端口一览
|
||||||
|
|
||||||
@@ -166,9 +169,9 @@
|
|||||||
| 5200 | core | — | PMC HTTP 管理接口 |
|
| 5200 | core | — | PMC HTTP 管理接口 |
|
||||||
| 9200 | static | 127.0.0.1 | 前端托管(经 gateway 代理) |
|
| 9200 | static | 127.0.0.1 | 前端托管(经 gateway 代理) |
|
||||||
| 8006 | dnfs | 127.0.0.1 | 树状菜单(经 gateway 代理) |
|
| 8006 | dnfs | 127.0.0.1 | 树状菜单(经 gateway 代理) |
|
||||||
| 8005 | db | 127.0.0.1 | 数据库服务(仅内部调用) |
|
| 8005 | db | 127.0.0.1 | 数据库服务(pymsg/TCP,仅内部调用) |
|
||||||
| 8007 | ai_review | 127.0.0.1 | AI 审核(仅内部调用) |
|
| 8007 | ai_review | 127.0.0.1 | AI 审核(仅内部调用) |
|
||||||
| 8009 | captcha | 127.0.0.1 | 验证码服务(独立组件) |
|
| 8009 | captcha | 127.0.0.1 | 验证码服务(stdlib http.server,独立组件) |
|
||||||
| 8008 | smtp | 127.0.0.1 | 邮件服务(仅内部调用,注册验证码邮件后端) |
|
| 8008 | smtp | 127.0.0.1 | 邮件服务(仅内部调用,注册验证码邮件后端) |
|
||||||
| 8765 | aibox | 127.0.0.1 | AI 智能体(WebSocket,独立产品) |
|
| 8765 | aibox | 127.0.0.1 | AI 智能体(WebSocket,独立产品) |
|
||||||
|
|
||||||
@@ -249,10 +252,10 @@ gateway 改为内部 HTTP 模式([example.ini](gateway/example.ini) 中 `tls =
|
|||||||
- **服务按业务能力拆分**:WAF 网关、应用网关、前端托管、节点树、数据库、内容审核各自独立、独立端口、独立数据存储。
|
- **服务按业务能力拆分**:WAF 网关、应用网关、前端托管、节点树、数据库、内容审核各自独立、独立端口、独立数据存储。
|
||||||
- **API 网关模式**:`waf` 作为边缘网关统一入口,`gateway` 承担鉴权与反向代理,内部服务通过 `closed` 列表隔离。
|
- **API 网关模式**:`waf` 作为边缘网关统一入口,`gateway` 承担鉴权与反向代理,内部服务通过 `closed` 列表隔离。
|
||||||
- **进程编排/守护**:`core` 作为服务管理器拉起并守护子进程,类似简易版 supervisor。
|
- **进程编排/守护**:`core` 作为服务管理器拉起并守护子进程,类似简易版 supervisor。
|
||||||
- **多语言异构**:C++(core / gateway)、Python(static / dnfs / db / ai_review)、Go(waf)混合,通过 HTTP 通信。
|
- **多语言异构**:C++(core / gateway)、Python(static / dnfs / db / ai_review)、Go(waf)混合,对外 HTTP、内部数据访问走 pymsg/TCP。
|
||||||
- **数据访问层解耦**:`db` 服务将数据库操作收敛为统一的 HTTP 接口,业务服务无需直连数据库。
|
- **数据访问层解耦**:`db` 服务将数据库操作收敛为统一的 pymsg/TCP 接口,业务服务无需直连数据库。
|
||||||
- **AI 能力集成**:`ai_review` 以 RAG 增强的 LLM 审核为业务层提供内容安全能力,支持人工反馈闭环。
|
- **AI 能力集成**:`ai_review` 以 RAG 增强的 LLM 审核为业务层提供内容安全能力,支持人工反馈闭环。
|
||||||
- **集中式可观测性**:`db` 内置日志收集服务(/log/* 路由),各服务通过 `log_client.py` SDK 异步推送日志,实现全链路汇聚、查询与统计,无需引入额外中间件。
|
- **集中式可观测性**:`db` 内置日志表与自动清理,各服务通过 `log_client.py` SDK 异步推送日志(pymsg),实现全链路汇聚,无需引入额外中间件。
|
||||||
- **安全分层**:`waf` 在边缘做规则匹配(OWASP CRS / 自定义 SecLang 规则),`gateway` 在应用层做鉴权,`db` 仅监听本机,多层防护。
|
- **安全分层**:`waf` 在边缘做规则匹配(OWASP CRS / 自定义 SecLang 规则),`gateway` 在应用层做鉴权,`db` 仅监听本机,多层防护。
|
||||||
|
|
||||||
与云原生微服务的差异:未引入服务注册发现、容器编排、消息总线,服务地址在 `waf/config.yaml` 与 `gateway/example.ini` 中静态配置。对于个人/中小规模的 DIY 场景,这种取舍在简洁性与可控性上是合理的。
|
与云原生微服务的差异:未引入服务注册发现、容器编排、消息总线,服务地址在 `waf/config.yaml` 与 `gateway/example.ini` 中静态配置。对于个人/中小规模的 DIY 场景,这种取舍在简洁性与可控性上是合理的。
|
||||||
|
|||||||
+79
-3
@@ -26,7 +26,7 @@ using namespace qing;
|
|||||||
namespace json = boost::json;
|
namespace json = boost::json;
|
||||||
namespace po = boost::program_options;
|
namespace po = boost::program_options;
|
||||||
|
|
||||||
constexpr const char *_VER = "0.4.0";
|
constexpr const char *_VER = "0.4.1";
|
||||||
|
|
||||||
//////////////////////////////////
|
//////////////////////////////////
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -291,6 +291,13 @@ http::response<http::string_body> handleCaptcha(
|
|||||||
const http::request<http::string_body>& req,
|
const http::request<http::string_body>& req,
|
||||||
const std::unordered_map<std::string, std::string>& params);
|
const std::unordered_map<std::string, std::string>& params);
|
||||||
|
|
||||||
|
http::response<http::string_body> handleLogout(
|
||||||
|
const http::request<http::string_body>& req,
|
||||||
|
const std::unordered_map<std::string, std::string>& params);
|
||||||
|
|
||||||
|
/* 从 Cookie 头中解析指定名称的值(找不到返回空串) */
|
||||||
|
std::string get_cookie_value(const std::string& cookie_header, const std::string& name);
|
||||||
|
|
||||||
|
|
||||||
/* headers can't processed */
|
/* headers can't processed */
|
||||||
bool is_special_header(const std::string &header);
|
bool is_special_header(const std::string &header);
|
||||||
@@ -532,6 +539,11 @@ void registerRoutes(HttpServer &server) {
|
|||||||
[](const auto& req, const auto& params) { return handleRegister(req, params); });
|
[](const auto& req, const auto& params) { return handleRegister(req, params); });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.post("/logout", [](const auto& req, const auto& params) -> http::response<http::string_body> {
|
||||||
|
return wrapperHttp(req, params,
|
||||||
|
[](const auto& req, const auto& params) { return handleLogout(req, params); });
|
||||||
|
});
|
||||||
|
|
||||||
server.get("*", [](const auto& req, const auto& params) -> http::response<http::string_body> {
|
server.get("*", [](const auto& req, const auto& params) -> http::response<http::string_body> {
|
||||||
return wrapperHttp(req, params,
|
return wrapperHttp(req, params,
|
||||||
[](const auto& req, const auto& params) { return handleProxy(req, params, "get"); });
|
[](const auto& req, const auto& params) { return handleProxy(req, params, "get"); });
|
||||||
@@ -776,12 +788,38 @@ http::response<http::string_body> handleLogin(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
res.set("Connection", "close");
|
res.set("Connection", "close");
|
||||||
|
|
||||||
|
/* 签发 HttpOnly Cookie(防 XSS 窃取 Token;JS 不再经手 Token) */
|
||||||
|
{
|
||||||
|
std::string ck = "q4_token=" + token +
|
||||||
|
"; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=" +
|
||||||
|
std::to_string(_TOKEN_KEEP_TIME);
|
||||||
|
res.set("Set-Cookie", ck);
|
||||||
|
}
|
||||||
|
|
||||||
res.result(http::status::ok);
|
res.result(http::status::ok);
|
||||||
res.body() = json::serialize(jv_rr);
|
res.body() = json::serialize(jv_rr);
|
||||||
res.prepare_payload();
|
res.prepare_payload();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 退出登录 /logout:清除 q4_token Cookie */
|
||||||
|
http::response<http::string_body> handleLogout(
|
||||||
|
const http::request<http::string_body>& req,
|
||||||
|
const std::unordered_map<std::string, std::string>& params)
|
||||||
|
{
|
||||||
|
http::response<http::string_body> res;
|
||||||
|
res.version(req.version());
|
||||||
|
|
||||||
|
res.set("Set-Cookie",
|
||||||
|
"q4_token=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0");
|
||||||
|
res.set("Connection", "close");
|
||||||
|
res.result(http::status::ok);
|
||||||
|
res.body() = R"({"success": true, "message": "logged out"})";
|
||||||
|
res.prepare_payload();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
/* 处理注册 /register */
|
/* 处理注册 /register */
|
||||||
http::response<http::string_body> handleRegister(
|
http::response<http::string_body> handleRegister(
|
||||||
const http::request<http::string_body>& req,
|
const http::request<http::string_body>& req,
|
||||||
@@ -878,6 +916,7 @@ http::response<http::string_body> handleProxy(
|
|||||||
std::string t_url = "";
|
std::string t_url = "";
|
||||||
std::string url = "";
|
std::string url = "";
|
||||||
std::string auth = "";
|
std::string auth = "";
|
||||||
|
std::string cookie_token = ""; /* 从 Cookie 提取的 Token(用于向下游注入 Authorization) */
|
||||||
json::value jv_t;
|
json::value jv_t;
|
||||||
{
|
{
|
||||||
auto flag = false;
|
auto flag = false;
|
||||||
@@ -912,10 +951,21 @@ http::response<http::string_body> handleProxy(
|
|||||||
url += "/";
|
url += "/";
|
||||||
url += target_path;
|
url += target_path;
|
||||||
|
|
||||||
|
std::string raw = "";
|
||||||
it = req.base().find("Authorization");
|
it = req.base().find("Authorization");
|
||||||
if (it == req.base().end() && !flag)
|
if (it != req.base().end()) {
|
||||||
|
raw = std::string(it->value());
|
||||||
|
} else {
|
||||||
|
/* 回退:从 HttpOnly Cookie 读取 q4_token(浏览器同源请求自动携带) */
|
||||||
|
auto cit = req.base().find("Cookie");
|
||||||
|
if (cit != req.base().end()) {
|
||||||
|
cookie_token = get_cookie_value(std::string(cit->value()), "q4_token");
|
||||||
|
if (!cookie_token.empty())
|
||||||
|
raw = "Bearer " + cookie_token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (raw.empty() && !flag)
|
||||||
THROW_EX_CUSTOM(HttpException, 2301, "Unauthorization", 401, "*");
|
THROW_EX_CUSTOM(HttpException, 2301, "Unauthorization", 401, "*");
|
||||||
std::string raw = req["Authorization"];
|
|
||||||
if (raw.substr(0, 7) == "Bearer ") {
|
if (raw.substr(0, 7) == "Bearer ") {
|
||||||
raw = raw.substr(7, raw.size() - 7);
|
raw = raw.substr(7, raw.size() - 7);
|
||||||
size_t idx = raw.find("_");
|
size_t idx = raw.find("_");
|
||||||
@@ -996,6 +1046,11 @@ http::response<http::string_body> handleProxy(
|
|||||||
client.set_header("X-Forwarded-For: unknown");
|
client.set_header("X-Forwarded-For: unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Token 来自 Cookie 时,向下游补齐 Authorization 头(dnfs 等下游仍按 Bearer 解析,无需改动) */
|
||||||
|
if (!cookie_token.empty()) {
|
||||||
|
client.set_header("Authorization: Bearer " + cookie_token);
|
||||||
|
}
|
||||||
|
|
||||||
/* 转发请求 */
|
/* 转发请求 */
|
||||||
if (method == "get")
|
if (method == "get")
|
||||||
client.get_stream_sync(url, callback);
|
client.get_stream_sync(url, callback);
|
||||||
@@ -1031,6 +1086,27 @@ bool is_special_header(const std::string &header) {
|
|||||||
|| header == "Forwarded";
|
|| header == "Forwarded";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string get_cookie_value(const std::string& cookie_header, const std::string& name) {
|
||||||
|
// Cookie 头格式:"k1=v1; k2=v2; q4_token=xxx"
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos <= cookie_header.size()) {
|
||||||
|
size_t end = cookie_header.find(';', pos);
|
||||||
|
if (end == std::string::npos) end = cookie_header.size();
|
||||||
|
std::string pair = cookie_header.substr(pos, end - pos);
|
||||||
|
size_t eq = pair.find('=');
|
||||||
|
if (eq != std::string::npos) {
|
||||||
|
std::string k = pair.substr(0, eq);
|
||||||
|
size_t b = k.find_first_not_of(" \t");
|
||||||
|
size_t e = k.find_last_not_of(" \t");
|
||||||
|
if (b != std::string::npos) k = k.substr(b, e - b + 1);
|
||||||
|
if (k == name) return pair.substr(eq + 1);
|
||||||
|
}
|
||||||
|
if (end == cookie_header.size()) break;
|
||||||
|
pos = end + 1;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
bool is_safe_target(const std::string& target) {
|
bool is_safe_target(const std::string& target) {
|
||||||
// 请求路径安全性校验(P1-1)
|
// 请求路径安全性校验(P1-1)
|
||||||
// 防御目标:路径遍历、协议注入、空字节、非绝对路径形式
|
// 防御目标:路径遍历、协议注入、空字节、非绝对路径形式
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ def proxy_get_notes():
|
|||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
h = {"Content-Type": "application/json"}
|
h = {"Content-Type": "application/json"}
|
||||||
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
||||||
|
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
|
||||||
req = _urlreq.Request(f"{DNFS_URL}/get_notes", data=_json.dumps(data).encode(), headers=h, method="POST")
|
req = _urlreq.Request(f"{DNFS_URL}/get_notes", data=_json.dumps(data).encode(), headers=h, method="POST")
|
||||||
with _urlreq.urlopen(req, timeout=15) as r:
|
with _urlreq.urlopen(req, timeout=15) as r:
|
||||||
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
||||||
@@ -426,6 +427,7 @@ def proxy_add_note():
|
|||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
h = {"Content-Type": "application/json"}
|
h = {"Content-Type": "application/json"}
|
||||||
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
||||||
|
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
|
||||||
req = _urlreq.Request(f"{DNFS_URL}/add_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
req = _urlreq.Request(f"{DNFS_URL}/add_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
||||||
with _urlreq.urlopen(req, timeout=15) as r:
|
with _urlreq.urlopen(req, timeout=15) as r:
|
||||||
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
||||||
@@ -440,6 +442,7 @@ def proxy_delete_note():
|
|||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
h = {"Content-Type": "application/json"}
|
h = {"Content-Type": "application/json"}
|
||||||
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
||||||
|
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
|
||||||
req = _urlreq.Request(f"{DNFS_URL}/delete_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
req = _urlreq.Request(f"{DNFS_URL}/delete_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
||||||
with _urlreq.urlopen(req, timeout=15) as r:
|
with _urlreq.urlopen(req, timeout=15) as r:
|
||||||
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
||||||
@@ -454,6 +457,7 @@ def proxy_update_note():
|
|||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
h = {"Content-Type": "application/json"}
|
h = {"Content-Type": "application/json"}
|
||||||
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
|
||||||
|
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
|
||||||
req = _urlreq.Request(f"{DNFS_URL}/update_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
req = _urlreq.Request(f"{DNFS_URL}/update_note", data=_json.dumps(data).encode(), headers=h, method="POST")
|
||||||
with _urlreq.urlopen(req, timeout=15) as r:
|
with _urlreq.urlopen(req, timeout=15) as r:
|
||||||
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
|
||||||
|
|||||||
+18
-23
@@ -1,11 +1,10 @@
|
|||||||
// Session restore from localStorage
|
// 认证改为 HttpOnly Cookie(gateway 下发 q4_token),浏览器同源请求自动携带,JS 不再经手 Token
|
||||||
if(!sessionStorage.getItem('q4_token')){var t=localStorage.getItem('q4_token');if(t)sessionStorage.setItem('q4_token',t);}
|
|
||||||
|
|
||||||
/* by qing.
|
/* by qing.
|
||||||
*
|
*
|
||||||
* 用于获取一个节点下的所有子节点信息,0为根节点。
|
* 用于获取一个节点下的所有子节点信息,0为根节点。
|
||||||
*/
|
*/
|
||||||
async function getNode(token, id) {
|
async function getNode(id) {
|
||||||
try {
|
try {
|
||||||
obj = { "pid": id }
|
obj = { "pid": id }
|
||||||
jsonBody = JSON.stringify(obj)
|
jsonBody = JSON.stringify(obj)
|
||||||
@@ -14,7 +13,6 @@ async function getNode(token, id) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json', // 告诉服务器内容为json
|
'Content-Type': 'application/json', // 告诉服务器内容为json
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Target-Url': 'http://127.0.0.1:8006'
|
'Target-Url': 'http://127.0.0.1:8006'
|
||||||
},
|
},
|
||||||
body: jsonBody
|
body: jsonBody
|
||||||
@@ -54,7 +52,7 @@ async function getNode(token, id) {
|
|||||||
*
|
*
|
||||||
* 用于向dnfs添加节点
|
* 用于向dnfs添加节点
|
||||||
*/
|
*/
|
||||||
async function addNode(token, pid, title, content) {
|
async function addNode(pid, title, content) {
|
||||||
try {
|
try {
|
||||||
obj = {
|
obj = {
|
||||||
"pid": pid,
|
"pid": pid,
|
||||||
@@ -67,7 +65,6 @@ async function addNode(token, pid, title, content) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json', // 告诉服务器内容为json
|
'Content-Type': 'application/json', // 告诉服务器内容为json
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Target-Url': 'http://127.0.0.1:8006'
|
'Target-Url': 'http://127.0.0.1:8006'
|
||||||
},
|
},
|
||||||
body: jsonBody
|
body: jsonBody
|
||||||
@@ -89,8 +86,8 @@ async function addNode(token, pid, title, content) {
|
|||||||
|
|
||||||
input_note_title.value = "";
|
input_note_title.value = "";
|
||||||
input_note_content.value = "";
|
input_note_content.value = "";
|
||||||
getNode(token, stack[stack.length - 1]).then(load_notes);
|
getNode(stack[stack.length - 1]).then(load_notes);
|
||||||
getNode(token, stack[stack.length - 1]).then(loadMap)
|
getNode(stack[stack.length - 1]).then(loadMap)
|
||||||
|
|
||||||
//if (result.success) {
|
//if (result.success) {
|
||||||
alert(`添加节点成功`, result);
|
alert(`添加节点成功`, result);
|
||||||
@@ -108,7 +105,7 @@ async function addNode(token, pid, title, content) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNode(token, id) {
|
async function deleteNode(id) {
|
||||||
if (confirm("确认删除此节点?") == false) {
|
if (confirm("确认删除此节点?") == false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -121,7 +118,6 @@ async function deleteNode(token, id) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Target-Url': 'http://127.0.0.1:8006'
|
'Target-Url': 'http://127.0.0.1:8006'
|
||||||
},
|
},
|
||||||
body: jsonBody
|
body: jsonBody
|
||||||
@@ -142,8 +138,8 @@ async function deleteNode(token, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stack.pop();
|
stack.pop();
|
||||||
getNode(token, stack[stack.length - 1]).then(load_notes);
|
getNode(stack[stack.length - 1]).then(load_notes);
|
||||||
getNode(token, stack[stack.length - 1]).then(loadMap)
|
getNode(stack[stack.length - 1]).then(loadMap)
|
||||||
|
|
||||||
console.log(`删除节点成功`);
|
console.log(`删除节点成功`);
|
||||||
return result
|
return result
|
||||||
@@ -182,7 +178,7 @@ input_note_submit.addEventListener('click', function(event) {
|
|||||||
var pid = (stack.length == 0) ? 0 : stack[stack.length - 1];
|
var pid = (stack.length == 0) ? 0 : stack[stack.length - 1];
|
||||||
//alert(stack.length)
|
//alert(stack.length)
|
||||||
//alert(pid)
|
//alert(pid)
|
||||||
addNode(token, pid, title, content).then(result => {
|
addNode(pid, title, content).then(result => {
|
||||||
//alert(result.message);
|
//alert(result.message);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -199,7 +195,7 @@ delete_note.addEventListener('click', (event) => {
|
|||||||
console.log("节点未删除");
|
console.log("节点未删除");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
deleteNode(token, id);
|
deleteNode(id);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -232,8 +228,8 @@ function load_notes(main_node) {
|
|||||||
card.addEventListener('click', function(event) {
|
card.addEventListener('click', function(event) {
|
||||||
var id = Number(item.id)
|
var id = Number(item.id)
|
||||||
stack.push(id)
|
stack.push(id)
|
||||||
getNode(token, id).then(load_notes)
|
getNode(id).then(load_notes)
|
||||||
getNode(token, id).then(loadMap)
|
getNode(id).then(loadMap)
|
||||||
});
|
});
|
||||||
let ttl = document.createElement('center');
|
let ttl = document.createElement('center');
|
||||||
let ttl1 =document.createElement('h3');
|
let ttl1 =document.createElement('h3');
|
||||||
@@ -274,10 +270,10 @@ function loadMap(children_json) {
|
|||||||
fold_div.addEventListener("click", () => {
|
fold_div.addEventListener("click", () => {
|
||||||
if (is_expand == true) {
|
if (is_expand == true) {
|
||||||
is_expand = false;
|
is_expand = false;
|
||||||
getNode(token, stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap);
|
getNode(stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap);
|
||||||
} else {
|
} else {
|
||||||
is_expand = true;
|
is_expand = true;
|
||||||
getNode(token, stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap);
|
getNode(stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (need_return == true) {
|
if (need_return == true) {
|
||||||
@@ -290,8 +286,8 @@ function loadMap(children_json) {
|
|||||||
back.addEventListener("click", () => {
|
back.addEventListener("click", () => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
var id = (stack.length >= 0) ? stack[stack.length - 1] : 0;
|
var id = (stack.length >= 0) ? stack[stack.length - 1] : 0;
|
||||||
getNode(token, id).then(load_notes);
|
getNode(id).then(load_notes);
|
||||||
getNode(token, id).then(loadMap);
|
getNode(id).then(loadMap);
|
||||||
});
|
});
|
||||||
minimap.appendChild(back);
|
minimap.appendChild(back);
|
||||||
}
|
}
|
||||||
@@ -309,6 +305,5 @@ function loadMap(children_json) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = sessionStorage.getItem('q4_token')
|
getNode(0).then(load_notes);
|
||||||
getNode(token, 0).then(load_notes);
|
getNode(0).then(loadMap)
|
||||||
getNode(token, 0).then(loadMap)
|
|
||||||
@@ -76,8 +76,7 @@ form.addEventListener('submit', async function(e) {
|
|||||||
try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); }
|
try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); }
|
||||||
if (!res.ok) throw new Error(result.message || '登录失败');
|
if (!res.ok) throw new Error(result.message || '登录失败');
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
localStorage.setItem('q4_token', result.data.token);
|
// Token 已由网关通过 HttpOnly Cookie 下发,JS 不再经手
|
||||||
sessionStorage.setItem('q4_token', result.data.token);
|
|
||||||
alert('登录成功');
|
alert('登录成功');
|
||||||
location.href = '/dnfs.html';
|
location.href = '/dnfs.html';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in new issue
Block a user