core(§7 Phase G G2): task list 页 + /tasks/{id} 占位
- web/app.py 加 list_tasks(limit, status):PG tasks + messages count,
updated_at 降序,返回模板友好 dict。Web 与 cli.py 数据形状不一致
(CLI 用 tuple,Web 用 dict),不预付抽象,等真有 schema 同步成本
再抽。
- / 路由换成 task 列表,支持 ?status=active|completed|abandoned
filter(无效值静默降级 all)。/tasks/{task_id} 占位路由:UUID 解析
失败 → 404,DB 不存在 → 404,有效则渲 task_placeholder.html(G3 来填
消息流)。
- Linux portability:_norm_path() 显示前 replace('\','/') 把 Win
存的 backslash 归一,Win Path.resolve()-str → "D:/..." 显示;Linux
forward-slash 原路通过。Path.as_posix() 在 Linux 读 Win backslash
串时不归一,所以选 replace 而非 as_posix。
- 模板 home.html 表格(id/updated/status/mode/model/msgs/tokens/desc-dir)
+ status badge 配色(active 绿 / completed 蓝 / abandoned 灰) +
filter 表单 + 空态文案。task_placeholder.html 渲染 G3 提示。CSS
tabular-nums 数字对齐 / hover 高亮 / accent-soft note。
Smoke 18 路径全绿(in-process Starlette TestClient):3 task seed
(active/completed/abandoned)+ Win\Linux 双路径形态 → / 渲染对、
status filter 正反向、garbage status 静默 all、UUID 占位、notauuid
404、ghost UUID 404、limit 生效、/healthz 不退化。版本 0.1 → 0.2。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
91202b6172
commit
80a658eba4
26
PROGRESS.md
26
PROGRESS.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。
|
||||
|
||||
最后更新:2026-05-14(Phase G G1)
|
||||
最后更新:2026-05-14(Phase G G2)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
| 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 |
|
||||
| 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 |
|
||||
| 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill |
|
||||
| §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工;**Phase G Web UI 进行中(G1 脚手架 ✅;G2 task list 待;G3 chat 只读;G4 SSE 流式;G5 文件浏览;G6 打磨)**。下一阶 C(Executor) / D(HTTP /v1) 待。 |
|
||||
| §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工;**Phase G Web UI 进行中(G1 脚手架 ✅;G2 task list ✅;G3 chat 只读 待;G4 SSE 流式;G5 文件浏览;G6 打磨)**。下一阶 C(Executor) / D(HTTP /v1) 待。 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
- **05-14 / §7 B Step 4 task_dir 双形态**:CLI `chat --task-dir <path>` 支持用户显式指定项目目录(§7.1 task-primary + dir 副视图心智模型落地)—— 留空走默认派生 `workspace/tasks/<uuid>/`,显式走用户路径(绝对或相对 cwd,Path.resolve())。`main.py::resolve_task_id` 增 `task_dir_arg`;resume 时从 PG `tasks.task_dir` 读(`SELECT task_dir WHERE task_id=?`),空则降级默认派生。新增 `is_managed_task_dir(td, ws)` 判断是否在 `workspace/tasks/<uuid>/` 模板下,作 `_cleanup_if_empty` 保护开关 —— 用户自指定的项目目录**绝不 rmtree**(可能含用户已有文件);DB 行该删还是删。`core/export_docx.py::export_chat_to_docx` 重构:task_id 升一等参数(从 `task_dir.name` 提取改入参传入),task_dir 留空时自动从 PG 读,支持用户目录(非 UUID 命名)正常导出。cli `/export` 与 `cli.py export` 子命令均改走 `_resolve_uuid_or_prefix` + task_id 直传。Smoke 4 路径全绿:default-derived(managed=True, cleanup rmtree)/ --task-dir(managed=False, FS preserved)/ resume reads DB / export 自动 PG 查路径。
|
||||
- **05-14 / §7 B Step 6 no-subtask 校验**:`core/storage/utils.py::check_no_subtask(task_dir, user_id=SENTINEL)` —— 同 user 下查 `new LIKE existing||'/%' OR existing LIKE new||'/%'`(`task_dir != new` 过滤掉同 task_dir 同项目多对话场景)。冲突抛 `NoSubtaskError`(`ValueError` 子类),消息带冲突 task 的 UUID 前 8 位 + 它的 task_dir。**分隔符容差**:SQL 里 `replace(task_dir, :bs, '/')` 把存的 Windows `\` 在比较前归一,新值也 `replace('\\', '/')`,跨 OS / 历史数据混合分隔符不漏判;`bs` 通过 bind 参数传(绕开 SQL 字符串转义陷阱)。空 / whitespace `task_dir` 直接 return(legacy / 未绑项目)。`main.py::build_agent` 在 `resolve_task_id` 后、TaskState 构造前调,`if not resume` 单层闸 —— resume 跳过(目录改名走未来 Folder API cascade,这里只拦新建)。`cli.py` 三处 build_agent 调用现有 try/except 直接接住 NoSubtaskError 并友好打印。Smoke 全绿:同 dir 允许 / child 拒 / parent 拒 / sibling 允许 / `proj_a_other` 不误中 `proj_a`(因为用 `/%` 而非 `%`)/ 空跳过 / Win `\` 子目录拒 / 混合分隔符(`\` 存 + `/` 查)仍拒 / build_agent 端到端三分支(child raise / same pass / resume bypass)。
|
||||
- **05-14 / §7 Phase G G1 Web UI 脚手架**:新增 `web/` 包(`app.py` FastAPI 工厂 + `templates/{base,home}.html` + `static/style.css`),`cli.py web --host --port --reload` 子命令(默认 127.0.0.1:8765,本地形态 sentinel user 无 auth,Phase D 才上 OIDC)。模板用 Jinja2 + HTMX/HTMX-SSE 走 CDN(无 node 链路),`base.html` 留 `{% block nav %}` 让 G2+ 扩。**Starlette 新版 `TemplateResponse` 签名**:`(request, name, context)`,旧式塞 context 里会让 jinja 用 dict 当 cache key 报 `unhashable type`,踩过修了。requirements 加 `fastapi>=0.111 uvicorn[standard] jinja2>=3.1 python-multipart`(后者为 G5 文件上传留)。Smoke 四路径全绿(in-process via Starlette `TestClient`):`/healthz` → "ok" / `/` → 1063B(title + static link + version) / `/static/style.css` → 1624B / `/nonexistent` → 404。**Linux portability 顺手**:模板里 path 显示约定用 `Path.as_posix()`(G3+ 模板落地);SSE 响应头 G4 上时带 `X-Accel-Buffering: no`(nginx 反代友好)。
|
||||
- **05-14 / §7 Phase G G2 task list 页**:`web/app.py::list_tasks(limit, status)` 读 PG `tasks` + `messages` count(updated_at 降序),返回模板友好的 dict 列表;**不复用 `cli.py::_list_task_rows`** —— CLI 拿 tuple, Web 拿 dict,数据形状有别,等真有 schema 变更同步成本时再抽(避免预付抽象)。`/` 路由换成 task 表渲染,filter via `?status=active|completed|abandoned`(无效值静默降级为 all);`/tasks/{task_id}` 占位路由 UUID 校验 + DB 存在性校验,缺一则 404,有效则渲染 `task_placeholder.html`(G3 来填消息流)。**Linux portability 落地**:`_norm_path()` 把存的 backslash 在显示时全替成 forward slash(`Path.as_posix()` 在 Linux 读 Win backslash 串时不归一,所以直接 `replace('\\','/')`);Win Path.resolve() 存 `D:\projects\...`、Linux 存 `/home/user/...`,都能正确显示。template:`home.html` 表格(id/updated/status/mode/model/msgs/tokens/desc-dir),status 用 badge(`status-active/completed/abandoned` 配色),hover 高亮;空态文案。CSS:table 紧凑(.9rem)+ `tabular-nums` 对齐 + accent-soft placeholder note。Smoke 18 路径全绿(in-process):3 task seed(active/completed/abandoned)+ Win\Linux 双路径形态 → / 渲染对、status filter 正/反向、garbage status 静默 all、UUID 占位、notauuid 404、ghost UUID 404、limit 生效、/healthz 不退化。版本 0.1 → 0.2。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -79,10 +80,10 @@ db/migrations/env.py 61 ← §7 B Step 1
|
|||
db/migrations/versions/
|
||||
0001_initial_schema.py 125 ← §7 B Step 1
|
||||
web/__init__.py 5 ← Phase G G1
|
||||
web/app.py 40 ← Phase G G1: FastAPI 工厂
|
||||
web/app.py 133 ← Phase G G1-G2: 工厂 + list_tasks + / + /tasks/{id} 占位
|
||||
─────────────────────────────────
|
||||
Python 合计 ~3146 行
|
||||
+ web/templates/{base,home}.html ~40 行 + web/static/style.css 64 行(不计 Python 主仓库)
|
||||
Python 合计 ~3239 行
|
||||
+ web/templates/{base,home,task_placeholder}.html ~103 行 + web/static/style.css 88 行(不计 Python 主仓库)
|
||||
```
|
||||
|
||||
加 skills/ppt 脚本 ~600 行 + SKILL.md / references / config / prompts + alembic.ini,总仓库约 3500 行。
|
||||
|
|
@ -91,12 +92,11 @@ Python 合计 ~3146 行
|
|||
|
||||
## 下一步候选(性价比排序)
|
||||
|
||||
1. **§7 Phase G G2 task list 页**(~小半天)—— `/` 渲染最近 task,filter by status + 分页,链 chat 页。复用 `_list_task_rows`。
|
||||
2. **§7 Phase G G3 chat 只读页**(~小半天)—— `/tasks/{id}` 渲染 PG messages,Markdown server-side 渲染。
|
||||
3. **§7 Phase G G4 chat 发送 + SSE**(~1 天)—— `WebEventSink` 把 §7 A 的 `sink.emit` 推 text/event-stream,HTMX `sse-swap` 追加 DOM。**核心一步**。
|
||||
4. **§7 Phase G G5 文件浏览 + G6 打磨**(~半天 + 半天)—— task_dir 树 / upload / download / 错误 toast / `/done /abandon` 按钮 / `/export` 链接。
|
||||
5. **§7 C Executor + sandbox**(~2-3 天)—— Phase G 完后再做,或穿插。
|
||||
6. **Phase 6 context 三层压缩**(~1 天)—— 兜底,V4 长上下文一般用不到。
|
||||
7. **Proposal mermaid 预渲染**(~半天)—— ASCII 透传不够用时再上 `mmdc`。
|
||||
1. **§7 Phase G G3 chat 只读页**(~小半天)—— `/tasks/{id}` 渲染 PG messages,Markdown server-side 渲染(`markdown` 或 `mistune`);tool_call / tool_result 折叠展示。
|
||||
2. **§7 Phase G G4 chat 发送 + SSE**(~1 天)—— `WebEventSink` 把 §7 A 的 `sink.emit` 推 text/event-stream(响应头带 `X-Accel-Buffering: no`),HTMX `sse-swap` 追加 DOM。**核心一步**,需异步跑 `AgentLoop` —— 走 `asyncio.to_thread` 或在 BG task 启 sync runner + 队列。
|
||||
3. **§7 Phase G G5 文件浏览 + G6 打磨**(~半天 + 半天)—— task_dir 树 / upload / download / 错误 toast / `/new` `/done /abandon` 按钮 / `/export` 链接。
|
||||
4. **§7 C Executor + sandbox**(~2-3 天)—— Phase G 完后再做,或穿插。
|
||||
5. **Phase 6 context 三层压缩**(~1 天)—— 兜底,V4 长上下文一般用不到。
|
||||
6. **Proposal mermaid 预渲染**(~半天)—— ASCII 透传不够用时再上 `mmdc`。
|
||||
|
||||
> §7 B 已完工。Phase G 进行中(G1 ✅)。剩余路线:G2-G6 → C(Executor)→ D(HTTP /v1 + OIDC)→ E(CLI 双模式)→ F(deploy / billing)。
|
||||
> §7 B 已完工。Phase G 进行中(G1 ✅ G2 ✅)。剩余路线:G3-G6 → C(Executor)→ D(HTTP /v1 + OIDC)→ E(CLI 双模式)→ F(deploy / billing)。
|
||||
|
|
|
|||
4
RUN.md
4
RUN.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看 `DESIGN.md`,进度看 `PROGRESS.md`。
|
||||
|
||||
最后更新:2026-05-14(Phase G G1)
|
||||
最后更新:2026-05-14(Phase G G2)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ REPL 内命令:`/exit /reset /new /resume [last|<id>] /id /status /done /abandon
|
|||
.venv/Scripts/python.exe cli.py web --reload
|
||||
```
|
||||
|
||||
> G1 阶段只有 / + /healthz + /static;G2-G6 渐进上 task list / chat / SSE / 文件浏览。Linux:`.venv/bin/python cli.py web` 一致。
|
||||
> G1 ✅ 脚手架 + /healthz;G2 ✅ `/` task 列表 + `?status=active|completed|abandoned` filter + `/tasks/{uuid}` 占位(G3 来填消息流);G3-G6 待。task_dir 显示统一 forward-slash(Win 存 `\` 也归一)。Linux:`.venv/bin/python cli.py web` 一致。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
105
web/app.py
105
web/app.py
|
|
@ -1,36 +1,129 @@
|
|||
"""FastAPI app 工厂。G1 = 脚手架 + 占位 /;G2 起接 PG。
|
||||
"""FastAPI app 工厂。G1 脚手架 → G2 task list 接 PG → G3+ 渐进上。
|
||||
|
||||
设计:
|
||||
- 单 FastAPI 进程,模板走 Jinja2,静态走 StaticFiles
|
||||
- 模板里 path 显示一律 `Path.as_posix()`,Win / Linux 看到统一形态
|
||||
- 模板里 path 显示一律 `replace('\\', '/')`,Win / Linux 看到统一形态
|
||||
(`Path.as_posix()` 在 Linux 读 Windows backslash 串时不归一,所以直接 replace)
|
||||
- SSE 在 G4 加,响应头会带 `X-Accel-Buffering: no`(nginx 反代友好)
|
||||
- 本地形态 sentinel user 固定;Phase D 加 OIDC 之后才有真正 user 态
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from core.storage import session_scope
|
||||
from core.storage.models import Message, Task
|
||||
|
||||
WEB_ROOT = Path(__file__).resolve().parent
|
||||
TEMPLATES_DIR = WEB_ROOT / "templates"
|
||||
STATIC_DIR = WEB_ROOT / "static"
|
||||
|
||||
STATUS_FILTERS = ("active", "completed", "abandoned")
|
||||
|
||||
|
||||
def _norm_path(p: str) -> str:
|
||||
"""跨 OS 显示归一:backslash → forward slash。Win 存 `\\`、Linux 存 `/`,显示统一 `/`。"""
|
||||
return (p or "").replace("\\", "/")
|
||||
|
||||
|
||||
def list_tasks(limit: int = 50, status: Optional[str] = None) -> list[dict[str, Any]]:
|
||||
"""Tasks 列表(updated_at 降序),含 messages 计数。
|
||||
|
||||
返回 dict 列表,模板友好;cli.py 的 `_list_task_rows` 自留 tuple 形态不变
|
||||
(CLI / Web 数据形状不一定一致 — 等真有 schema 变更同步成本时再抽。)
|
||||
"""
|
||||
if status and status not in STATUS_FILTERS:
|
||||
status = None # 无效 status 静默忽略,等价 all
|
||||
with session_scope() as s:
|
||||
q = (
|
||||
select(
|
||||
Task.task_id, Task.updated_at, Task.created_at, Task.status,
|
||||
Task.mode, Task.model, Task.model_profile,
|
||||
Task.tokens_prompt, Task.tokens_completion, Task.description,
|
||||
Task.task_dir,
|
||||
)
|
||||
.order_by(Task.updated_at.desc())
|
||||
)
|
||||
if status:
|
||||
q = q.where(Task.status == status)
|
||||
rows_db = s.execute(q.limit(limit)).all()
|
||||
msg_counts = dict(s.execute(
|
||||
select(Message.task_id, func.count()).group_by(Message.task_id)
|
||||
).all())
|
||||
|
||||
result = []
|
||||
for r in rows_db:
|
||||
tid = r.task_id
|
||||
result.append({
|
||||
"task_id": str(tid),
|
||||
"task_id_short": str(tid)[:8],
|
||||
"updated_at": r.updated_at,
|
||||
"created_at": r.created_at,
|
||||
"status": r.status,
|
||||
"mode": r.mode or "",
|
||||
"model_label": r.model_profile or r.model or "",
|
||||
"tokens": (r.tokens_prompt or 0) + (r.tokens_completion or 0),
|
||||
"n_messages": msg_counts.get(tid, 0),
|
||||
"description": r.description or "",
|
||||
"task_dir": _norm_path(r.task_dir or ""),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""FastAPI 工厂。uvicorn --reload 模式需要工厂签名(factory=True)。"""
|
||||
app = FastAPI(title="zcbot web", version="0.1")
|
||||
app = FastAPI(title="zcbot web", version="0.2")
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def home(request: Request):
|
||||
# Starlette 新签名:request 升一等位置参数,context 不再带 request
|
||||
def home(request: Request, status: Optional[str] = None, limit: int = 50):
|
||||
tasks = list_tasks(limit=limit, status=status)
|
||||
return templates.TemplateResponse(
|
||||
request, "home.html", {"version": app.version}
|
||||
request, "home.html",
|
||||
{
|
||||
"version": app.version,
|
||||
"tasks": tasks,
|
||||
"status": status or "",
|
||||
"limit": limit,
|
||||
"filters": STATUS_FILTERS,
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/tasks/{task_id}", response_class=HTMLResponse)
|
||||
def task_detail(request: Request, task_id: str):
|
||||
"""G2 占位:UUID 校验 + "G3 进行中" 提示。G3 落地后替换为消息渲染。"""
|
||||
try:
|
||||
tid = UUID(task_id)
|
||||
except ValueError:
|
||||
return HTMLResponse(
|
||||
f"invalid task id: {task_id!r}", status_code=404
|
||||
)
|
||||
# 顺便 DB 里查一下 task 是否存在 — 不存在直接 404,避免 G3 上之前给假象
|
||||
with session_scope() as s:
|
||||
row = s.execute(
|
||||
select(Task.task_id, Task.description, Task.task_dir, Task.status)
|
||||
.where(Task.task_id == tid)
|
||||
).first()
|
||||
if row is None:
|
||||
return HTMLResponse(f"task not found: {tid}", status_code=404)
|
||||
return templates.TemplateResponse(
|
||||
request, "task_placeholder.html",
|
||||
{
|
||||
"task_id": str(tid),
|
||||
"task_id_short": str(tid)[:8],
|
||||
"description": row.description or "",
|
||||
"task_dir": _norm_path(row.task_dir or ""),
|
||||
"status": row.status,
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/healthz", response_class=HTMLResponse)
|
||||
|
|
|
|||
|
|
@ -52,8 +52,37 @@ small.muted, .muted { color: var(--muted); font-weight: normal; }
|
|||
padding: 1.5rem 1.25rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.5rem; margin: 0 0 .5rem; }
|
||||
h1 { font-size: 1.5rem; margin: 0 0 .5rem; display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
|
||||
h2 { font-size: 1.1rem; margin: 1.5rem 0 .5rem; }
|
||||
.lead { font-size: 1rem; color: #444; }
|
||||
.status ul { padding-left: 1.25rem; }
|
||||
.status li { margin: .25rem 0; }
|
||||
.mono { font-family: var(--mono); }
|
||||
.small { font-size: .8rem; }
|
||||
|
||||
/* page head + filters */
|
||||
.page-head { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: .5rem; }
|
||||
.filters { display: flex; gap: .5rem; align-items: center; font-size: .9rem; color: var(--muted); }
|
||||
.filters select { padding: .25em .5em; font-size: .9rem; }
|
||||
.btn { padding: .25em .75em; border: 1px solid var(--border); border-radius: 3px; color: var(--fg); background: var(--surface); }
|
||||
.btn:hover { text-decoration: none; background: #f4f4f4; }
|
||||
|
||||
/* task list table */
|
||||
.empty { padding: 2rem 0; }
|
||||
table.task-list { width: 100%; border-collapse: collapse; margin-top: .5rem; }
|
||||
table.task-list th, table.task-list td { text-align: left; padding: .5rem .65rem; border-bottom: 1px solid var(--border); vertical-align: top; }
|
||||
table.task-list th { background: #f4f4f4; font-weight: 600; color: #555; font-size: .85rem; white-space: nowrap; }
|
||||
table.task-list td { font-size: .9rem; }
|
||||
table.task-list .num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
table.task-list tr:hover { background: #fdf6f6; }
|
||||
.task-id { font-family: var(--mono); font-weight: 600; }
|
||||
.dir .desc { margin-bottom: .15em; }
|
||||
.dir .small { word-break: break-all; }
|
||||
|
||||
/* status badge */
|
||||
.status { font-size: .75rem; padding: .1em .55em; border-radius: 3px; font-weight: 500; }
|
||||
.status-active { background: #dff7e0; color: #196b3a; }
|
||||
.status-completed { background: #e0eaf7; color: #1a3d6b; }
|
||||
.status-abandoned { background: #f0f0f0; color: #777; }
|
||||
|
||||
/* placeholder pages */
|
||||
.placeholder-note { padding: 1rem 1.25rem; background: var(--accent-soft); border-left: 3px solid var(--accent); margin-top: 1rem; border-radius: 3px; }
|
||||
.placeholder-note p { margin: .25rem 0; }
|
||||
|
|
|
|||
|
|
@ -1,19 +1,60 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}zcbot · home{% endblock %}
|
||||
{% block content %}
|
||||
<h1>zcbot web <small class="muted">v{{ version }}</small></h1>
|
||||
<p class="lead">
|
||||
§7 Phase G 脚手架已就位。task list / chat / SSE 流式回复 / 文件上传下载将在 G2-G6 上线。
|
||||
</p>
|
||||
<section class="status">
|
||||
<h2>状态</h2>
|
||||
<ul>
|
||||
<li>G1 ✅ 脚手架 + <code>cli.py web</code></li>
|
||||
<li>G2 🚧 task list 页</li>
|
||||
<li>G3 🚧 chat 只读页</li>
|
||||
<li>G4 🚧 chat 发送 + SSE 流式</li>
|
||||
<li>G5 🚧 文件浏览 / 上传 / 下载</li>
|
||||
<li>G6 🚧 打磨 + export</li>
|
||||
</ul>
|
||||
</section>
|
||||
{% block title %}zcbot · tasks{% endblock %}
|
||||
{% block nav %}
|
||||
<a href="/" class="active">tasks</a>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<h1>tasks <small class="muted">最近 {{ tasks|length }} 条{% if status %} · status={{ status }}{% endif %}</small></h1>
|
||||
<form class="filters" method="get" action="/">
|
||||
<label>status:
|
||||
<select name="status" onchange="this.form.submit()">
|
||||
<option value="">all</option>
|
||||
{% for f in filters %}
|
||||
<option value="{{ f }}"{% if status == f %} selected{% endif %}>{{ f }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% if status %}<a href="/" class="btn">reset</a>{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if not tasks %}
|
||||
<p class="empty muted">
|
||||
没有 task{% if status %}(status={{ status }}){% endif %}。
|
||||
CLI 起一个:<code>cli.py chat --desc "..."</code>;Web 起 task 留到 G6。
|
||||
</p>
|
||||
{% else %}
|
||||
<table class="task-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>updated</th>
|
||||
<th>status</th>
|
||||
<th>mode</th>
|
||||
<th>model</th>
|
||||
<th class="num">msgs</th>
|
||||
<th class="num">tokens</th>
|
||||
<th>desc / dir</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in tasks %}
|
||||
<tr>
|
||||
<td><a href="/tasks/{{ t.task_id }}" class="task-id" title="{{ t.task_id }}">{{ t.task_id_short }}</a></td>
|
||||
<td class="muted">{{ t.updated_at.strftime("%m-%d %H:%M") }}</td>
|
||||
<td><span class="status status-{{ t.status }}">{{ t.status }}</span></td>
|
||||
<td>{{ t.mode }}</td>
|
||||
<td class="muted">{{ t.model_label }}</td>
|
||||
<td class="num">{{ t.n_messages }}</td>
|
||||
<td class="num">{{ t.tokens }}</td>
|
||||
<td class="dir">
|
||||
{% if t.description %}<div class="desc">{{ t.description }}</div>{% endif %}
|
||||
{% if t.task_dir %}<div class="muted small mono">{{ t.task_dir }}</div>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}zcbot · task {{ task_id_short }}{% endblock %}
|
||||
{% block nav %}
|
||||
<a href="/">tasks</a>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<h1>task <code class="mono">{{ task_id_short }}</code>
|
||||
<span class="status status-{{ status }}">{{ status }}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{% if description %}<p class="lead">{{ description }}</p>{% endif %}
|
||||
{% if task_dir %}<p class="muted mono small">{{ task_dir }}</p>{% endif %}
|
||||
|
||||
<div class="placeholder-note">
|
||||
<p><strong>G3 进行中</strong> — 这页将渲染该 task 的消息流(从 PG <code>messages</code> 表读,markdown server-side 渲染)。</p>
|
||||
<p class="muted">完整 task_id: <code class="mono">{{ task_id }}</code></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue