Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ TIEBA_BROWSER_HEADLESS=true
TIEBA_BROWSER_CHANNEL=
WEB_ADMIN_PASSWORD=change-this-admin-password
WEB_ADMIN_SESSION_TTL_HOURS=168
# 会话绝对寿命(小时):自创建起超过该时长必须重新登录,滑动续期不能突破该上限。
WEB_ADMIN_SESSION_MAX_AGE_HOURS=168
WEB_ADMIN_COOKIE_SECURE=auto
1 change: 1 addition & 0 deletions docs/admin/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ MCP server 的普通字段支持 `${ENV_VAR}` 与 `${ENV_VAR:-default}`;认证
| `WEB_ADMIN_HOST` | 监听地址 | `127.0.0.1` |
| `WEB_ADMIN_PORT` | 监听端口 | `5104` |
| `WEB_ADMIN_SESSION_TTL_HOURS` | session 有效期(小时) | `168` |
| `WEB_ADMIN_SESSION_MAX_AGE_HOURS` | session 绝对寿命(小时),自创建起超过后必须重新登录 | `168` |
| `WEB_ADMIN_COOKIE_SECURE` | 是否下发 Secure cookie:`auto` / `true` / `false` | `auto` |

### Docker 构建相关
Expand Down
1 change: 1 addition & 0 deletions docs/admin/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ ssh -N -L 5104:127.0.0.1:5104 user@server

```env
WEB_ADMIN_SESSION_TTL_HOURS=168
WEB_ADMIN_SESSION_MAX_AGE_HOURS=168
WEB_ADMIN_COOKIE_SECURE=auto
```

Expand Down
25 changes: 22 additions & 3 deletions docs/admin/web-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ FastAPI 管理接口独立校验应用会话。内层登录带速率限制:同

- 存储位置:`data/web_admin_sessions.db`
- 介质:SQLite
- 内容:`session_id`、创建时间、过期时间、最近访问时间、客户端 IP、User-Agent
- 内容:`session_id` 的 SHA-256 哈希(明文 id 只存在于浏览器 cookie)、创建时间、过期时间、最近访问时间、客户端 IP、User-Agent

管理员登录后,浏览器通过会话 Cookie 访问后台;口令只用于登录校验。
管理员登录后,浏览器通过会话 Cookie 访问后台;口令只用于登录校验。会话随活动滑动续期(`WEB_ADMIN_SESSION_TTL_HOURS`),同时受绝对寿命限制(`WEB_ADMIN_SESSION_MAX_AGE_HOURS`):自创建起超过绝对寿命的会话会被删除并要求重新登录。

### 3. cookie 属性

Expand Down Expand Up @@ -84,13 +84,15 @@ Web Admin 使用以下环境变量:
```env
WEB_ADMIN_PASSWORD=change-this-admin-password
WEB_ADMIN_SESSION_TTL_HOURS=168
WEB_ADMIN_SESSION_MAX_AGE_HOURS=168
WEB_ADMIN_COOKIE_SECURE=auto
```

说明:

- `WEB_ADMIN_PASSWORD` 应用层登录口令,必填
- `WEB_ADMIN_SESSION_TTL_HOURS` session 续期窗口,默认 `168`
- `WEB_ADMIN_SESSION_MAX_AGE_HOURS` session 绝对寿命,默认 `168`,自创建起超过该时长必须重新登录
- `WEB_ADMIN_COOKIE_SECURE` `auto | true | false`

在根目录 `.env` 设置这些变量后,源码安装重新启动 Web 进程,容器安装重新创建 Web 服务。源码安装先在 `frontend/` 构建前端,再从项目根运行 `python web_api.py`;容器安装与变量更新步骤见[部署指南](deployment.md)。`web_api.py` 启动时读取根 `.env`,已有进程环境变量优先。
Expand All @@ -104,11 +106,27 @@ WEB_ADMIN_COOKIE_SECURE=auto
```nginx
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-For $remote_addr;
```

这样 `WEB_ADMIN_COOKIE_SECURE=auto` 才能正确判断当前请求应当下发 `Secure` cookie。

`X-Forwarded-For` 使用 `$remote_addr` 覆盖语义:后端从直连对端为内网地址的请求中取 XFF 最右一个非内网跳作为真实客户端 IP,登录限流按该 IP 计数。`$proxy_add_x_forwarded_for` 会把客户端自带的 XFF 保留在链上,覆盖语义可以让链上只有 nginx 实际看到的对端地址。

该算法假定可信代理全是内网地址。如果 nginx 前面还有公网前置代理(CDN、云负载均衡),取到的将是前置代理的出口地址而非终端客户端:限流与封禁按出口地址共享计数,误伤同出口的其他管理员;这种拓扑下应在最外层代理做限流。反过来,不要把后台直接绑定到不信任的二层网络(源码安装时显式设置 `WEB_ADMIN_HOST=0.0.0.0` 且无代理)——此时对端是内网地址而路径上没有代理追加真实来源,XFF 完全由客户端控制,限流键可被任意伪造。

外层 `auth_basic` 本身没有速率限制,建议在 nginx 站点中为 `/ops/` 位置补充请求限流,例如:

```nginx
limit_req_zone $binary_remote_addr zone=quickquip_ops:10m rate=10r/m;

location /ops/ {
limit_req zone=quickquip_ops burst=20 nodelay;
auth_basic "quickquip";
...
}
```

如果你的站点已经明确只通过 HTTPS 暴露,但暂时不方便补这些 header,也可以直接设置:

```env
Expand All @@ -124,6 +142,7 @@ WEB_ADMIN_COOKIE_SECURE=true
- 浏览器会话使用 `SameSite=Strict`
- 写操作会额外检查 `Origin` / `Referer` 是否与当前请求宿主一致
- FastAPI 独立校验应用登录会话
- 后端对全部响应统一下发安全响应头(CSP、`X-Frame-Options`、`X-Content-Type-Options`、`Referrer-Policy`)

外层站点访问控制和应用层 session 共同组成后台的纵深防御边界。

Expand Down
2 changes: 1 addition & 1 deletion docs/dev/llm-configuration-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

本文供配置 API 调用方和维护者查阅。LLM 配置使用 `data/llm_config.db`,Web Admin、CLI 与 Bot 通过同一个应用服务保存草稿、建立快照和提交控制任务。人格文件、Skill 内容、群覆盖、历史记录、用量和 Trace 由各自存储维护。

接口前缀为 `/ops/api/llm-configuration`,需要现有管理员会话 Cookie `quickquip_admin_session`。写请求必须携带同源 `Origin` 或 `Referer`。请求体上限为 1 MiB,分块传输也执行此限制。完整机器契约见 [OpenAPI JSON](llm-configuration.openapi.json);运行中的管理 API 在 `/openapi.json` 提供同一字段约束。
接口前缀为 `/ops/api/llm-configuration`,需要现有管理员会话 Cookie `quickquip_admin_session`。写请求必须携带同源 `Origin` 或 `Referer`。请求体上限为 1 MiB,分块传输也执行此限制。完整机器契约见 [OpenAPI JSON](llm-configuration.openapi.json),由 `python scripts/ci/export_llm_configuration_openapi.py` 在进程内构造应用导出,随约束或路由变更同步提交;运行中的管理 API 不提供 `/docs`、`/redoc`、`/openapi.json` 文档端点。

前端设计与操作流程见[前端交付契约](llm-configuration-frontend.md),其中说明中文字段含义、参数继承控件、保存与应用、冲突处理及设计验收场景。

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const BASE = import.meta.env.BASE_URL.replace(/\/$/, '')
export const BASE = import.meta.env.BASE_URL.replace(/\/$/, '')

class ApiError extends Error {
status: number
Expand Down
65 changes: 51 additions & 14 deletions frontend/src/api/tieba.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { request } from './index'
import { request, BASE } from './index'

/** 对应 TiebaThread.to_dict();peek 接口原样返回此形状 */
export interface TiebaThread {
Expand Down Expand Up @@ -63,22 +63,59 @@ export function tiebaImgProxyUrl(url: string) {
return `/ops/api/tieba/imgproxy?url=${encodeURIComponent(url)}`
}

export function openTiebaSyncStream(forum: string, onMessage: (data: string) => void, onDone: () => void) {
export interface TiebaSyncStream {
/** 中止同步流(组件卸载时调用);服务端的抓取任务可能继续执行至完成 */
close: () => void
}

/**
* 触发贴吧同步并消费其 SSE 进度流。同步有副作用,走 POST + fetch 流式读取
* (EventSource 只支持 GET),返回的句柄用于主动关闭。
* 结束语义:收到 [done]、流结束或连接出错即视为完成(不重连),回调 onDone。
*/
export function startTiebaSync(
forum: string,
onMessage: (data: string) => void,
onDone: () => void,
): TiebaSyncStream {
const controller = new AbortController()
const params = forum ? `?forum=${encodeURIComponent(forum)}` : ''
const es = new EventSource(`/ops/api/tieba/sync${params}`)
es.onmessage = (e) => {
if (e.data === '[done]') {
es.close()
onDone()
} else {
onMessage(e.data)
}
}
es.onerror = () => {
es.close()
let doneFired = false
const fireDone = () => {
if (doneFired) return
doneFired = true
onDone()
}
return es

fetch(`${BASE}/api/tieba/sync${params}`, {
method: 'POST',
credentials: 'same-origin',
signal: controller.signal,
})
.then(async (res) => {
if (!res.ok || !res.body) return
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
let sep = buffer.indexOf('\n\n')
while (sep >= 0) {
const frame = buffer.slice(0, sep)
buffer = buffer.slice(sep + 2)
sep = buffer.indexOf('\n\n')
const data = frame.startsWith('data: ') ? frame.slice(6) : null
if (data === '[done]') return
if (data) onMessage(data)
}
}
})
.catch(() => {})
.finally(fireDone)

return { close: () => controller.abort() }
}

export async function listTiebaForums(): Promise<TiebaForumsResponse> {
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/composables/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ref } from 'vue'
import type { Ref } from 'vue'
import { getAuthState, login as apiLogin, logout as apiLogout } from '../api/auth'
import { setUnauthorizedHandler } from '../api/index'
import { toastMsg } from '../toast'
import { toast, toastMsg } from '../toast'

interface ApiError {
status?: number
Expand Down Expand Up @@ -55,11 +55,12 @@ async function handleLogout(): Promise<void> {
authError.value = ''
try {
await apiLogout()
authenticated.value = false
} catch (error: unknown) {
// 退出请求失败时服务端会话可能仍有效,保留登录态并如实提示
const err = error as ApiError
authError.value = err.data?.detail || err.message || '退出失败'
toast(`退出失败,当前会话仍有效:${err.data?.detail || err.message || '请稍后重试'}`, 'error')
} finally {
authenticated.value = false
authBusy.value = false
}
}
Expand Down
23 changes: 16 additions & 7 deletions frontend/src/composables/useTiebaSync.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import { ref, nextTick } from 'vue'
import { openTiebaSyncStream } from '../api/tieba'
import { ref, nextTick, onScopeDispose } from 'vue'
import { startTiebaSync, type TiebaSyncStream } from '../api/tieba'

/**
* 贴吧同步流(SSE):连接、日志缓冲与自动滚动。
* 结束语义与 api/tieba.openTiebaSyncStream 一致:收到 [done] 或连接出错
* 即关闭流并视为完成(不重连),随后回调 onSynced 让调用方刷新数据。
* 组件卸载时不主动关流(沿用拆分前视图的行为;SSE 生命周期强化留待后续)。
* 贴吧同步流(POST + fetch 流式读取):连接、日志缓冲与自动滚动。
* 结束语义与 api/tieba.startTiebaSync 一致:收到 [done]、流结束或连接出错
* 即视为完成(不重连),随后回调 onSynced 让调用方刷新数据。
* 组件卸载时主动中止流;服务端的抓取任务可能继续执行至完成。
*/
export function useTiebaSync(onSynced: () => void) {
const syncing = ref(false)
const syncLog = ref<string[]>([])
const logEl = ref<HTMLPreElement | null>(null)
let stream: TiebaSyncStream | null = null

function startSync(forum: string | null) {
stream?.close()
syncing.value = true
syncLog.value = [`▶ 开始同步${forum ? ' ' + forum + '吧' : '全部贴吧'}…`]
openTiebaSyncStream(
stream = startTiebaSync(
forum || '',
(msg) => {
syncLog.value.push(msg)
nextTick(() => { if (logEl.value) logEl.value.scrollTop = logEl.value.scrollHeight })
},
() => {
stream = null
syncing.value = false
onSynced()
},
Expand All @@ -32,5 +35,11 @@ export function useTiebaSync(onSynced: () => void) {
syncLog.value = []
}

onScopeDispose(() => {
stream?.close()
stream = null
syncing.value = false
})

return { syncing, syncLog, logEl, startSync, clearLog }
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ MCP server 的普通字段支持 `${ENV_VAR}` 与 `${ENV_VAR:-default}`;认证
| `WEB_ADMIN_HOST` | 监听地址 | `127.0.0.1` |
| `WEB_ADMIN_PORT` | 监听端口 | `5104` |
| `WEB_ADMIN_SESSION_TTL_HOURS` | session 有效期(小时) | `168` |
| `WEB_ADMIN_SESSION_MAX_AGE_HOURS` | session 绝对寿命(小时),自创建起超过后必须重新登录 | `168` |
| `WEB_ADMIN_COOKIE_SECURE` | 是否下发 Secure cookie:`auto` / `true` / `false` | `auto` |

### Docker 构建相关
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ ssh -N -L 5104:127.0.0.1:5104 user@server

```env
WEB_ADMIN_SESSION_TTL_HOURS=168
WEB_ADMIN_SESSION_MAX_AGE_HOURS=168
WEB_ADMIN_COOKIE_SECURE=auto
```

Expand Down
25 changes: 22 additions & 3 deletions skills.example/self-docs/references/docs-admin-web-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ FastAPI 管理接口独立校验应用会话。内层登录带速率限制:同

- 存储位置:`data/web_admin_sessions.db`
- 介质:SQLite
- 内容:`session_id`、创建时间、过期时间、最近访问时间、客户端 IP、User-Agent
- 内容:`session_id` 的 SHA-256 哈希(明文 id 只存在于浏览器 cookie)、创建时间、过期时间、最近访问时间、客户端 IP、User-Agent

管理员登录后,浏览器通过会话 Cookie 访问后台;口令只用于登录校验。
管理员登录后,浏览器通过会话 Cookie 访问后台;口令只用于登录校验。会话随活动滑动续期(`WEB_ADMIN_SESSION_TTL_HOURS`),同时受绝对寿命限制(`WEB_ADMIN_SESSION_MAX_AGE_HOURS`):自创建起超过绝对寿命的会话会被删除并要求重新登录。

### 3. cookie 属性

Expand Down Expand Up @@ -86,13 +86,15 @@ Web Admin 使用以下环境变量:
```env
WEB_ADMIN_PASSWORD=change-this-admin-password
WEB_ADMIN_SESSION_TTL_HOURS=168
WEB_ADMIN_SESSION_MAX_AGE_HOURS=168
WEB_ADMIN_COOKIE_SECURE=auto
```

说明:

- `WEB_ADMIN_PASSWORD` 应用层登录口令,必填
- `WEB_ADMIN_SESSION_TTL_HOURS` session 续期窗口,默认 `168`
- `WEB_ADMIN_SESSION_MAX_AGE_HOURS` session 绝对寿命,默认 `168`,自创建起超过该时长必须重新登录
- `WEB_ADMIN_COOKIE_SECURE` `auto | true | false`

在根目录 `.env` 设置这些变量后,源码安装重新启动 Web 进程,容器安装重新创建 Web 服务。源码安装先在 `frontend/` 构建前端,再从项目根运行 `python web_api.py`;容器安装与变量更新步骤见[部署指南](deployment.md)。`web_api.py` 启动时读取根 `.env`,已有进程环境变量优先。
Expand All @@ -106,11 +108,27 @@ WEB_ADMIN_COOKIE_SECURE=auto
```nginx
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-For $remote_addr;
```

这样 `WEB_ADMIN_COOKIE_SECURE=auto` 才能正确判断当前请求应当下发 `Secure` cookie。

`X-Forwarded-For` 使用 `$remote_addr` 覆盖语义:后端从直连对端为内网地址的请求中取 XFF 最右一个非内网跳作为真实客户端 IP,登录限流按该 IP 计数。`$proxy_add_x_forwarded_for` 会把客户端自带的 XFF 保留在链上,覆盖语义可以让链上只有 nginx 实际看到的对端地址。

该算法假定可信代理全是内网地址。如果 nginx 前面还有公网前置代理(CDN、云负载均衡),取到的将是前置代理的出口地址而非终端客户端:限流与封禁按出口地址共享计数,误伤同出口的其他管理员;这种拓扑下应在最外层代理做限流。反过来,不要把后台直接绑定到不信任的二层网络(源码安装时显式设置 `WEB_ADMIN_HOST=0.0.0.0` 且无代理)——此时对端是内网地址而路径上没有代理追加真实来源,XFF 完全由客户端控制,限流键可被任意伪造。

外层 `auth_basic` 本身没有速率限制,建议在 nginx 站点中为 `/ops/` 位置补充请求限流,例如:

```nginx
limit_req_zone $binary_remote_addr zone=quickquip_ops:10m rate=10r/m;

location /ops/ {
limit_req zone=quickquip_ops burst=20 nodelay;
auth_basic "quickquip";
...
}
```

如果你的站点已经明确只通过 HTTPS 暴露,但暂时不方便补这些 header,也可以直接设置:

```env
Expand All @@ -126,6 +144,7 @@ WEB_ADMIN_COOKIE_SECURE=true
- 浏览器会话使用 `SameSite=Strict`
- 写操作会额外检查 `Origin` / `Referer` 是否与当前请求宿主一致
- FastAPI 独立校验应用登录会话
- 后端对全部响应统一下发安全响应头(CSP、`X-Frame-Options`、`X-Content-Type-Options`、`Referrer-Policy`)

外层站点访问控制和应用层 session 共同组成后台的纵深防御边界。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

本文供配置 API 调用方和维护者查阅。LLM 配置使用 `data/llm_config.db`,Web Admin、CLI 与 Bot 通过同一个应用服务保存草稿、建立快照和提交控制任务。人格文件、Skill 内容、群覆盖、历史记录、用量和 Trace 由各自存储维护。

接口前缀为 `/ops/api/llm-configuration`,需要现有管理员会话 Cookie `quickquip_admin_session`。写请求必须携带同源 `Origin` 或 `Referer`。请求体上限为 1 MiB,分块传输也执行此限制。完整机器契约见 [OpenAPI JSON](llm-configuration.openapi.json);运行中的管理 API 在 `/openapi.json` 提供同一字段约束。
接口前缀为 `/ops/api/llm-configuration`,需要现有管理员会话 Cookie `quickquip_admin_session`。写请求必须携带同源 `Origin` 或 `Referer`。请求体上限为 1 MiB,分块传输也执行此限制。完整机器契约见 [OpenAPI JSON](llm-configuration.openapi.json),由 `python scripts/ci/export_llm_configuration_openapi.py` 在进程内构造应用导出,随约束或路由变更同步提交;运行中的管理 API 不提供 `/docs`、`/redoc`、`/openapi.json` 文档端点。

前端设计与操作流程见[前端交付契约](llm-configuration-frontend.md),其中说明中文字段含义、参数继承控件、保存与应用、冲突处理及设计验收场景。

Expand Down
2 changes: 1 addition & 1 deletion skills.example/self-docs/references/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
- `docs/dev/branching.md` → `references/docs-dev-branching.md` — QuickQuip 开发工作流与发布流程 | 关键词:dev、main、style.md、testing.md、documenting.md、architecture.md、versioning.md、feat、fix、refactor
- `docs/dev/documenting.md` → `references/docs-dev-documenting.md` — QuickQuip 公开文档规范 | 关键词:docs/、README.md、docs/user/、docs/admin/、docs/dev/、CHANGELOG.md、ROADMAP.md、../index.md、prod.example/、.env
- `docs/dev/game-framework.md` → `references/docs-dev-game-framework.md` — 游戏框架开发者指南 | 关键词:BaseGame、aliases、src/quickquip/app/message_pipeline.py、GameResult、finished、group_id、OrderedDict[str, Session]、src/quickquip/adapters/nonebot/commands.py、command_parts/、_ensure_schema()
- `docs/dev/llm-configuration-api.md` → `references/docs-dev-llm-configuration-api.md` — LLM 配置后端接口 | 关键词:data/llm_config.db、/ops/api/llm-configuration、quickquip_admin_session、Origin、Referer、/openapi.json、format_version、。顶层字段为、style_profiles、pricing
- `docs/dev/llm-configuration-api.md` → `references/docs-dev-llm-configuration-api.md` — LLM 配置后端接口 | 关键词:data/llm_config.db、/ops/api/llm-configuration、quickquip_admin_session、Origin、Referer、在进程内构造应用导出,随约束或路由变更同步提交;运行中的管理 API 不提供、/redoc、/openapi.json、format_version、。顶层字段为
- `docs/dev/llm-configuration-frontend.md` → `references/docs-dev-llm-configuration-frontend.md` — LLM 配置管理前端交付契约 | 关键词:/ops/#/llm-config、draft_revision、published_revision、loaded_revision、draft_version、published_version、loaded_version、{saved_at, note}、PUT /note、202
- `docs/dev/llm-module.md` → `references/docs-dev-llm-module.md` — QuickQuip LLM 模块说明 | 关键词:/search、LLM_TRACE_FLAG_FILE、data/llm_trace.db、run_tool_call_loop、trigger_kind、group_direct、private_direct、group_passive、interrupted、request_cancelled
- `docs/dev/mcp-integration.md` → `references/docs-dev-mcp-integration.md` — QuickQuip MCP 集成说明 | 关键词:ToolRegistry、data/llm_config.db、settings.mcp、config.toml、search_web、tavily_search、tavily_crawl、tavily_research、docker、/var/run/docker.sock
Expand Down
Loading
Loading