feat(wechat): 微信对话 task 渠道标记 + 列表置顶(channel 字段)+ bump 0.23.0
tasks 加 channel 列(web/wechat,migration 0013,server_default='web' 回填存量, 并把 description='(微信 ClawBot 对话)' 的存量 task backfill 成 wechat)。微信常驻 task 后端强制置顶(列表查询前置 case pin 表达式,跨分页稳定),前端任务名前打绿色 「微信」徽章一眼可辨。channel 仅 INSERT 写定,后续 upsert/save 不传不覆盖。 - core/storage/models.py: Task.channel 列 - db/migrations/.../0013_task_channel.py: 加列 + backfill - core/storage/utils.py: ensure_local_task_row 加 channel 参数 - web/app.py: 微信建 task 传 channel=wechat;_task_dict 透出;列表 pin 置顶 - web/static/js/chat.js: channel===wechat 打徽章 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
95857ba687
commit
85336ccb7e
|
|
@ -334,6 +334,8 @@ users(user_id uuid pk, email text null unique, password_hash text null, oidc_sub
|
||||||
|
|
||||||
tasks(task_id uuid pk, user_id fk, name text not null, working_dir text not null, skill, description,
|
tasks(task_id uuid pk, user_id fk, name text not null, working_dir text not null, skill, description,
|
||||||
status, model_profile, tokens_prompt, tokens_completion, cost_usd,
|
status, model_profile, tokens_prompt, tokens_completion, cost_usd,
|
||||||
|
channel text not null default 'web', -- web/wechat 渠道来源(0013);仅 INSERT 写定,
|
||||||
|
-- upsert/save 不传不覆盖。前端据此打徽章 + 列表强制置顶
|
||||||
run_status text not null default 'idle', -- idle/running/cancelling/error(0004 合 runs 表)
|
run_status text not null default 'idle', -- idle/running/cancelling/error(0004 合 runs 表)
|
||||||
run_error text null,
|
run_error text null,
|
||||||
created_at, updated_at);
|
created_at, updated_at);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@
|
||||||
|
|
||||||
## 已完成关键能力
|
## 已完成关键能力
|
||||||
|
|
||||||
|
### 2026-06-24 / 微信对话 task 渠道标记 + 置顶(bump 0.23.0)
|
||||||
|
|
||||||
|
- 痛点:微信常驻 task 与网页常规 task 结构相同,只能靠 description 魔法值反推;且 `created_at` 固定后随用户开新 task 越沉越深,这个「渠道收件箱」反而最难找。
|
||||||
|
- `tasks` 加 `channel` 列(`web`/`wechat`,migration 0013,`server_default='web'` 回填存量、并把 description=`(微信 ClawBot 对话)` 的存量 task backfill 成 `wechat`)。`ensure_local_task_row` 加 `channel` 参数,微信建 task 处传 `wechat`;`channel` 仅 INSERT 写定,后续 upsert/save 不传 → 不覆盖。
|
||||||
|
- `_task_dict` 透出 `channel`;列表查询排序前置 `case((channel=='wechat',0),else_=1)` pin 表达式 → 微信 task 后端强制置顶(跨分页稳定),用户选的排序对其余 task 照常生效。
|
||||||
|
- 前端 `chat.js` 任务名前打绿色「微信」徽章(`channel==='wechat'`)。文件:`core/storage/models.py`、`core/storage/utils.py`、`web/app.py`、`web/static/js/chat.js`、`db/migrations/versions/...0013_task_channel.py`。
|
||||||
|
|
||||||
### 2026-06-24 / 微信绑定 UI 并入主 SPA(bump 0.22.2)
|
### 2026-06-24 / 微信绑定 UI 并入主 SPA(bump 0.22.2)
|
||||||
|
|
||||||
- 上一版绑定页是独立 `/static/wechat_bind.html`,主界面没入口、用户找不到。
|
- 上一版绑定页是独立 `/static/wechat_bind.html`,主界面没入口、用户找不到。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.22.2"
|
__version__ = "0.23.0"
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ class Task(Base):
|
||||||
working_dir: Mapped[str] = mapped_column(Text, nullable=False)
|
working_dir: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
skill: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
skill: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
description: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||||
|
# 渠道来源(0011):web=网页端常规任务 / wechat=微信 ClawBot 常驻对话。
|
||||||
|
# 仅 INSERT 时由建 task 方写定,后续 upsert/save 不传 → 不覆盖。前端据此打徽章 + 置顶。
|
||||||
|
channel: Mapped[str] = mapped_column(Text, nullable=False, default="web", server_default="web")
|
||||||
status: Mapped[str] = mapped_column(Text, nullable=False, default="active")
|
status: Mapped[str] = mapped_column(Text, nullable=False, default="active")
|
||||||
model: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
model: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||||
model_profile: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
model_profile: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ def ensure_local_task_row(
|
||||||
model: str = "",
|
model: str = "",
|
||||||
model_profile: str = "",
|
model_profile: str = "",
|
||||||
reasoning_effort: str = "",
|
reasoning_effort: str = "",
|
||||||
|
channel: str = "web",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""占位 INSERT(ON CONFLICT DO NOTHING)—— 不覆盖已有字段。
|
"""占位 INSERT(ON CONFLICT DO NOTHING)—— 不覆盖已有字段。
|
||||||
|
|
||||||
|
|
@ -45,6 +46,7 @@ def ensure_local_task_row(
|
||||||
model=model,
|
model=model,
|
||||||
model_profile=model_profile,
|
model_profile=model_profile,
|
||||||
reasoning_effort=reasoning_effort,
|
reasoning_effort=reasoning_effort,
|
||||||
|
channel=channel,
|
||||||
)
|
)
|
||||||
.on_conflict_do_nothing(index_elements=["task_id"])
|
.on_conflict_do_nothing(index_elements=["task_id"])
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""tasks.channel 列(渠道来源:web / wechat).
|
||||||
|
|
||||||
|
Revision ID: 0013
|
||||||
|
Revises: 0012
|
||||||
|
Create Date: 2026-06-24
|
||||||
|
|
||||||
|
给 tasks 加 channel 列,标记任务来源渠道:
|
||||||
|
- web = 网页端常规任务(默认)
|
||||||
|
- wechat = 微信 ClawBot 常驻对话(每用户一条)
|
||||||
|
|
||||||
|
只加列、不动现有数据;server_default='web' 让历史行自动回填为 web。建表后把
|
||||||
|
现网已存在的微信常驻 task(description = '(微信 ClawBot 对话)')backfill 成
|
||||||
|
'wechat',让置顶 / 徽章逻辑对存量数据立即生效。
|
||||||
|
|
||||||
|
前端据 channel 给微信任务打徽章并后端强制置顶(列表查询排序前置 pin 表达式)。
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "0013"
|
||||||
|
down_revision: Union[str, None] = "0012"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"tasks",
|
||||||
|
sa.Column("channel", sa.Text(), nullable=False, server_default="web"),
|
||||||
|
)
|
||||||
|
# backfill 存量微信常驻 task —— 用建 task 时写死的 description 作标记。
|
||||||
|
op.execute(
|
||||||
|
"UPDATE tasks SET channel = 'wechat' "
|
||||||
|
"WHERE description = '(微信 ClawBot 对话)'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("tasks", "channel")
|
||||||
10
web/app.py
10
web/app.py
|
|
@ -32,7 +32,7 @@ from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
|
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import BigInteger, cast, func, select, update
|
from sqlalchemy import BigInteger, case, cast, func, select, update
|
||||||
from starlette.background import BackgroundTask
|
from starlette.background import BackgroundTask
|
||||||
|
|
||||||
from core import __version__
|
from core import __version__
|
||||||
|
|
@ -190,6 +190,7 @@ def _task_dict(
|
||||||
"working_dir": _norm_path(row.working_dir or ""),
|
"working_dir": _norm_path(row.working_dir or ""),
|
||||||
"status": row.status,
|
"status": row.status,
|
||||||
"skill": row.skill or "",
|
"skill": row.skill or "",
|
||||||
|
"channel": getattr(row, "channel", None) or "web",
|
||||||
"model": row.model or "",
|
"model": row.model or "",
|
||||||
"model_profile": row.model_profile or "",
|
"model_profile": row.model_profile or "",
|
||||||
"tokens_prompt": tokens_prompt,
|
"tokens_prompt": tokens_prompt,
|
||||||
|
|
@ -883,7 +884,7 @@ def create_app() -> FastAPI:
|
||||||
ensure_local_task_row(
|
ensure_local_task_row(
|
||||||
task_id=tid, name="微信对话", working_dir=to_db_path(fs_dir),
|
task_id=tid, name="微信对话", working_dir=to_db_path(fs_dir),
|
||||||
skill="", user_id=uid, model=model_id, model_profile=profile,
|
skill="", user_id=uid, model=model_id, model_profile=profile,
|
||||||
description="(微信 ClawBot 对话)",
|
description="(微信 ClawBot 对话)", channel="wechat",
|
||||||
)
|
)
|
||||||
await asyncio.to_thread(_wx.set_chat_task, uid, tid)
|
await asyncio.to_thread(_wx.set_chat_task, uid, tid)
|
||||||
|
|
||||||
|
|
@ -1440,9 +1441,12 @@ def create_app() -> FastAPI:
|
||||||
select(func.count()).select_from(Task).where(*conditions)
|
select(func.count()).select_from(Task).where(*conditions)
|
||||||
).scalar_one() or 0
|
).scalar_one() or 0
|
||||||
|
|
||||||
|
# 微信渠道常驻 task 后端强制置顶(恒排全局最上,跨分页稳定),
|
||||||
|
# 用户选的排序对其余 task 照常生效。
|
||||||
|
pin = case((Task.channel == "wechat", 0), else_=1).asc()
|
||||||
rows = s.execute(
|
rows = s.execute(
|
||||||
select(Task).where(*conditions)
|
select(Task).where(*conditions)
|
||||||
.order_by(*_parse_ordering(ordering))
|
.order_by(pin, *_parse_ordering(ordering))
|
||||||
.limit(page_size).offset(offset)
|
.limit(page_size).offset(offset)
|
||||||
).scalars().all()
|
).scalars().all()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,10 +124,14 @@ function renderTaskList(tasks, append = false) {
|
||||||
const desc = t.description || "";
|
const desc = t.description || "";
|
||||||
const statusLabel = statusLabels[t.status] || t.status;
|
const statusLabel = statusLabels[t.status] || t.status;
|
||||||
const rowTitle = `${taskName}\n${t.task_id}`; // hover 出全名 + 完整 id(替代 meta 里被去掉的 id8)
|
const rowTitle = `${taskName}\n${t.task_id}`; // hover 出全名 + 完整 id(替代 meta 里被去掉的 id8)
|
||||||
|
// 微信渠道任务:置顶 + 名前打绿色「微信」徽章,一眼可辨(后端已强制排最上)
|
||||||
|
const wechatTag = t.channel === "wechat"
|
||||||
|
? `<span class="badge active" title="微信 ClawBot 渠道" style="margin-right:4px;">微信</span>`
|
||||||
|
: "";
|
||||||
return `
|
return `
|
||||||
<div class="task-row${active}" data-tid="${t.task_id}" title="${escapeHtml(rowTitle)}" style="display:flex;align-items:flex-start;gap:6px;">
|
<div class="task-row${active}" data-tid="${t.task_id}" title="${escapeHtml(rowTitle)}" style="display:flex;align-items:flex-start;gap:6px;">
|
||||||
<div style="flex:1;min-width:0;">
|
<div style="flex:1;min-width:0;">
|
||||||
<div class="desc">${escapeHtml(taskName)}</div>
|
<div class="desc">${wechatTag}${escapeHtml(taskName)}</div>
|
||||||
${wdName ? `<div class="meta muted" title="${escapeHtml(t.working_dir || "")}" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">📁 ${escapeHtml(wdName)}</div>` : ""}
|
${wdName ? `<div class="meta muted" title="${escapeHtml(t.working_dir || "")}" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">📁 ${escapeHtml(wdName)}</div>` : ""}
|
||||||
${desc ? `<div class="meta muted" title="${escapeHtml(desc)}" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(desc)}</div>` : ""}
|
${desc ? `<div class="meta muted" title="${escapeHtml(desc)}" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(desc)}</div>` : ""}
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue