AskaEth
|
c09a21136c
|
feat(sessions): 会话列表用服务端标题 + 置顶 + 最后消息预览(A4/D5/B3)
列表此前显示的是本地硬生成的「会话 N」,因为后端 title 恒空。后端后来做了
「首条用户消息自动命名」+ PATCH 改名/置顶 + lastMessage 预览,本轮接入,
并把本地那套序号机器整块删掉。
- data:SessionSummaryDto 补 pinned / lastMessage(均带默认值,老快照仍能解);
新增 UpdateSessionRequest / Response 与 @PATCH api/v1/sessions/{id}
- core/prefs:**整块退役**本地序号机器(order / KEY_ORDER / OrderSerializer /
decodeOrder / ensureOrder / 「会话 ?」)——有了服务端标题,本地序号永远不会一致,
且 id 没有序号时会渲染出字面的「会话 ?」
- 标题规则:服务端标题非空白即原样显示,否则兜底(主会话「与昔涟」/ 次级「新会话」)。
主会话刻意保留「与昔涟」而不采用服务端自己的「主会话」
- 顺序**完全信任服务端**(pinned DESC, main DESC, created_at ASC),客户端不重排;
因此置顶的次级会排在主会话之前——顺带修正 SessionDto 里「主会话恒首位」的过时注释
- 置顶:主会话与次级都可置顶(契约允许);先就地翻转拿即时反馈 → PATCH →
成功后静默重拉(服务端才是顺序权威,快照顺带修正)→ 失败回滚并报错
- 改名:长按行打开对话框(预填当前标题 + n/80 计数),不做乐观更新(服务端会 trim 后
回写,等静默重拉取权威值);**不提供「恢复自动命名」**——服务端无法表达(空白即 400)
- **刷新触发(前置条件,不是锦上添花)**:列表此前只在 VM 初始化 / 新建 / 删除时拉取,
所以自动标题与预览要重启 App 才可见。现改为**打开抽屉时静默刷新**,
且不闪「同步中…」横幅
- 顺带修同文件里两个既有缺陷:① **快照的指纹闸门其实没实现**(fingerprint 写了却从不
比较、clear() 是死代码)→ 现按 baseUrl+username 指纹过滤,并把 clear() 接到登出
(否则换账号后第一帧会闪上个账号的会话列表);已实测确认指纹与写入时逐字节一致,
不会误丢老快照。② 预览剥标签时旁白与正文会粘连(看截图发现的排版瑕疵)→
标签改替成空格再折叠空白
测试:175 全绿(153 → +22)。含真实抓取响应体的 DTO 契约测试、标题兜底与预览规则的
纯函数测试、指纹闸门测试。
|
2026-10-04 13:55:52 +08:00 |
|
AskaEth
|
f5576540b4
|
feat(auth): 接入 /auth/refresh,不再把登录口令落盘
客户端此前把明文口令存进 EncryptedSharedPreferences,用于过期后的静默重登
(当时后端没有 refresh 端点)。后端现在有 /auth/refresh + /auth/logout 了,
本轮改为存 refresh token,口令在首次成功认证后即被抹掉。
关键约束:服务端的 refresh 是**单次使用 + 原子旋转 + 无宽限期**——同一个 token
第二次使用立刻 invalid_grant;而本端并发 401 是常态(管理概览页两个请求并发、
聊天页 REST 与 WS 同时起步)。因此:
- core/auth/TokenRefresher.kt:单飞刷新 + 代际守卫(等锁期间别人已换好令牌就直接
复用,绝不用同一个 refresh token 打第二次)+ 终态记忆(被 invalid_grant 拒过的
token 记下来,一次失效里只打一次注定失败的请求;Transient 不记忆以保留重试)
- 用应用层 Interceptor 而非 OkHttp Authenticator:后者对**任何** 401 都会被调用
(含不带令牌的登录请求),且由 RetryAndFollowUp 内部循环驱动(上限 20 次跟进),
持续 401 的令牌会打出刷新风暴。应用层拦截器只在「带了令牌仍 401」时介入,且严格
重试一次
- 可恢复的 401 不再登出:Transient(网络/5xx)原样交回 401 并保留会话;只有
Unrecoverable,或「换上刚签发的令牌仍 401」,才判终态
- WS 握手 401:先尝试一次续期再重连,终态才登出
- 登出补调 POST /auth/logout(此前从不调用,服务端 grant 会白活 30 天)
- 主动 skew(60s)刷新 + 兜底重试;刷新请求走不带令牌的客户端,不会递归回本路径
迁移对用户无感、绝不强制重登:启动时若「无 refresh token 但有旧口令」,用旧路径
静默登录一次换到 refresh token,随即 commit() 抹掉口令;离线失败就下次启动再试,
且不清会话。另外鉴权过期时也走一次续期——否则去掉口令会把会话砍到 24h,比改动前
更差。
HttpLoggingInterceptor 保持 BASIC 不动:/auth/refresh 与 /auth/logout 把 refresh
token 放在**请求体**里,再高一级就会把凭据打进 logcat。
测试:153 全绿(115 → +38)。含真实抓取响应体的 DTO 契约测试、TokenRefresher 的
并发/代际/终态单测、bootstrap 迁移单测,以及走真实 OkHttp + Retrofit + 回环
HttpServer 的端到端测试(证明只重试一次)。未新增任何依赖。
未验证项:主动/被动续期路径本身没能真机触发(唯一输入是加密存储里的 expires_at,
shell 写不进去;而 tokenTTL 是 24h 常量),仅由回环端到端测试覆盖。
|
2026-10-04 13:34:53 +08:00 |
|
AskaEth
|
09a3222b7b
|
feat(enums): 接入 /meta/enums 权威枚举表,删掉客户端硬编码映射
客户端此前自带 platform / mood / mode 三张硬编码标签表,会与服务端漂移——
实测已经漂了:客户端把 `web` 显示成「Web」,权威表里是「网页」。
- data:MetaApi(GET /api/v1/meta/enums)+ MetaDto + MetaRepository,
经 ApiClientFactory 创建以复用既有 token / 401 处理
- domain/enums/EnumCatalog.kt:标签查找器,**不含任何硬编码兜底表**;
不在表内的取值一律原样返回(api-client §1.1 的未知值回落红线);
platformLabelOrNull 供 deviceId 前缀这类「认识/不认识要走不同回落」的调用方用;
空 id 或空 label 一律不入表(否则角标会显示空白,或查空串凭空借到别人的标签)
- core/prefs/EnumPreferences.kt:last-good 快照,baseUrl+username 指纹闸门,
换服务器/账号即作废,登出清空
- EnumCatalogViewModel + LocalEnumCatalog:在 CyreneRoot 注入(与 LocalAppPrefs 同套路),
聊天角标与管理页共读同一份
- 删除 platformLabels / personaModeLabels / moodLabels;MessageSource 与 AdminFormat
改为查表,并保留各自原有的原样回落
为什么不留硬编码兜底表:没有登录就看不到任何消息,而权威表在登录前后即会拉取,
所以 last-good 快照几乎总是存在;留着硬编码表等于把「漂移」重新引进来,而且是无声的——
旧标签和新鲜标签长得一模一样。
不转换的:trace kind/status、memory scope、thinking.mode、WS session_busy 的 mode 集合——
它们都不在 /meta/enums 内,且原本就已按原样回落。
测试:115 全绿(88 → +27:MetaDtoRealPayload 8 / EnumCatalog 7 / EnumPrefs 3 /
MessageSource 8,AdminFormatTest 改写并 +1),其中 /meta/enums 与登录响应一样
用真实抓取的响应体做夹具。
|
2026-10-04 13:19:34 +08:00 |
|
AskaEth
|
9f054aad28
|
fix(admin): 后台思考 conclusion 的 <p>/<action> 标记被原样显示
现象:后台思考页把服务端 conclusion 里的 Web 端标记当纯文本打印,真机截图里
能看到 `<p></p><p></p><p>亲爱的…</p>` 和 `<action>轻轻戳了戳你</action>` 这样的
原始标记,部分记录开头还有一串空 `<p></p>`。
为什么:backthink 的 conclusion 与聊天消息是同一套标记,而 AdminBackthinkScreen
只是把它拼进 body 字符串,从未解析。
改法:复用项目已有的 domain/parse/ChatContentParser.parse()(不新写正则),
在 domain/admin/AdminFormat.kt 加纯函数 conclusionParagraphs()——按
head → 正文 → tail 产出段落、丢弃无有效片段的空段落(这才是消掉空 <p></p> 的关键);
UI 侧新增 AdminRichTextCard,Text 正常渲染,Action/Narration 走聊天页同一套
灰斜体旁白样式(对齐 MessageBubble.AnnotationLine 的既有约定)。
AdminTextCard 行为不变(表头抽成共享的 AdminCardHeader),已回访记忆页确认无回归。
测试:新增 7 例(AdminFormatTest 13→20,用真实抓取的 conclusion 做夹具,并断言
渲染模型里不残留 <p>/</p>/<action>/< 或 > 任何一项),全套 88 例全绿。
|
2026-10-04 13:02:09 +08:00 |
|
AskaEth
|
acff289887
|
test(admin): 用真实后端响应锁定管理端 DTO 契约
原先 AdminDtoDecodingTest 用的是手写的合成 JSON,等于拿「我以为的形状」自证,
证明不了真实契约。新增 AdminDtoRealPayloadTest,把 5 个管理端点的真实响应体
(登录后从本机 10942 实抓)过一遍生产用的 Json 实例。
顺带把真实数据里几个容易翻车的形状钉死:
- trace.durationNs 必须是 Long(真实 ~1.17e9 ns,超过 Int 上限就炸)——
真实值恰好塞得进 Int,所以额外用一例 3e9 的边界值才真正证明它是 Long
- 根 span 没有 parentId 键 → null
- 未配置人格的 thinking 键完全不出现 → null(不得变成全零对象)
- persona 的 mode / memoryNamespace 是空串,而非缺键
- backthink.id 是 JSON 字符串("487"),at 是数字(Unix 毫秒)
- props / events 同一个 map 里混着数字与字符串
为什么:C-2 的 DTO 此前只经「编译通过」这一层,从未被真实响应验证过。
真实响应全部一次解出,无需改动生产代码。
测试:新增 7 例(AdminDtoRealPayloadTest),全套 81 例全绿。
|
2026-10-04 13:02:08 +08:00 |
|
AskaEth
|
a7b3452e6d
|
feat(admin): 新增 C-2 管理员只读监控(概览/人格/记忆/后台思考)
侧边栏「管理员监控」进入的独立管理区 + 独立 Scaffold,四页全部只读,
不调用任何 /admin/* 写端点。
- data:AdminApi(5 条只读 GET)+ AdminDto + AdminRepository,
经 ApiClientFactory 创建以复用既有 token / 401 处理
- domain:AdminAccess.kt 为权限真源(纯函数,便于单测);
AdminFormat.kt 把 RFC3339 与 Unix 毫秒两种时间形状分开解析
- ui:AdminHomeScreen 做成可加页的壳(加页 = AdminPage 加一条枚举),
四个子页 + 统一失败态;列表项一律 PreferenceCard + BasicComponent
- RBAC:后端契约要求客户端自行降级(api-client §2.0),按矩阵分档——
viewer 只能进概览/人格;入口禁用 + ViewModel 短路(不发请求)+
服务端 403 也渲染「无权限」态,三层 fail-closed;
任何失败都不渲染成「暂无数据」
为什么:M0–M5 + 设置页 C-1 已完成,C-2 是路线上的下一项;后端 5 个读端点
均已实现(非 501),且手机端后续要走「全功能管理」(后端工作小结 主线②,
凭据前置已完成),故管理区做成可扩展的壳而非只读死胡同。
测试:74 个单测全绿,其中新增 29 个
(AdminAccessTest 9 / AdminFormatTest 13 / AdminDtoDecodingTest 7)
|
2026-10-03 21:31:09 +08:00 |
|
AskaEth
|
9bc45dc315
|
fix(ui): PreferenceCard 补 padding import
|
2026-09-08 23:17:06 +08:00 |
|
AskaEth
|
664fb2d9ed
|
style(ui): PreferenceCard 补左右外边距 12dp
|
2026-09-08 23:16:23 +08:00 |
|
AskaEth
|
cf291b58ab
|
style(ui): 列表项加卡片容器;登出移到设置页底部;按钮改矢量图标
新增 PreferenceCard(BasicComponent 本身无背景/圆角);抽屉去掉登出、新建与删除改图标按钮;设置页返回改 ChevronBackward、底部新增登出图标。
|
2026-09-08 23:13:00 +08:00 |
|
AskaEth
|
df2a26701c
|
style(ui): 列表项改用 Miuix BasicComponent(MD3 圆角卡片)
抽屉的会话行/入口行、设置页的主题选项统一换成 BasicComponent,间距 8dp,选中态用 ✓ 标记。
|
2026-09-08 22:55:29 +08:00 |
|
AskaEth
|
00856eeaf7
|
fix(ui): 浅色模式系统栏图标改深色;返回键逐层回退
CyreneRoot 用 WindowInsetsControllerCompat 跟随主题切换系统栏明暗;ChatHomeScreen 为设置页与抽屉各加 BackHandler,避免直接回桌面。
|
2026-09-08 22:45:20 +08:00 |
|
AskaEth
|
61214a18fa
|
feat(settings): C 起步——App 设置页(主题/旁白/思维链默认展开/关于)
新增 AppSettings(DataStore)与 LocalAppPrefs;CyreneRoot 按偏好决定 Miuix 主题模式并下发偏好;设置页从侧边栏进入,返回回聊天。旁白开关与思维链默认展开已接入渲染层。
|
2026-09-08 22:41:37 +08:00 |
|
AskaEth
|
91117986e4
|
feat(chat): 改为文本选择模式,全选仅作用于当前气泡
用 SelectionContainer 包住气泡/旁白/思维链/工具卡的正文,长按可拖选;移除长按整段复制的实现与其工具文件。
|
2026-09-08 22:38:28 +08:00 |
|
AskaEth
|
33770d87a6
|
feat(chat): 回车发送 + 长按气泡复制
输入框 imeAction=Send(回车即发送);长按气泡体复制原文到剪贴板并 Toast 提示。
|
2026-09-08 22:08:52 +08:00 |
|
AskaEth
|
0ea41f5815
|
fix(chat): 补 LaunchedEffect import
|
2026-09-08 21:57:43 +08:00 |
|
AskaEth
|
5a5b02f6fb
|
feat(chat): 思维链保留 10 分钟并自动折叠;输入栏下方留白承载忙碌提示
流式结束后把思维链挂到落库消息上,10 分钟后自动清除;ThinkingCard 在 active 变 false 时自动折叠,历史思维链默认折叠。输入栏底部预留 22dp 避开屏幕圆角,顶栏副标题只留连接状态,忙碌提示移到输入栏下方(带左缩进)。
|
2026-09-08 21:56:55 +08:00 |
|
AskaEth
|
5975827aea
|
fix(chat): Column 同时传 content 与尾随 lambda 导致编译失败
|
2026-09-08 21:50:54 +08:00 |
|
AskaEth
|
7faa604fd0
|
feat(chat): M4 思维链卡片 + 工具卡 + 图片
thinking.*/tool_call.*/tool_execution.update 累积进流式状态并渲染为可折叠卡片(思维链默认展开);历史消息的 thinking(来自 REST /messages)同样展示;image 用 Coil 3 加载。
|
2026-09-08 21:48:40 +08:00 |
|
AskaEth
|
5ac0ea3c3b
|
fix(chat): 输入法弹出时输入框被顶到顶部
窗口默认 adjustUnspecified 会先做 pan,叠加 imePadding 后高度被算两次;改为 adjustResize。
|
2026-09-08 21:42:36 +08:00 |
|
AskaEth
|
5d71c6a2ab
|
fix(ui): 移除聊天页顶栏登出按钮;抽屉内容避开系统栏
登出已收进侧边栏;抽屉面板保留全高背景,内容用 systemBars inset 避开状态栏/导航栏。
|
2026-09-08 21:34:10 +08:00 |
|
AskaEth
|
e72621db94
|
feat(sessions): M5 侧边栏抽屉 + 会话列表(切换/新建/删除)
自研 MiuixDrawer(draggable+Animatable,关闭态不拦截触摸);DataStore 持久化会话快照/序号映射/当前会话;SessionsViewModel 先快照后刷新;次级会话删除二次确认,主会话禁删。
|
2026-09-08 21:29:24 +08:00 |
|
AskaEth
|
e04995a84d
|
fix(chat): 新消息自动跟随底部(微信/QQ 式)
reverseLayout 会锚定原可见项,导致新消息插到视口外。改为:滚动结束时记录是否贴底(容差 64px),最新一项变化且处于跟随状态时 animateScrollToItem(0);用户上滑查看历史时不再强制滚动。
|
2026-09-08 21:16:49 +08:00 |
|
AskaEth
|
67ed0479e4
|
feat(chat): M3 发送与流式渲染
输入框 + 发送(WS chat 上行,deviceId=voiceassist:cyrene:v1);乐观消息先上屏,message_persisted 到达后按文本归一;message.start/delta/end + stream_end 走流式增量解析,末尾带光标;发送失败回滚文本到输入框。
|
2026-09-08 21:09:37 +08:00 |
|
AskaEth
|
a8a422c4e1
|
fix(chat): 来源角标改挂到消息组末尾
角标应紧随它标注的那条消息(主人右下、昔涟左下),放在上方会显得在标注下一条。
|
2026-09-08 21:06:42 +08:00 |
|
AskaEth
|
6323f89b5b
|
fix(chat): 气泡间距 6dp;来源文案按角色区分「来自 / 发送至」
主人发的显示「来自 X」,昔涟发的显示「发送至 X」。
|
2026-09-08 21:04:32 +08:00 |
|
AskaEth
|
01b5f980a5
|
fix(chat): 来源角标移到消息组顶部;IM deviceId 显示友好平台名
角标挂底部会视觉贴到下一条消息上;微信等 IM 的 deviceId 形如 <平台名>:<配置id>:<uid>,只取平台名显示「来自微信」。
|
2026-09-08 21:03:33 +08:00 |
|
AskaEth
|
63fe9a12a6
|
feat(chat): 接入 <p> 多气泡与 <action>/<narration> 旁白渲染
chat 模式消息按 <p> 拆成多个气泡;<action>/<narration> 解析为灰斜体小字依附气泡上/下;来源角标只在最后一条气泡显示一次。非 chat 模式原样渲染。
|
2026-09-08 21:01:48 +08:00 |
|
AskaEth
|
171f6ff59a
|
fix(net): 放行明文 HTTP,修复自部署地址报「网络不可用」
Android 9+ 默认禁止 cleartext,OkHttp 抛 UnknownServiceException 被映射为无网。新增 network_security_config.xml(放行 cleartext + 信任用户 CA),并给该异常单独的提示文案。
|
2026-09-08 20:56:06 +08:00 |
|
AskaEth
|
253913ad82
|
chore: 机器特定的构建配置移出仓库
aapt2FromMavenOverride(ARM64 专用绝对路径)与 sdkDownload 移到本机 ~/.gradle/gradle.properties,仓库配置保持跨平台可用。
|
2026-09-08 20:49:00 +08:00 |
|
AskaEth
|
84de51c7b7
|
feat: 初始化 Android 客户端(M0 骨架 + M1 登录 + M2 基础模块)
M0:Gradle 9.7.1 / AGP 9.2.1 / Kotlin 2.4 / Miuix 0.9.3,minSdk 33,compileSdk 37。
M1:登录页(服务器地址可配、服务端/本地错误分流、EncryptedSharedPreferences、自动登录与静默重登、401 登出)。
M2 基础模块:消息内容解析器(<p>/<action>/<narration> + 流式增量)、WebSocket 客户端、REST 数据层与分页去重。
45 个单元测试全绿,真机验证通过(登录页 + 启动图标)。
devdocs/ 收录设计文档、前后端协同事项与细化方案。
|
2026-09-08 20:40:58 +08:00 |
|