diff --git a/DESIGN.md b/DESIGN.md index 8cde5a6..c219d19 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -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, 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_error text null, created_at, updated_at); diff --git a/PROGRESS.md b/PROGRESS.md index cbf886f..ad34984 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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) - 上一版绑定页是独立 `/static/wechat_bind.html`,主界面没入口、用户找不到。 diff --git a/core/__init__.py b/core/__init__.py index 96a8bf8..5185426 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.22.2" +__version__ = "0.23.0" diff --git a/core/storage/models.py b/core/storage/models.py index d324922..efa0fe5 100644 --- a/core/storage/models.py +++ b/core/storage/models.py @@ -65,6 +65,9 @@ class Task(Base): working_dir: Mapped[str] = mapped_column(Text, nullable=False) skill: 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") model: Mapped[str] = mapped_column(Text, nullable=False, default="") model_profile: Mapped[str] = mapped_column(Text, nullable=False, default="") diff --git a/core/storage/utils.py b/core/storage/utils.py index d1f0057..217819b 100644 --- a/core/storage/utils.py +++ b/core/storage/utils.py @@ -25,6 +25,7 @@ def ensure_local_task_row( model: str = "", model_profile: str = "", reasoning_effort: str = "", + channel: str = "web", ) -> None: """占位 INSERT(ON CONFLICT DO NOTHING)—— 不覆盖已有字段。 @@ -45,6 +46,7 @@ def ensure_local_task_row( model=model, model_profile=model_profile, reasoning_effort=reasoning_effort, + channel=channel, ) .on_conflict_do_nothing(index_elements=["task_id"]) ) diff --git a/db/migrations/versions/20260624_1100_0013_task_channel.py b/db/migrations/versions/20260624_1100_0013_task_channel.py new file mode 100644 index 0000000..0754211 --- /dev/null +++ b/db/migrations/versions/20260624_1100_0013_task_channel.py @@ -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") diff --git a/web/app.py b/web/app.py index 3ab301c..5c49787 100644 --- a/web/app.py +++ b/web/app.py @@ -32,7 +32,7 @@ from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse 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 core import __version__ @@ -190,6 +190,7 @@ def _task_dict( "working_dir": _norm_path(row.working_dir or ""), "status": row.status, "skill": row.skill or "", + "channel": getattr(row, "channel", None) or "web", "model": row.model or "", "model_profile": row.model_profile or "", "tokens_prompt": tokens_prompt, @@ -883,7 +884,7 @@ def create_app() -> FastAPI: ensure_local_task_row( task_id=tid, name="微信对话", working_dir=to_db_path(fs_dir), 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) @@ -1440,9 +1441,12 @@ def create_app() -> FastAPI: select(func.count()).select_from(Task).where(*conditions) ).scalar_one() or 0 + # 微信渠道常驻 task 后端强制置顶(恒排全局最上,跨分页稳定), + # 用户选的排序对其余 task 照常生效。 + pin = case((Task.channel == "wechat", 0), else_=1).asc() rows = s.execute( select(Task).where(*conditions) - .order_by(*_parse_ordering(ordering)) + .order_by(pin, *_parse_ordering(ordering)) .limit(page_size).offset(offset) ).scalars().all() diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 8a1d337..c648a68 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -124,10 +124,14 @@ function renderTaskList(tasks, append = false) { const desc = t.description || ""; const statusLabel = statusLabels[t.status] || t.status; const rowTitle = `${taskName}\n${t.task_id}`; // hover 出全名 + 完整 id(替代 meta 里被去掉的 id8) + // 微信渠道任务:置顶 + 名前打绿色「微信」徽章,一眼可辨(后端已强制排最上) + const wechatTag = t.channel === "wechat" + ? `微信` + : ""; return `