Steven
|
4b7627d70e
|
fix(knowledge): 切视图或检索时清掉批量选中,别搬走看不见的条目
CI / backend (push) Canceled after 0s
|
2026-11-07 01:57:22 +08:00 |
|
Steven
|
28eab4ae5f
|
feat(knowledge): 知识页支持刷新列表与批量移动知识
CI / backend (push) Canceled after 0s
|
2026-11-07 01:50:37 +08:00 |
|
Steven
|
dcd77c6b35
|
feat(knowledge): 批量把知识移到目录或其它工作区
|
2026-11-07 01:50:31 +08:00 |
|
Steven
|
4e3f56af79
|
docs(roadmap): 记一下 0.3.0 发版,模拟器上把应用内升级走通了
CI / backend (push) Canceled after 0s
|
2026-11-06 12:41:59 +08:00 |
|
Steven
|
377dabcc9e
|
chore(app): 安卓 0.3.0,跟着这一轮知识库与登录的改动走
CI / backend (push) Canceled after 0s
versionCode 3 / versionName 0.3.0。这一版安卓端能用的东西比 0.2.0 多不少:
知识是备忘录(分类、标签、筛选、搜索)、知识能删、公众号链接抓成知识之后在手机上也读得到,
登录改成手机号或邮箱 + 密码(生产已经关掉短信验证码,旧包只能走验证码,登不进去)。
|
2026-11-06 12:31:25 +08:00 |
|
Steven
|
3e29ef5ee2
|
docs(roadmap): 知识标签与删除的生产实拍,附截图脚本
CI / backend (push) Canceled after 0s
|
2026-11-06 02:20:09 +08:00 |
|
Steven
|
690779c5fb
|
fix(wechat): 公众号新版页面抓不到正文,改从 cgiDataNew 里读
CI / backend (push) Canceled after 0s
用户发来 https://mp.weixin.qq.com/s/ZEOsi4R4JcmZ_mhS6qpcww,存进库的正文只有 67 个字符
("原文链接 + 一个句号")。复盘发现公众号换了渲染方式:新版页面把整篇塞进
window.cgiDataNew 这个 JS 对象由前端渲染,页面里根本没有 id="js_content";
三种 UA 拿到的字节数完全一样,也不是 UA 的问题。
- 从 JS 字符串字面量里读正文(content_noencode / desc),按 \xNN、\uNNNN 反转义,
话题标签是双重转义过的 HTML,先 unescape 再走 DOM。
- 配图字段排在文字之后,所以前后各找 2500 字符合图;剩下没用过的 mmbiz 图按
12 张上限补在末尾(图文型文章靠就近配对会漏)。
- 解析顺序:旧 DOM → 新版 JS 数据 → 通用抽取,一层层退。
实测两篇:2373 字符 + 12 张图、2772 字符 + 13 张图,作者也都取到了。
同时补上用户提的两件:
- 知识能删了:DELETE /knowledge/{id}?confirm=true,软删 + 真删检索切片,
只有作者或 owner/admin 能删;列表卡片(悬停出现)和阅读页动作行各一个入口。
删完详情也回 7001,否则收藏的链接还能打开一篇已删除的知识。
- 分类下面有标签清单了:GET /knowledge/tags 按标签数"有多少条知识用了它",
侧栏渲染成 #标签 + 条数,点一下筛选、再点取消。
|
2026-11-06 02:08:36 +08:00 |
|
Steven
|
626f83fb9c
|
feat(wechat): 微信里发一条链接,正文和配图抓下来存成知识
CI / backend (push) Canceled after 0s
公众号的文章也能抓:走 mp.weixin.qq.com 的 DOM(#js_content),
按 data-src 取图,不靠通用抽取器——它会把公众号的图全丢掉。
正文里的外链图下载进我们自己的存储(最多 12 张、单张 8 MB),
地址改成 /attachments/{id}/raw?签名:公众号那边一换图,外链就是一排红叉。
抓取入口谁都能触发,所以只允许公网地址(内网段、回环、链路本地、
保留段、解析不出的域名一律拒绝),否则就是个 SSRF 跳板。
抓不到要说人话:"这个链接没抓下来(原因)。想直接记成任务的话,
加「任务:」前缀再发一次。" 反爬验证页、SPA 空壳都走这条路。
- 新增 services/article.py:extract_url / parse / fetch / store_images / compose_body
- wechat_intake:整条消息是链接时走抓取,落知识草稿,回「已存成知识草稿:标题」
- 依赖 trafilatura==2.0.0(装法见 requirements.txt 注释:必须逐个装再 --no-deps)
- 测试 9 例:链接判定、公众号解析、验证页、通用页裁导航、SSRF 拒绝、图片本地化、失败提示
|
2026-11-06 00:54:21 +08:00 |
|
Steven
|
f6e7616756
|
fix(integrations): 「接收中」跨进程一致,不再跟着 gunicorn 进程抖动
CI / backend (push) Canceled after 0s
上一版用 `manager.polling()` 判"有没有人在收消息",那是进程内状态:请求落到持有者
回答 true、落到备用进程回答 false,页面上就会每两秒闪一次"已停止"。
改成查数据库里的 advisory lock(`pg_locks`,objsubid=2 的两段式锁),谁抢到租约所有
进程都看得见。`lease_held()` 自己开一条短会话查,不复用请求会话——接口里常在
`await session.commit()` 之后才拼返回值,那时原事务已经关了。
|
2026-11-05 23:39:33 +08:00 |
|
Steven
|
2170a0664d
|
fix(integrations): 重启后机器人要能自己接回来
CI / backend (push) Canceled after 0s
上一版把双进程抢会话修了,但生产重启后仍然不收消息:`start_all()` 是进程启动时的
查询,那一刻没有工作区上下文,`bot_channel` 的 RLS 把行全藏起来,一行都查不到,
于是"页面上显示已连接,实际上没人在拉消息"。
- 迁移 0016 补一条只读策略:`current_setting('app.bot_list') = 'on'` 时允许 SELECT
(只开 SELECT,防止顺手拿它改别人的通道);
- `start_all()` 走 `active_channel_ids()`,先设这个 GUC 再列通道;
- 补测试 `test_startup_sees_logged_in_channels`。
|
2026-11-05 23:33:44 +08:00 |
|
Steven
|
07b01db7cc
|
fix(integrations): 一个通道只让一个进程收消息,gunicorn 双进程不再抢同一条会话
CI / backend (push) Canceled after 0s
生产是 `gunicorn -w 2`,两个进程各自跑 lifespan,start_all() 会把同一个通道的
长轮询拉两遍:消息被其中一个消费、回复从另一个发出,游标还会互相覆盖。
改成 Postgres 会话级 advisory lock 做租约:抢到的进程收消息,另一个每 10 秒重试,
主进程一挂连接断、锁释放,备用进程立刻接上。页面上的"接收中"只认抢到租约的那个
(`manager.polling()`),不再把等锁的也算进去。
补测试 `test_only_one_process_holds_the_polling_lease`:
第一个抢到、第二个抢不到、第一个放手后第三个能接上。
|
2026-11-05 23:28:35 +08:00 |
|
Steven
|
1e8cd01b3d
|
fix(integrations): 扫码确认后不再 500,回话不带链接,栏目改叫「机器人」
CI / backend (push) Canceled after 0s
1. 扫码确认那一刻页面报「服务器内部错误」:`bot_channel` 的轮询口子直接
`current_setting('app.bot_channel', true)::uuid`,而事务级 set_config 提交后
会把连接上的变量还原成空串而不是 NULL,空串 cast 就 InvalidTextRepresentation。
迁移 0015 改成 nullif(...,'')::uuid,与 app_ws_id() 同一写法。
2. 微信回话只回「已保存任务:…」/「已保存知识草稿:…」,不再带站点链接
(PUBLIC_BASE_URL 是内网地址,手机上打不开)。
3. 栏目名「接入」改名「机器人」;页面标题与提示文案同步。
pytest 151 passed;ruff check/format 干净;tsc 0;vite build 通过;
web/e2e/integrations.mjs 7 项全绿。
|
2026-11-05 23:22:49 +08:00 |
|
Steven
|
8e3dc65cc6
|
fix(integrations): 上游扫码状态叫 wait,页面认的是 waiting,先对齐再翻
CI / backend (push) Canceled after 0s
|
2026-11-05 23:06:57 +08:00 |
|
Steven
|
8436c0edbf
|
feat(integrations): 微信要先扫码配对,改成 iLink 扫码登录 + 长轮询
CI / backend (push) Canceled after 0s
上一版按"webhook + URL 里带令牌"自己编了一套,用户当场指出微信必须先扫码配对。
现在照腾讯 iLink Bot 协议自己做客户端:取二维码 → 手机扫码确认 → 存加密登录态 →
后端长轮询收消息 → 用 context_token 回话。独立实现,不依赖 DSH 的运行环境。
后端
- wechat_ilink.py:get_bot_qrcode / get_qrcode_status / getupdates / sendmessage,
bot_token 用 Fernet(密钥由 JWT_SECRET 派生)加密入库,日志与接口都不回明文
- wechat_intake.py:一句话 → 任务或知识草稿,前缀识别、#标签、首行标题
- wechat_bot.py:每条通道一个轮询任务,启动时按 status='active' 恢复;
message_id 去重;errcode -14 转 expired 让用户重新扫
- api/integrations.py 重写成扫码三接口 + 停用/断开/最近消息
- 迁移 0013(登录态列)、0014(轮询入口按 app.bot_channel 主键的 RLS 口子)
Web
- 接入页改成扫码流程:页面里画出二维码、每 2 秒问一次状态、连上显示微信用户与最近消息
验证
- pytest 149 passed(新增 test_wechat_bot 9 例)
- ruff check / ruff format --check 干净;tsc 0;vite build ✓
- web/e2e/integrations.mjs 7 项 ALL PASS(拿的是微信真二维码)
教训:接入第三方协议先去已有实现里找协议,别从直觉倒推。
|
2026-11-05 23:03:18 +08:00 |
|
Steven
|
d21394a5ac
|
docs(roadmap): 补微信接入的生产实拍,记下回调地址漏前缀的坑
CI / backend (push) Canceled after 0s
|
2026-11-05 22:08:24 +08:00 |
|
Steven
|
c194609193
|
fix(integrations): 回调地址漏了 /api/v1,生产上打过去是 405
CI / backend (push) Canceled after 0s
生产把应用挂在域名根、路由带 /api/v1 前缀,request.base_url 里没有前缀,
拼出来的地址少了这一段;本地被 Vite 的代理掩盖了。改成从 base_url 截站点根、
前缀一律取 settings.api_prefix,并补一条断言把这个差异钉住。
|
2026-11-05 22:06:37 +08:00 |
|
Steven
|
90b99f8253
|
feat(integrations): 微信里发一句话,变成一条任务或知识
CI / backend (push) Canceled after 0s
一条通道一张令牌,机器人把消息 POST 到 /integrations/clawbot/{token},
正文按前缀落到任务或知识草稿:没有前缀就是任务,「知识:」「笔记:」记成知识,
「帮助」只回用法。首行当标题、其余当正文,`#标签` 摘出来挂到事项上。
同一个 msg_id 只处理一次——微信会重推,重推不能变成两条任务。
为什么不塞进 system_setting 的 JSON:一个工作区可能挂多个通道,还得能单独停用。
令牌库里只存 sha256,只在创建/重新生成时显示一次;收消息时还不知道是哪个工作区,
所以 bot_channel 的 RLS 额外放行 token_hash = current_setting('app.bot_token_hash'),
读到通道再 bind_tenant 进上下文,按通道创建者记账。
- 后端:models_integrations.py(bot_channel / bot_message)、迁移 0012(RLS)、
api/integrations.py(收消息 + 管理接口)、错误码 9002/9003
- Web:/w/:wsId/integrations「接入」页——建通道、复制回调地址与令牌、最近消息、
停用/换令牌/删除,只有 owner/admin 能改
- 测试:test_clawbot.py 10 例;web/e2e/integrations.mjs 7 项(冒充 clawbot 真推一条)
验证:pytest 150 passed,ruff check/format 全绿,tsc 0,vite build 通过,e2e 7 项全绿。
|
2026-11-05 22:04:34 +08:00 |
|
Steven
|
2c338490e5
|
fix(auth): 令牌到期自动续上,不再用半小时就被踢回登录页
CI / backend (push) Canceled after 0s
服务端一直签 access(30 分钟)和 refresh(14 天)两个令牌,但两个客户端拿到 401 都是直接清
令牌跳登录页,refresh 从来没被用过——"14 天有效"只是纸面参数,真实寿命就是 30 分钟。
- web/client.ts:请求前剩不到 90 秒先换;撞 401 先换再重放一次原请求;并发共用一次刷新;
上传附件同一条路
- android/Api.kt:call 拆 call/callOnce,2002 时用 Mutex 串行刷新后重放
- 后端 /auth/refresh 看一眼账号状态:停用的账号不能靠 refresh 一直续(2012)
- 新增 session-refresh.mjs:假 access + 真 refresh 要能留住人,两个都假才回登录页
|
2026-11-03 22:53:43 +08:00 |
|
Steven
|
02ef47e753
|
docs(roadmap): 换成放宽后的生产阅读页实拍
CI / backend (push) Canceled after 0s
|
2026-11-03 12:25:19 +08:00 |
|
Steven
|
da31083df2
|
fix(knowledge): 阅读页正文列放宽,宽屏上不再留白比字多
CI / backend (push) Canceled after 0s
1440 的屏幕上 40rem 正文只有 640px,两边各空出半屏,用户直接发截图说"留白比内容都多"。
正文列 56rem(约 55 个汉字一行)、卡片 78rem,行宽到头但不用来回扫行。
e2e 的宽度断言跟着改成 720–920px,避免以后再窄回去没人拦。
|
2026-11-03 12:23:53 +08:00 |
|
Steven
|
35fdea7260
|
test(web): e2e 与投图脚本改成密码登录,补生产的阅读页实拍
CI / backend (push) Canceled after 0s
生产短信登录已经关了,脚本里那条发码路径跑不通;统一走 PASSWORD,读不到才回落到短信。
|
2026-11-03 12:15:31 +08:00 |
|
Steven
|
d34a6ac69e
|
feat(knowledge): 阅读改成独立整页,抽屉只用来编辑
CI / backend (push) Canceled after 0s
知识原来一律在抽屉里打开,正文贴着屏幕右半边,长文读十几屏眼睛一直在右边。
阅读是「坐下来读一会儿」,编辑是「改两句就走」,两件事不该共用一个容器。
- 新增阅读页 /w/:wsId/knowledge/:itemId:正文列限到 40rem 居中,28px 标题,
更新/发布/切片行,标签与来源,动作行放编辑与发布/归档/恢复
- 列表页与搜索结果改成跳阅读页,不再弹抽屉
- KnowledgeDrawer 加 initialEditing:进来就是编辑器,保存或取消直接关
- 文案常量搬到 lib/knowledge.ts,列表页与阅读页共用一份
顺带修三个 e2e 脚本:新建后进阅读页没有「关闭」按钮可点、侧栏色点按钮让树
顺序断言多出空串、翻页脚本还在用已关闭的短信登录(改密码登录)。
|
2026-11-03 12:12:51 +08:00 |
|
Steven
|
127303b7aa
|
docs(roadmap): 记下邮箱注册的生产验收,附注册页实拍
CI / backend (push) Canceled after 0s
|
2026-11-03 01:06:59 +08:00 |
|
Steven
|
b5170ad1ce
|
feat(auth): 邮箱验证码注册,生产重新有了自助入口
CI / backend (push) Canceled after 0s
短信登录关掉后,生产没有任何注册入口——这一版补上最普通的那条路:
填邮箱、收验证码、设密码,注册完直接进首页。
先发码再注册,不是"注册时顺带发信"
POST /auth/email/code 只做三件事:查重、限流、发码。
填完密码才被告知"这个邮箱注册过了"最气人,所以占用(2019)在发码前就回。
邮箱既是账号名也是唯一联系方式,所以必须真验一次码
验证通过时间落 app_user.email_verified_at,/auth/me 回 email_verified。
"随手填一个"会让账号变成没人能找回的孤岛。
app_user.phone 改为可空(迁移 0011)
邮箱注册的账号本来就没有手机号。user.phone 对这类账号回空串,
成员列表同理,不再伪装成一串星号。
防刷
同邮箱 60 秒一次(2022)、验证码 10 分钟有效、错 5 次即作废(2021)。
6 位数可以被刮,不设上限等于把注册口子交给脚本。
本机联调开关 EMAIL_CODE_DEV_ECHO
为 true 时把码回给调用方且不发真信——否则每跑一次 e2e 就往真实收件箱塞一封。
生产打开会让 validate_runtime() 拒绝启动。
前端
新增 /register(邮箱、验证码、密码、确认、昵称;60 秒倒计时;
服务端关着注册时整页只说"联系管理员开通"),登录页按 /auth/methods
决定要不要显示「还没有账号?用邮箱注册」。
验证
pytest 139 passed、ruff check / format 全绿
tsc --noEmit 0、vite build 通过
e2e register.mjs 7 项:登录页入口 → 拿码 → 注册落进首页 → 退出用邮箱+密码登回来
→ 同一邮箱再发码回 2019 → 控制台零报错
e2e login-email-smtp.mjs 15 项回归通过(含真发一封测试信)
e2e account.mjs 14 项、knowledge.mjs 全绿
顺带修的两处测试
test_account 的字段白名单补 email_verified(接口确实多了这个字段);
test_config_prod 用 Settings(_env_file=None, ...) 构造,别被本机 .env 里的
EMAIL_CODE_DEV_ECHO=true 带偏——那个开关在生产是硬错误,本机却是跑 e2e 必需的。
|
2026-11-03 01:04:26 +08:00 |
|
Steven
|
c0c5230fd1
|
docs(roadmap): 补登录方式与系统设置的上线情况,附生产实拍
CI / backend (push) Canceled after 0s
|
2026-11-03 00:07:53 +08:00 |
|
Steven
|
742af4dd02
|
test(web): 账号页按钮改名后顺手修 e2e,别让脚本等一个不存在的按钮
|
2026-11-03 00:07:53 +08:00 |
|
Steven
|
7558637567
|
feat(auth): 短信登录默认关,换成手机号/邮箱 + 密码,发信邮箱进后台
CI / backend (push) Canceled after 0s
用户要求:内测的固定验证码 123456 等于"知道手机号就能进任何账号",先关掉;
登录改用账号密码,并多一种"邮箱当账号名"的方式;发信邮箱要能在后台改。
短信那条路(默认关):
- `SMS_LOGIN_ENABLED=false`,`/auth/sms/send` 与 `/auth/login` 统一回 2017,不是静默失败;
- 生产校验改成"开着短信登录就必须配真通道",不开时 provider 是 dev 也不拦;
- 新增 `GET /auth/methods`,登录页据此渲染,关着的路不出现(少一次"点进去才发现不能用")。
邮箱登录:
- `app_user.email` + `uq_app_user_email`(`lower(email)` 部分索引),登录与绑定都归一成小写;
- `POST /auth/login/password` 收 `phone` 或 `email`(二选一),失败统一 2013、连错 10 次锁 15 分钟;
- 绑定在 `PATCH /auth/me`:格式错 2018、被占用 2019、留空串表示解绑。
系统设置(跨租户,只有平台管理员能改):
- 新表 `system_setting`(不挂租户、无 RLS,写权限由 `deps.platform_admin` 把关);
- `GET/PUT /settings/smtp`、`POST /settings/smtp/test`,授权码只写不读,留空表示沿用旧值;
- `services/mail.py` 只用标准库 smtplib:465 走 SSL、587 走 STARTTLS,认证/拒收/网络失败都归 2020 但消息不同;
- Web 新增「系统设置」页(状态卡 + 测试发信),侧栏只对平台管理员显示入口;账号页补「邮箱」。
验证:pytest 130 passed、ruff check/format 全绿、tsc 通过、vite build 通过;
`web/e2e/login-email-smtp.mjs` 15 项全绿(含用 126 授权码真发一封测试邮件)。
顺带记下的两个坑:`platform_admin_phones` 是 list,pydantic-settings 会按 JSON 解析,
要用 `NoDecode` 交给 before-validator 拆逗号;迁移号写成 0009 会撞既有的 0009,一并改成 0010。
|
2026-11-03 00:00:11 +08:00 |
|
Steven
|
23687d6e37
|
style(web): 任务与工单列表标题也加重一档,三个列表看起来是一套
知识页的标题改成 15px/semibold 之后,任务和工单还是 text-sm font-medium,
同一个产品里扫列表时视觉层级不一致。这里统一成 `text-[15px] font-semibold leading-6`,
理由和知识页一样:标题是卡片上唯一要一眼看到的东西,不能和类型、分类、标签那排 chip 等重。
|
2026-11-02 23:59:59 +08:00 |
|
Steven
|
0660e5cbda
|
fix(web): 知识卡片标题加重一档,扫列表时先看到标题而不是一排 chip
CI / backend (push) Canceled after 0s
|
2026-11-02 23:02:25 +08:00 |
|
Steven
|
694b643683
|
test(web): 用界面把知识列表翻到底,确认「加载更多」不再 500
CI / backend (push) Canceled after 0s
|
2026-11-01 02:58:11 +08:00 |
|
Steven
|
b662429842
|
fix(knowledge): 翻页游标的字段名写错,点「加载更多」必 500
CI / backend (push) Canceled after 0s
知识列表拿游标做 keyset 分页时写成了 `cur.item_id`,而 `Cursor` 只有
`created_at` 和 `id` 两个字段——属性不存在,直接 AttributeError,
前端只看到"服务器内部错误"。
库里不足一页(默认 30 条)时永远走不到这条分支,所以一直没暴露;
旧备忘导进来 84 条之后,用户点「加载更多」就撞上了。
补一条回归用例:造 25 条、每页 10 条翻到底,断言不重不漏、末页游标为空。
|
2026-11-01 02:55:23 +08:00 |
|
Steven
|
d24be46d8c
|
docs(roadmap): PWA 的截图放进 2.Design/screens,文档里指对地方
CI / backend (push) Canceled after 0s
|
2026-10-31 04:35:49 +08:00 |
|
Steven
|
483b554c3c
|
feat(web): Web 装到桌面(PWA),带安装引导与发版提示
CI / backend (push) Canceled after 0s
用 vite-plugin-pwa 生成 manifest 与 Service Worker,不自己写缓存逻辑。
- 装到桌面:manifest 名字/主题色/三个图标(any + maskable + apple-touch-icon),
图标和页头、安卓桌面是同一枚;首页底部给安装引导,能装就弹「安装」,
iOS 给「分享 → 添加到主屏幕」,关掉记在 localStorage
- 发版提示:registerType=prompt,收到新版本只在底部提示,点「刷新」才换。
故意不自动刷新——Vditor、图表是懒加载分片,后台换 chunk 会让面板 404
- 缓存边界:只预缓存页面骨架(约 0.8MB),/api/ 一律 NetworkOnly,
离线宁可报错也不端旧数据;/vditor/ 走 CacheFirst,不拖慢首次安装
- 顺手修:UpdatePrompt 居中改用 flex 包一层,animate-slide-up 的 keyframes
自带 transform,会和 -translate-x-1/2 抢属性,手机窄屏会把卡片推出屏幕
验证:web/e2e/pwa.mjs 19 项全绿(manifest/SW 激活/预缓存/安装引导/iOS 文案/
发版提示/断网仍能打开);tsc 通过、vite build 通过。
已知限制:Service Worker 只在 https 或 localhost 注册,生产入口还是 http,
所以内网 IP 上装不了——需要域名 + 证书反代或隧道,已写进 DEPLOY.md 第 6 节。
|
2026-10-31 04:33:43 +08:00 |
|
Steven
|
370d8923c8
|
test(web): 生产核对脚本自己等节流,别把 429 当成登录坏了
|
2026-10-31 04:33:34 +08:00 |
|
Steven
|
429905892c
|
docs(roadmap): 记下账号与外壳这一轮,包括生产上抓到的 http 安全上下文 bug
CI / backend (push) Canceled after 0s
|
2026-10-31 04:17:33 +08:00 |
|
Steven
|
e3f1310cff
|
fix(web): 内网 http 下写请求与复制邀请链接都会失败
CI / backend (push) Canceled after 0s
crypto.randomUUID() 与 navigator.clipboard 只在安全上下文(https / localhost)里存在,
生产入口是纯 http,于是:
- 所有 POST/PATCH/DELETE 在拼 Idempotency-Key 时直接抛 TypeError,
登录页只显示"登录失败",GET 一切正常,所以这个坑一直没暴露
- 「复制邀请链接」永远回"浏览器不允许自动复制"
改成幂等键自己拼一个 v4(getRandomValues 不受安全上下文限制,退回 Math.random),
复制走 navigator.clipboard → document.execCommand("copy") 两级降级。
顺带说明:这是生产上"能看不能写"的真正原因,不是权限或接口问题。
|
2026-10-31 04:13:13 +08:00 |
|
Steven
|
096acb00e7
|
feat(web): 首页换成和工作区同一套外壳,补上账号设置页
CI / backend (push) Canceled after 0s
首页原来是居中窄栏,点进工作区又变全屏,进出像换了个网站。现在把首页也做成
全宽 + 左侧栏(SideRail 抽出来给首页和账号页共用),工作区卡片改成网格,
新建工作区是网格里的虚线卡片——宽度不再跳。
账号页 /account:基本信息(头像首字母、昵称、只读手机号、加入时间)、
登录密码(设置/修改 + 确认输入)、退出登录。工作区侧栏底部的用户块点进去就是它。
e2e account.mjs 14 项全绿,其中一条断言进工作区后侧栏宽度和首页差值 < 1px。
|
2026-10-31 04:05:05 +08:00 |
|
Steven
|
a5510cdb79
|
feat(auth): 手机号也能用密码登录,验证码之外多一条路
短信通道挂了人就进不来,所以给账号加第二把自助的钥匙:设过密码的用密码进,
没设过的照旧只用验证码,不强制。
- app_user.password_hash(迁移 0008 加列)+ scrypt 哈希(标准库实现,
n=2^14/r=8/p=1,存 scrypt$n$r$p$salt$hash),不引 passlib
- POST /auth/login/password:没设过密码与密码错都回 2013,不区分账号是否存在;
15 分钟内连错 10 次 → 2016,期间正确密码也挡住
- POST /auth/password:设/改共用,改要先验旧密码,新旧相同算弱密码 2015
- PATCH /auth/me 改昵称;用户输出走白名单 _user_out,密码哈希一个字节都不出去
- 登录页顶部「验证码 / 密码」两段切换
tests/test_account.py 10 项;全量 pytest 113 passed,ruff 全绿。
|
2026-10-31 04:04:55 +08:00 |
|
Steven
|
29aae12224
|
fix(app): 开完安装权限回到 App,再点那个按钮要能装上
CI / backend (push) Canceled after 0s
只做了"送去系统设置",没接"回来":用户开完开关回到对话框,
按钮还是「去开启安装权限」,而 openInstallPermission() 发现已授权就直接 return,
点了毫无反应——只能杀掉 App 重来。
现在点之前先复查 canInstall():权限开了就直接交给安装器,没开才跳设置页。
顺带补上模拟器上的全流程验证与截图(弹窗、权限提示、系统安装器、首页入口)。
|
2026-10-31 03:25:11 +08:00 |
|
Steven
|
7668799040
|
feat(app): 安卓应用内升级,安装包由后端发布
CI / backend (push) Canceled after 0s
需求是"改完 Android 端怎么送到手机上"。不发应用商店,也不想每次手动传包,
于是做了最小闭环:后端存一份安装包清单,App 启动时查、下载、交给系统安装器。
后端
- 新增 /app/release/latest 与 /app/release/download/{file},都免登录:
还没装上新包的客户端未必带着有效令牌,检查更新不该被登录挡住
- download_url 用相对路径,客户端拿自己配置的 API 地址拼——同一份清单要在
内网、公网、模拟器之间共用,写死域名只会让其中一个坏掉
- has_update 只比对 version_code,不比版本名字符串(0.10.0 与 0.9.0 的字典序是坑)
- 下载只认清单里登记的文件名,含 ..%2F 一律 9001
- scripts/publish_apk.py 负责复制 APK 并写 latest.json(sha256/size/published_at),
backend/static/apk/ 不进版本库
安卓
- AppUpdater:查清单 → 下到应用自己的 updates 目录(先写 .part 再改名)→
校验 sha256 → 经 FileProvider 交给系统安装器
- UpdateDialog:版本号、更新说明、下载进度、失败重试;「允许安装未知应用」
是系统开关,只跳设置页,不代点
- 启动时自动查一次,首页底部另有「检查更新」入口
- 图标换成和 Web 的 <Logo/> 同一套图形:墨绿圆角底、三条米白圆头线、陶土点;
同时出 mipmap 各密度 PNG、自适应图标与 512px 预览
- versionCode=2 / versionName=0.2.0;发布签名从 android/keystore.properties 读,
没有该文件时回落调试签名,保证别人 clone 下来也能 assembleRelease
文档
- API.md 第 7 节:两个接口、清单字段、签名与 sha256 的约定、9001
- DEPLOY.md 第 5 节:出包命令(JDK 17 + Gradle 8.9 + 干净的 GRADLE_USER_HOME)、
发布清单、传到服务器、验证
- ROADMAP 第 6 节记录本次范围与本机踩过的构建环境坑
验证
- 后端 pytest 103 passed;ruff check / format --check 全绿
- assembleRelease 成功,app-release.apk 7.4 MB,apksigner 验签显示 CN=Dadao Task
- 本地与生产 GET /app/release/latest?version_code=1 → has_update=true,
version_code=2 → false;下载返回 200 / application/vnd.android.package-archive
|
2026-10-31 03:15:00 +08:00 |
|
Steven
|
35fe4d92cb
|
fix(android): 知识 tab 顺序与 Web 对齐,「全部」默认选中
CI / backend (push) Canceled after 0s
- knowledgeTabs 改成 all/draft/published/archived,tab 默认 "all"。
- 空态文案按 tab 区分(全部 / 某一类 / 某个分类),不再写死"待核对"。
- 与 Web 版同一行为:进知识库先看全部,待核对是待办而不是门面。
|
2026-10-31 02:27:55 +08:00 |
|
Steven
|
50227c2114
|
feat(knowledge): 「全部」排到 tab 第一位并默认选中,正文编辑区跟着屏高走
CI / backend (push) Canceled after 0s
- 知识列表 tab 顺序改成 全部 / 待核对 / 已发布 / 已归档,默认停在「全部」:
进知识库先看"我这儿都有些什么",待核对是待办,不该当门面。
- 正文编辑器不再是写死的 240px:新增 useEditorHeight(reserve),
按 window.innerHeight 减掉标题/分类/标签/附件/底部按钮算高度
(新建留 420、编辑留 340,最低 360、最高 960,窗口 resize 时重算),
新建面板与详情编辑两处共用,长文一屏能写完一段。
- 截图脚本 shots-knowledge-view.mjs 支持 EDIT=1 出编辑态图、ALL=1 留在默认 tab;
新增 2.Design/screens/knowledge-tab-all.png、knowledge-edit-tall.png。
- 验证:tsc --noEmit 通过、vite build ✓、e2e knowledge.mjs 9 项与
knowledge-notebook.mjs 17 项全绿(含"侧栏分类按树排""已发布也有编辑")。
|
2026-10-31 02:25:25 +08:00 |
|
Steven
|
b29286778f
|
fix(knowledge): 分类侧栏按树的顺序排,已发布的知识也能直接改
CI / backend (push) Canceled after 0s
两个都是"看着不对劲"的问题:
1. 侧栏把二级分类全堆在一级分类后面(按 depth 排序的副作用),
「技术」和它的上级「我的备忘」隔着十几行,看着像一棵假树。
后端改成按树的先序返回(父分类后紧跟它的子分类,兄弟仍按 sort_order、name),
前端照 depth 缩进就是个真树;二级以下再加一个「·」标记,小屏也分得清。
2. 点开已发布的知识只有「归档」,没有改的入口——因为后端原来硬性拒绝
修改已发布内容(7003)。但"发布"是给团队看的,写错了总要能改:
现在已发布的可以直接 PATCH,改完立刻重建检索切片(chunk_count 当场更新),
状态和版本号照常推进;归档的仍然要先「恢复成草稿」再改。
Web 与安卓同步:抽屉/详情里已发布也显示「编辑」,安卓分类标签区不再锁。
验证:pytest 95 passed(新增"分类按树返回"用例、发布后可改用例)、
ruff 全绿、tsc 0、vite build ✓;`knowledge-notebook.mjs` 17 项、
`knowledge.mjs` 9 项 e2e 全绿。安卓仍未编译(JDK 25 / Gradle 8.7 不兼容)。
|
2026-10-31 02:06:49 +08:00 |
|
Steven
|
441b1b93e1
|
feat(knowledge): 点开知识先看到排好版的正文,要改再点「编辑」
CI / backend (push) Canceled after 0s
之前抽屉一打开就是 Vditor 编辑器,正文是一堆 `##` 和 `-` 的原文,
导入的 84 条旧备忘看着尤其像草稿。现在分成「看」和「写」两态:
- 默认只读:按 Markdown 渲染(markdown-it + DOMPurify),分类与标签是 chip,
附件列表只读;底部是「编辑」和状态动作,右侧显示发布于何时。
- 点「编辑」才挂编辑器;「取消」直接丢弃未保存改动(重新拉一次详情)。
- 安卓详情页同样用 Markwon 把正文排版后再显示,要改走「编辑正文」。
顺带修了 `web/e2e/knowledge.mjs` 里一条早就失效的断言:它等的是
「已发布 · 版本 N」,但界面上没有这行字(git 历史里也没有),
改成等抽屉底部的「发布于 …」。
验证:pytest 94 passed、ruff 全绿、tsc 0、vite build ✓;
`knowledge-notebook.mjs` 16 项、`knowledge.mjs` 7 项 e2e 全绿。
安卓本轮仍未编译(本机 JDK 25 与 Gradle 8.7/AGP 8.7.3 不兼容)。
|
2026-10-31 01:34:25 +08:00 |
|
Steven
|
bbd30ec85e
|
feat(knowledge): 导入脚本能指定环境与账号,生产也导了一份
CI / backend (push) Canceled after 0s
同一套备忘录要在 dev 和生产各导一次,所以把写死的东西参数化:
--api 指定目标环境、--phone 指定记到谁名下、--workspace 指定工作区名。
令牌缓存按「环境 + 手机号」分文件(.local/memo-token-<phone>-<host>.txt):
之前只按账号存,混着用会出现"刚登录完下一个请求就 401"——dev 的令牌拿不到生产的库。
生产(openclaw-01)实测:13605513683 名下「我的备忘」84 条(83 发布 + 1 归档)、
18 个分类,时间戳与 dev 一致。
|
2026-10-31 00:46:01 +08:00 |
|
Steven
|
039e5b903c
|
feat(knowledge): 旧 memos 备忘搬进知识库的导入脚本
CI / backend (push) Canceled after 0s
一次性的搬迁脚本:memo_app(MySQL)→ 知识库。放在 scripts/ 下是为了留个凭据,
说明这 84 条备忘是怎么变成知识的,以及哪些东西刻意没搬。
- 走 HTTP 接口而不是直连库:直接 INSERT 会绕过版本号、审计和切片导入,
导进去的知识搜不到——检索只认 published 且有 chunk 的文档。
- 分类照搬两层(接口上限三级),标签照搬,全部按 note 发布,归档那条保持归档。
- 正文里的图片文件在旧机器上已经找不到了,所以把  换成一行
「原图未迁移:…(旧路径 …)」,不留碎图,也不丢掉原路径。
- 按标题去重、分类复用,中断后可以重跑;token 缓存到 .local/ 避免撞上验证码节流。
- 旧库时间戳是 UTC,接口不接受客户端指定 created_at,所以额外生成一条 SQL
把 item.created_at/updated_at 与 published_at 对回原值。
实测:84 条(83 published + 1 archived)、18 个分类、语义检索可命中。
|
2026-10-31 00:26:14 +08:00 |
|
Steven
|
7c84ff3b8d
|
feat(knowledge): 知识能挂附件,新建时就能传
CI / backend (push) Canceled after 0s
知识是备忘录,备忘录不能只有字:手册、日志、导出表格都得能挂上去。
附件本来就落在 item 上(`ALLOWED_KINDS` 早含 knowledge),缺的是"新建时
还没有条目 id"这一段。
- 懒建草稿:`KnowledgeComposer` 第一次传附件(或往正文贴图)时先 `POST /knowledge`
存一条草稿拿到 id,`draftingRef` 去重防止贴图与上传同时打出两条草稿;
之后的保存走 `PATCH /knowledge/{id}`。没传过附件就取消,库里不留空条目。
- `RichTextEditor` 增 `ensureItemId`:没有 itemId 时先把草稿存下来再传图,
于是"新建时不能贴图"这条限制也没了;工具栏上传按钮按是否有落点显隐。
- 新建面板加了附件区:有草稿 id 就是正常的 `AttachmentPanel`(上传/下载/删),
没有就先给一个「添加附件」按钮,点一下建草稿。
- 安卓知识详情加附件区:列表(文件名 + 大小)、`OpenDocument` 上传任意文件、
点「打开」用浏览器看签名直链。编辑器原本只能插图,现在手册/日志也能传。
- 文档:API.md 说明附件三类事项共用同一套接口,以及懒建草稿的先后顺序;
ROADMAP 加 3.13。
验证:后端 pytest 94 passed、ruff 全绿;Web `tsc --noEmit` 通过、`vite build` 通过;
`knowledge-notebook.mjs` 15 项全绿(新增「新建面板里能上传附件」「详情抽屉里能看到
刚传的附件」「附件挂在知识上,详情回读得到」)。安卓端仍是 JDK 25 + Gradle 8.7
不兼容,本次未编译验证。
|
2026-10-30 16:18:32 +08:00 |
|
Steven
|
ffae7e380f
|
fix(knowledge): PATCH 知识时带上 doc_type 直接 500
`patch_knowledge` 把 `doc_type`/`summary` 也塞进了给 `items` 表的 UPDATE,
于是 SQLAlchemy 报 `CompileError: Unconsumed column names: doc_type`——改类型的
请求一律 500。这两个字段本来就属于 `knowledge_doc`,下面几行已有赋值,重复写
进 items 的 values 是错的。
现在 `values` 只放 items 的列(title/body_md),`doc_changed`(doc_type/summary)
单独参与"要不要推进 version"的判断,语义不变:只要这条知识的样子变了,
version 就得 +1,否则下一次保存会拿旧 version 撞 4009。
补一个用例:PATCH 同时改 title/body_md/doc_type/tag_names 应该 200,
doc_type 生效、version 前进一步。这个 bug 之前没暴露,是因为 Web 抽屉只改
标签和分类,没人用 PATCH 改过类型。
|
2026-10-30 16:18:05 +08:00 |
|
Steven
|
9a382e4e8c
|
fix(knowledge): 新建知识的正文换成富文本编辑器,不再是纯文本框
CI / backend (push) Canceled after 0s
「记一条」面板里正文是个 textarea,排版、表格、贴图都没有——创建时反而比
详情页更寒酸。现在新建面板直接用详情同款 Vditor 编辑器,创建与修改一个模样。
- `RichTextEditor` 的 `itemId` 改为可选:新建时还没有条目 id,正文照写,
只是上传按钮不摆出来;真去贴图会提示「先保存这条内容,再回来贴图」。
- `KnowledgeComposer` 的正文换用 `RichTextEditor`(minHeight 320),页脚提示
按有没有条目 id 给不同文案。
- 安卓:新建对话框不再放纯文本正文框,保存后直接进 `RichTextEditorScreen`
写正文(排版、表格、贴图都在那儿),写完回到列表并展开详情。
- e2e 增加一条断言:新建面板里的正文就是富文本编辑器(aria-label=正文 的
contenteditable),正文按真实键盘输入后 Markdown 正常落库。
验证:`tsc --noEmit` 通过、`vite build` 通过、`knowledge-notebook.mjs` 11 项
全绿(新增「新建面板的正文是富文本编辑器」)。安卓端本机仍是 JDK 25 +
Gradle 8.7/AGP 8.7.3 不兼容,本次未编译验证。
|
2026-10-30 15:52:09 +08:00 |
|
Steven
|
ed31af3f46
|
feat(knowledge): 知识改成备忘录形态,一次写全 + 三级分类,抽屉加宽
CI / backend (push) Canceled after 0s
知识以前只能"填个标题就完成",正文、标签、归类都要点进去再补。现在新建
就是一张完整的备忘录:标题、类型、分类、标签、正文一次写完,存完直接打开
详情继续编辑;知识页左侧多了工作区三级分类树,可按分类子树 / 未分类 / 标签
筛选。凡是填信息的抽屉都加宽了,还能一键展开整屏。
后端
- 新增 `knowledge_category`(迁移 0007):`workspace_id NOT NULL` + 自引用
`parent_id`、`depth` 1..3 检查约束、同层重名唯一索引(lower(name))、
RLS 策略与 `dadao_app` 授权;`knowledge_doc.category_id` 外键 ON DELETE
SET NULL。
- 新增 `/knowledge/categories` 增删改查:超三级 `7007`、同层重名 `7008`、
跨租户或不存在 `7006`;`move=true` 才改父级,成环与移动后超过三级的子树
一律拒绝,删除有知识的分类要 `force=true`(知识变未分类)。
- 知识列表支持 `category_id`(含子树)、`uncategorized`、`tag`;输出带
`category`(含 `path` / `full_path`)。
- PATCH 只改分类或标签也会推进 `version`,避免"只动标签"之后下次保存撞
乐观锁 `4009`。
Web
- `Sheet` 增 `size="md/lg/xl"` 与展开整屏按钮;任务、工单、知识抽屉统一
加宽(`TaskDetail` / `Tickets` / `Knowledge`)。
- 知识页:左侧三级分类树(新建、按子树筛选、未分类)、`KnowledgeComposer`
一次写全新的备忘录、列表卡片露出分类路径与标签、详情抽屉里可直接改分类
与标签。
- 抽屉详情只拉一次:重复的 effect 或迟到的响应不再覆盖用户正在敲的内容。
安卓
- `Api.kt`:分类模型与接口、新建/更新带 `tag_names`/`category_id`、
列表带分类与标签筛选。
- 知识页重做:新建改为近全屏大对话框(标题/类型/分类/标签/正文),列表
顶部按分类筛选,详情里能改分类与标签后单独保存。
文档与测试
- `2.Design/API.md`:5.1/5.3/5.4 补分类与筛选参数,新增 5.5 分类接口章节,
错误码补 `7006`/`7007`/`7008`。
- `2.Design/ROADMAP-M0-M1.md`:新增 3.11(三级分类)、3.12(备忘录形态)。
- 新增 `backend/tests/test_knowledge_categories.py`(8 例):备忘形态落库、
三级到底、移动越界与成环、重名、按子树/未分类/标签/关键词筛选、删分类需
`force`、跨租户隔离。
- 新增 `web/e2e/knowledge-notebook.mjs`(10 项,全绿):建两级分类 → 一次
写全 → 卡片与筛选 → 抽屉改标签/分类 → 正文落库 → 重名 7008。
验证:`pytest` 93 passed、`ruff check` / `ruff format --check` 全过、
`tsc --noEmit` 通过、`vite build` 通过、`knowledge-notebook.mjs` 10 项全绿。
安卓端本机只有 JDK 25,Gradle 8.7 / AGP 8.7.3 不支持,本次未做编译验证。
|
2026-10-30 03:51:43 +08:00 |
|
Steven
|
91b3dde670
|
feat(notify): 站内通知与手工开单,指派/改状态/评论/验收都有人收到
CI / backend (push) Canceled after 0s
M2 最后一块:让"该我办了"能出现在系统里,并且允许不经过任务直接开单。
通知(backend/app/services/notify.py 统一出口)
- 七类触发:task_assigned、task_status_changed、item_commented、
ticket_assigned、ticket_submitted、ticket_approved/rejected、task_unblocked
- 不通知自己:notify() 按 actor_id 跳过,调用方不用各自判断
- 已读:GET /notifications、/unread-count、POST /read、/read-all
手工开单
- POST /tickets:直接指定人、初始 open,与任务升级单共用同一条 kind='ticket' item,
ticket_ext.parent_item_id 为空,验收通过不回流任务
- 指派前 _ensure_member 校验在册成员,越界指派返回 1001(避免"挂在看不到的名字上")
前端
- Web:通知页 /w/:wsId/notifications(未读/全部,点开标已读并跳目标);
工单页"新建"面板;AppShell 每 30 秒对未读数,标已读后 dispatch notifications:changed 立刻刷新;
任务与工单支持 ?open= 深链落地即展开
- 安卓:五个 tab 加"通知",未读数 30 秒刷新;工单动作按需求人/办理人/管理员显隐;手工开单对话框
验证
- pytest 85 passed(新增 backend/tests/test_notifications.py 6 个用例)
- ruff check / format --check 全 0
- tsc --noEmit 通过;vite build 成功
- web/e2e/notifications.mjs 12 项断言全绿(手工开单 → B 收到 → 点开跳转 → 未读归零)
- 安卓:本机 JDK 25 与 Gradle 8.7 / AGP 8.7.3 不兼容,本次未做编译验证,
装回 JDK 17 后第一件事是 assembleDebug
|
2026-10-30 02:19:42 +08:00 |
|
Steven
|
d390b7b58a
|
feat(db): 支持接团队共享库 fnsvr-01(PG 17.6),测试库与开发库分离
CI / backend (push) Canceled after 0s
fnsvr-01 的 PostgreSQL 在 8432 端口(不是 5432),实测 PostgreSQL 17.6 / Alpine。
已在该实例建好独立库与角色:
- dadaotask(开发)/ dadaotask_test(测试),owner 都是 dadao_owner
- dadao_owner 迁移用;dadao_app 应用用(NOSUPERUSER NOBYPASSRLS)
- 两个库都已 ALTER SCHEMA public OWNER TO dadao_owner + REVOKE CREATE FROM PUBLIC + GRANT 给 dadao_app
代码改动:
- config 增加 test_database_url / test_migration_database_url,并给出
resolved_database_url() / resolved_migration_database_url():ENV=test 且有测试库时优先用测试库,
否则退回开发库(CI 里直接给 DATABASE_URL 即可,行为不变)。
- db.py / alembic/env.py / tests/conftest.py 统一走这两个解析方法。
原先 conftest 清表用的是 settings.migration_database_url,而应用连的是测试库,
一旦测试库与开发库不同,TRUNCATE 就清到另一个库上——表现为"测试库脏数据越攒越多、
列表类断言随机失败"。这是本次真踩到的坑,不是假想。
- tests/conftest.py 保留 E402 豁免(环境变量必须在 import app 模块前写好)。
验证:
- 远程 dadaotask_test 上 pytest 33 passed,跑完表内 0 行(确认清表打在同一个库)
- 远程 dadaotask 上 alembic upgrade head 成功,/health 与 /version 正常,db_role 为 dadao_app 且三项全 false
- 本机便携 PG 16.4 上同样 33 passed(两个 PG 大版本都过)
- ruff check / format 干净
已知限制:该实例没有 pgvector(只有 pg_trgm / pgcrypto),M3 向量检索前必须先定扩展来源,
已写进 2.Design/DEVELOPMENT.md。
|
2026-10-28 03:37:41 +08:00 |
|
Steven
|
b40ea77eec
|
docs(m0): 用实测结果重写 M0 清单 + 补 PR 模板
CI / backend (push) Canceled after 0s
- 0.1–0.9 全部标为完成,但不是"代码写完"就算完成:
33 条测试全绿、ruff/format 通过、真机 uvicorn 冒烟(/health /version 登录 建工作区 列表 /auth/me)通过
之后才敢打勾;没跑过的(downgrade、mypy、CI 在 Gitea 实跑、Web/Android 联调)保持未勾并写明原因。
- 记录 5 处"计划与实际不符"的偏差及理由:
JWT 30min/14d、refresh 无状态不可撤销、权限用 ROLE_RANK 而非 PERMISSIONS 字典、
app_user 故意不加 RLS(登录发生在租户绑定之前)、minio 与 pgvector 推迟到 M3。
- 新增「环境现状与阻塞」一节:Docker 起不来用便携 PG、外网 TLS 被拦走 HTTP 镜像、
pip 必须 --no-cache-dir(否则写缓存卡死)、pg_ctl restricted token 报错、fnsvr-01 只有 8418/3306。
- 验收用例表加"现状"列,区分已验证 / 部分 / 未做。
顺带修:
- alembic downgrade 之前直接失败:workspace 的 RLS 策略引用了 member,
不先 DROP POLICY 就 drop table,报 "cannot drop table member because other objects depend on it"。
现已按策略先删后删表,downgrade base → upgrade head 全通。
- npm 依赖必须 --ignore-scripts 才能装(沙箱禁止 lifecycle 脚本派生进程),
已写进 DEVELOPMENT.md;web 首次构建成功(tsc -b + vite,177KB/gzip 58KB)。
验收:pytest 33 passed / ruff 干净 / alembic 升降级往返通过 / web build 通过。
|
2026-10-28 03:28:06 +08:00 |
|
Steven
|
2b493bd9f8
|
fix(m0): 迁移可执行 + 测试全绿(33 条)
CI / backend (push) Canceled after 0s
跑通迁移与测试后修掉的真问题:
- alembic 0001:asyncpg 走预备语句,一次 execute 只能一条 SQL。
CREATE FUNCTION / CREATE POLICY / GRANT 全部拆成单语句逐条执行,
否则报 "cannot insert multiple commands into a prepared statement"。
- 创建工作区:ORM flush 只按 relationship 排序,裸外键不算数,
member 可能先于 workspace 落地撞 FK。改为先 flush workspace 再插 member。
- datetime.UTC 是 3.11+ 才有的,本机 3.10 直接 ImportError。改用 timezone.utc。
- 幂等中间件:原来会缓存 4xx。校验失败被缓存后,用户改对参数仍拿到同一个 422。
改为只缓存 <400。
- 发码节流窗口改为可配置 SMS_THROTTLE_SECONDS(测试置 0),
节流用例自己显式打开,避免"整库关掉节流"导致节流从未被验证。
- 租户隔离用例:pytest.raises(Exception) 改为精确的 ProgrammingError,
否则任何异常都会让"应用角色不能建表"这条断言假通过。
验收:pytest 33 passed;ruff check / format 通过;真机跑 uvicorn,
/health、/version、登录、建工作区、列表、/auth/me 全通。
|
2026-10-28 03:22:59 +08:00 |
|
Steven
|
0c53aa7062
|
feat(m0): 后端骨架 + Web/安卓前端骨架 + 设计文档
- backend: FastAPI 应用工厂、统一错误信封、JWT、短信登录、工作区/成员/邀请、幂等中间件
- alembic 0001: workspace/member/invitation/audit_log/app_user + RLS 策略 + 双角色 GRANT
- tests: 认证/工作区/租户隔离/幂等 四组用例(尚未在本机执行)
- web: React18+Vite 骨架(登录、工作区、邀请落地页)
- android: Compose 骨架(登录、工作区列表)
- 2.Design: PRODUCT-PLAN / ROADMAP-M0-M1 / NAMING / DEVELOPMENT
|
2026-10-28 02:38:53 +08:00 |
|