Rodger-Wang
46ebf28d64
闹钟改成只在 App 内提示;通话翻译/通话录音挂前台服务;复刻档位日志提到 warn
1) 拾忆提醒不再发系统通知
改成进程内 Timer + App 内弹窗。代价必须说清楚:**App 不在前台就不会提示**,
它是「打开 App 时告诉你有什么要做」,不是系统闹钟。
- 一个 Timer 只指向队首那条,响完武装下一条;定时器最长压 30 分钟,
超过就到期重算(长定时器在移动端本来就不可靠)。
- 错过的不补弹,否则早上打开 App 会糊一串昨天的提醒。
- 仍保留 flutter_local_notifications,只用来撤老版本排进系统的那些排期
(装过老包的机器上还留着,不撤的话改口径之后仍会收到、且没法关)。
只撤自己的 id 区间,绝不 cancelAll(会干掉录音的前台通知)。
- 顺带删掉上一版加的「通知权限未开启」提示条,不再需要。
2) 通话翻译 / 通话录音 切后台继续工作
两条链路原先都没有前台服务:
- 通话翻译走 AliyunBailianE2EHelper,与 AzureAsrHelper 不是同一条路,
后者的前台服务管不到它;
- 通话录音的音频来自耳机 SPP 流,压根不经过识别插件。
没有前台服务时切后台会发生两件事且都不报错:国内 ROM(荣耀/华为尤其)
冻结或回收进程,WebSocket 断、PCM 流停摆;Android 9+ 后台拿不到麦克风。
→ AST 的 start/stopContinuousTranslation 里起停 AudioRecordingForegroundService;
另开两个 method channel 口子(startAudioForegroundService/stop…)给
通话录音用。三条链路互斥使用,暂不做引用计数,注释里写明并存时要改。
⚠️ 前台服务自带常驻通知,用户关掉通知权限后它不显示但服务照常跑——
所以第 1 条「不要系统通知」和这一条不冲突。
3) 声音复刻的档位日志提到 warn 级
[TRANS-INIT] 命中哪一档、[STS] initialize 的 voiceClone 开关,原先是 info,
release 包里看不到。而「有没有复刻」完全取决于命中的是 alibaba35 还是旧版档
(旧版模型静默忽略 enable_voice_clone),不看这两行只能靠猜。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
733367002a
闹钟真正排进系统通知:排期不再只认服务端,且当场重排
真机上「对助手说定个闹钟」完全不响,三个原因叠加,每一个都不报错:
1. MemoryReminderScheduler.reschedule 的数据源只有 MemoryService.loadUpcoming,
那是纯服务端接口。后端还没部署时它恒返回空,于是 SET_clock 把闹钟写进了
本地 reminders、拾忆页也显示出来了,却一条通知都没排。用户看到的是
「记下了但不会响」,而这跟「指令压根没落地」在表现上分不出来。
→ 加 _localSlots:未同步的本地闹钟也排进来,重复规则口径与
CalendarController._expandItem 逐条对齐。合流后必须自己再排一次序,
否则本地那批会按插入位置抢占前 N 个名额、把服务端更早的挤掉。
2. reschedule 只挂在 AppLifecycleManager 的「切回前台」上。新定的闹钟要等
下次切前台才进系统排期——「5 分钟后提醒我」当场就废了;冷启动那一轮
也根本不会排。
→ SET_clock 落地后(无论上传成败)当场重排;补传成功后也重排(来源从
本地兜底变成服务端,不重排会响两遍);CalendarController.refreshFromServer
末尾也排一次,覆盖冷启动。
3. 没有通知权限时 reschedule 直接 return,而 permissionDenied 这个标志
**没有任何 UI 在读**(注释里写了要显示提示,实际没接)。测试机上
POST_NOTIFICATIONS granted=false,所以无论如何都不会响、也没有任何解释。
→ 首页加一条常驻提示条指向权限管理页。刻意不是弹窗:申请必须由用户在
权限管理里主动发起,PermissionUtil 有 48 小时合规冷却。
文案 memNotifyDisabled 41 个语言文件里都已存在,无需补 key。
另外把指令链路的日志提到 warn 级(同上一轮恒玄那批的理由:release 的级别
门槛是 warning,写 info 等于真机上查不到)。协议层新增一行「指令帧」日志,
只在这一帧真带了 extra_info 或 command_calls 时打——它是区分
「百炼没下发」和「下发了但端侧没落地」的唯一证据。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
692b004586
AI 页闸门提示改走节流,别再连点连弹
两个现象是同一个根因:灰掉的卡片只是画成 0.45 透明度
(ModuleSquareCard 的 enabled 只管 opacity),点击照样走到 launchModule。
用户看到灰卡片会连点几下试,于是每点一下补一条一模一样的 snackbar,
Get 还会把它们排队,得逐条等超时才散得干净。
改的四处闸门,全部走 ToastThrottle + 8 秒窗口:
- 没在通话却点通话翻译/通话录音 → callRequired
- 通话中点现场录音/同传/面对面/多媒体 → inCallBusy
- 没连设备却点依赖设备的功能 → connectDevice(AI 页与翻译中转页共用一把 key,
否则两个页面来回点还是会各弹各的)
- 开发中的功能 → 按模块分 key,不同功能的提示不该互相节流掉
窗口取 8 秒而不是默认的 3 秒:普通提示(保存失败之类)用户看完就换动作了,
闸门提示的对象是「用户还会再点」的场景,3 秒等于每 3 秒补一条,一样是刷屏。
信息量只在第一条。
保留提示而不是彻底吞掉:吞掉的话用户点灰卡片零反馈,不知道为什么点不动。
第一下解释清楚,后面忽略。
ToastThrottle.show 加可选 window 参数 + gateWindow 常量。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
edc414bc61
设备管理按在线排序;恒玄日志在 release 里恢复可见、电量补查重试
设备管理页:
- 在线设备排最前,其余保持服务端顺序。用「两个桶拼起来」而不是 List.sort,
Dart 的 sort 不保证稳定,按 0/1 排会让离线设备的相对顺序每次刷新都变。
- 排序结果不会自己重算:列表的 Obx 只收集到 allDevices 一个可观察对象,
所以挂 ever(deviceStatus/connectedDeviceId/deviceMac) 在连接变化时重排,
onClose 里 dispose。
- isConnected 补上 deviceMac 比对,与 unbindDevice 里的判据对齐。
只比 connectedDeviceId 的话 iOS 上恒为 false(那是 peripheral UUID
不是 MAC),列表全显示未连接、排序也白排。
日志:
- Logger 的 console 开关原来是 !kReleaseMode。配上 release 级别 = warning,
结果是 debug/info 被级别挡掉、warn/error 被开关挡掉,release 包里一条都
到不了 logcat。为真机排查写的那套恒玄抓帧诊断,在真正拿去测的包里全是
死代码。现在 release 也开,级别门槛仍是 warning。
- 恒玄的原始帧 / 未识别回包 / 电量 / 固件版本 / 命令探测一律改 warn 级,
这样正式包里也留得住——定位新固件命令号只能靠这些第一手字节。
电量:
- 握手里只发一次 AA 09 不够(那一刻 SPP 刚建好、命令可能被丢;协议 3.1.10
这帧也可能只在状态变化时主动推)。加 3s/8s/20s 三次补查,拿到就停,
断开即取消。不做长期轮询:探测命令占着 SPP 会让录音/翻译排队。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
4c5b6f0c65
拾忆:修分类筛选无效、加自动补传与提示节流;试修 iOS 播报
真机反馈的三条。
## 1) 分类筛选点了没反应(我的 bug)
筛选条改的是 categoryFilter,但 eventsForScope **只按日期过滤,从没读过它** ——
值改了没人用。列表源头补上分类条件。
顺带:onDaySelected 里那句 `categoryFilter.value = null` 也去掉了。它是旧首页
折叠态横向条的语义(临时开关,切天清掉合理);拾忆页的筛选条是常驻的,
选了「花销」再点一天就被清空,看起来同样像按钮失灵。
## 2) 本地未同步项每点必弹,且无从同步
原来只是拦下来提示一句,没给出路,连点几下还叠一串一模一样的条。
- AssistantDirectiveService.syncPending():把所有 synced=false 的本地闹钟补传,
由拾忆页刷新(进页面/下拉/切前台)驱动。这是自愈机制——服务端没部署、断网、
或恰好那一下超时,闹钟都会先落在本地,等能连上时自动补上去,不用用户做任何事。
遇到业务错误或网络异常就 break:后面的一样会失败,别把整串请求都打一遍。
加了重入保护。
- refreshFromServer 里**先补传再拉取**。反过来的话刚补传的这次拉不到,
要等下一次刷新才显示。
- 新增 ToastThrottle:同一 key 3 秒内只弹一次(比 snackbar 默认停留略长,
保证上一条没消失就不会再来一条)。memory 的三处提示都接上。
登出时清节流状态,免得上个账号的记录影响下一个。
- 点未同步项时除了提示,**顺手触发一次补传**——用户点它说明他在意这条,
正好借这个动作试一次同步,成功了下次点就能编辑。
## 3) EMAI 回复没声音(试修,需真机验证)
日志里 PcmStreamPlayer 反复报 -11850 Operation Stopped,每来一包音频重试一次。
链路:手机这条路是「按住说话」,MicPcmStreamer 用 record 的
echoCancel/noiseSuppress,在 iOS 上插件会把 AVAudioSession 切到 voice-processing
并自己持有;松手时 _mic.stop() 只停引擎、**不还原会话**。随后回复音频到达,
AVPlayer 在那个会话上起不来。
_ensureSession 原来只在首次 configure、**从头到尾没有 setActive**。改成:
配置仍只做一次(配置本身是静态的),但**每轮 begin 都显式 setActive(true)**,
把会话按本方配置拿回来;起播失败时复位 _sessionConfigured,下一轮重新走完整
configure+setActive——只补激活救不回被改过 category/mode 的会话。
⚠️ 没动 category/mode/routing 那几个值。CLAUDE.md 记着这块「被改坏过三次」
(外放/听筒/A2DP 来回绕),这次只加激活时机,不碰路由语义。
⚠️ 这条是根据日志推断的,**未经真机验证**。
## 测试
dart analyze 零错误。flutter test 有 7 个失败,逐个核对过是既有失败:
6 个语言的 translations_test 报「与 enUS key 不一致」,实测这些语言相对 enUS
缺 14~188 个 key,**其中属于本次新增的是 0 个**;id_verify_dialog_test 同理。
改动前后失败数都是 7。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
5eeedf1013
修拾忆页看不到助手闹钟:换源时把本地那份的展示出口弄丢了
真机实测暴露的回归。设备上取到的证据:
assistant_reminders: 2 条("南山会议" 16:00、"晚餐时间" 17:00),synced=false
assistant_directives: 2 条 SET_clock,status=done
memory_items_cache: 0 条
也就是说助手完全正常:听懂了、下发了指令、指令也落地了。但用户在拾忆页
什么都看不到。
## 原因
_rebuildEvents 换成只读 MemoryService.items 之后,AssistantDirectiveService
的 reminders **全仓再没有任何消费者**(grep 可证)。后端还没部署 memory 模块,
memory_add 必然失败,于是本地那两条既进不了 MemoryService、也没有展示出口。
我上一轮的提交说明写着「迁移期双写……即使上传失败,用户至少还能在本机看到
自己刚定的闹钟」——**那句话是错的**,双写的「本地」那一半当时已经没有出口了。
## 改动
- CalendarController._rebuildEvents 补上 _localReminderEvents():把
**还没同步**的助手闹钟也投影进日历。已同步的跳过,否则 MemoryService 与
本地两边都投影会出现两条。
- 重新订阅 AssistantDirectiveService.reminders(_reminderWorker):
不订阅的话助手刚定完闹钟页面不会刷新,要退出重进才看得到。
- 「提醒 → 记忆项」的换算收敛成 AssistantReminderToMemory 扩展,
上传路径与日历投影共用同一份,删掉 service 里那份重复实现。
- 本地未同步项的 memoryId 是 0:点开编辑或勾完成时给明确提示
(memLocalOnlyHint,41 语言已补),不再静默无反应——它没有服务端 id,
改了也没处提交。
## 另一个症状:EMAI 回复没声音(未修,不在本次改动范围)
同一份日志里:
[ERROR][PcmStreamPlayer] 播放错误: PlatformException(-11850, Operation Stopped)
[ERROR][PcmStreamPlayer] 启动播放失败: (-11850) Operation Stopped
反复几十次。EMAI 的回复音频走 BailianMultimodalService.audioOut →
PcmStreamPlayer.feed,**整条链路不经过 TtsService**,也不在本次改动的文件里
(改动清单见 git diff --name-only)。-11850 是 AVFoundation 层的错误,
指向音频会话状态,需要单独排查。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
46c815eb87
修拾忆页第一次打开就报错:解包口径写错 + 红条抑制漏了 memory_list
两个都是我上一轮引入的,表现叠在一起就是「第一次打开拾忆界面抛异常」。
## 1) MemoryService 的解包口径写错(严重,后端一部署就全线失效)
DioManager.request 结尾是 `return response.data;` —— 返回的是 **data 本身**,
不是 Response。全仓既有调用方都直接下标取字段(my_devices 的 response['devices']、
bes_device_auth 的 resp['device'])。
而 MemoryService._data 写的是 `resp.data`。因为 request 的返回类型是 dynamic,
**编译与 dart analyze 都不报错**;后端还没部署时返回 null,正好走空分支,
所以本地一路绿灯。一旦服务端真的返回成功,每一次 memory 调用都会抛
NoSuchMethodError —— 也就是说这个功能在部署之后才会开始全面失效。
已改成 `if (resp is Map) return Map<String, dynamic>.from(resp);`,
并加 test/memory_service_unwrap_test.dart 把四种形状(成功有数据 / 成功空 {} /
业务错误 null / 传进来个对象)都钉住,任何人改回 .data 都会红。
## 2) 红条抑制名单漏了 memory_list
上一轮只加了 memory_today / memory_upcoming,**漏了 memory_list** —— 而它正是
打开拾忆页第一个调的接口(TodoTab.initState → refreshFromServer)。
后端没部署时网关返回 NoFindServiceHandleFunc,AuthInterceptor 就把
"[Handle Http] no found handle memory_list" 原样弹给用户。
改成按 `/api/home/memory_` 前缀整条抑制,读写两种情况都成立:
读在打开页面与切前台时自动调,页面自己有行内空态与「显示的是本地缓存」提示;
写的调用点各自弹了本地化的 memSaveFailed / memDeleteFailed / memCompleteFailed,
不挡的话会弹两条。逐个列举正是漏掉 memory_list 的原因,改前缀。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
dbf7f2403b
记忆中心文档 v0.3:标注实施状态与剩余待办
新增 §零(实施状态)与 §十三(剩余待办)。
§零 列出一~三期已落地的 18 项与对应落点,并记下实施中替你做的三个默认决策
(成本闸门选活跃度阈值而非额度、多币种一期只分行不换算、「上周」固定 ISO 周),
三条都可推翻。
§十三 分四类列剩余项:
A 外部系统操作(百炼后台补/摘工具、会议模板第四节、mcp 的 GATEWAY_TOKEN_KEY)
B 产品决策(USE_EXACT_ALARM、提醒送达率预期、实名闸门、埋点、数据导出)
C 已知偏差(会议日期用上传时间、周报 cron 是北京时间、迁移丢时区、i18n 英文占位)
D 未做(小票 OCR、后台排障页、allhelp 的 doubao 硬编码)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
fc3f747dfb
拾忆:周期报告弹窗 + 存量 DBTask 迁移
周期报告弹窗(MemoryReportSheet)
- 一次只弹最近一份未确认的。用户一个月不开 App 会攒 4 份周报 + 1 份月报,
连弹 5 次没人受得了——服务端不带参数时就只返回最近一份。
- 不可关闭(需求 4 要求用户确认),但**确认失败也放人走**:把人扣在一个不可
关闭的弹窗里比漏记一次确认糟得多,下次进来会再弹。
- summary 为空时不播报、只显示统计。文案生成失败时服务端是刻意只回写统计的
(宁可少一句话也不让报告整个没有),这里对齐那个降级。
- 统计数字全部来自 stat_json,客户端不做任何加减——同一份数据在两处各算一遍,
早晚会出现两个不同的结果。花销按币种分行,不做汇率换算。
- 排在每日安排弹窗之后:两个都要播报,同时弹会互相盖住,而「今天要做什么」
比回顾更即时。
存量 DBTask → memory_item 迁移
模块 Start 时起 goroutine 跑一次,幂等(client_key="task:<id>" + 唯一索引)。
- **不删也不改 allhelp_task**。不可逆迁移的通例:出问题时旧表是唯一回退依据。
- **失败不阻断启动**。它是补数据不是启动前置;返回 error 会让 home 退出,
而五个服务同镜像、entrypoint 会连坐全杀。
- trigger_time 用 time.Parse 保留原时区再取日期,**不先转 UTC**——
那会让 00:30+08:00 这类凌晨任务整体退到前一天(有用例守着)。
- RemindAhead 置 0:旧体系「服务端仅记录不触发」,用户从没被这些任务提醒过,
迁过来默认开提醒等于给所有人补发一堆历史提醒。
- UserEdited 置 true:迁移来的是用户资产,不该被会议重新生成之类的清理逻辑碰到。
- TaskType_Cron 按一次性迁,不猜重复规则——猜错会让用户每天被一个他没设过的
闹钟吵醒。
- 标题与描述都空、没有 uid、既无触发时间也无创建时间的一律跳过,不往新表塞垃圾。
7 个映射用例覆盖:基本字段、凌晨不退日期、五种 TaskType、三种状态、
无触发时间退回创建日期、四类该跳过的、标题回退描述。
验证:go build / go vet 全通过,comm + memory + mcp 三个包测试通过;
dart analyze 零错误,flutter build apk --debug 通过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
73eb2ef2ca
MCP 补上会话鉴权,并挂三个拾忆只读工具
## 先修鉴权:此前公网接口完全没有身份校验
authFromRequest 只把 Authorization 塞进 context,**全仓没有一处读回来,也没有
任何 JWT 校验**;三个用户工具(get_user_tasks / allhelp_task / cancel_user_task)
的 uid 都是 mcp.WithString("uid", Required()) —— 由大模型填进来的普通参数。
而 mcp 是独立进程、独立端口 7300,不经过 gateway 那套 parseToken + isInWhiteList
(那只管 /api/* 和 /web/*),又必须对百炼公网可达。所以这不是「内网接口没做鉴权」,
是公网接口完全没有鉴权:知道 uid 就能读改任意人的数据。
新增 modules/mcp/auth.go:
- 从 Authorization 解 JWT 取 uid,与 gateway/core.go 同一套口径(RegisteredClaims,
uid 在 ID 字段);兼容 "Bearer xxx" 前缀(gateway 收的是裸 token,标准 MCP
客户端会加前缀)。
- 参数里的 uid **只用于比对**,不一致直接拒。不静默改用会话 uid——调用方显然
误以为自己能指定用户,让它失败比让它以为成功了更安全。
- 解不出会话 uid 一律拒,**不回退到参数 uid**,那等于这层没做。
- TokenKey 未配置时拒绝所有用户数据类工具:配置漏了导致鉴权静默失效,
比工具不可用严重得多。TokenKey 必须与 gateway 同值,两边都读 GATEWAY_TOKEN_KEY。
存量三个工具同步改造,uid 参数保留但改为可选(兼容百炼后台已配好的工具定义)。
9 个测试守住这条线,其中三条是核心安全断言:参数 uid 与会话不符必须拒、
没有 token 必须拒而不是回退、未配 TokenKey 必须全拒。另有伪造签名、过期 token、
空 uid claim 的用例。
## 共享查询层:避免 home 与 mcp 各写一份
comm/memoryquery.go。mcp 从不 RpcCall、只裸查 MySQL,如果记忆项的过滤条件、
分类白名单、排序口径两边各写一遍,改了一边另一边不报错,只会让 EMAI 答的和
App 里显示的对不上——而且没人会发现。memory 模块的 listItems 也改走这里。
ApplyMemoryQuery 里 uid 为空时拼 "1 = 0" 而不是不加条件:少一个 uid 条件就是把
全库记忆项返回给调用方,而它有两个调用方,其中一个公网可达。
另提供 MemoryRelativeRange 把「本周/上月/最近7天」换算成日期区间给模型用——
模型对「今天几号」的认知来自对话上下文,很不可靠,让它自己算 start_date 经常
算错(SET_clock 把「下午 3:30」填成 03:30 是同一类问题)。
## 三个只读工具
get_memory_items / get_memory_stats / get_memory_report。
**只读是刻意的**:新增/修改/删除不放这里。写路径的校验、幂等(client_key)、
提醒时刻重算都在 home 的 memory 模块内,在 MCP 里复制一份必然漂移。
录入走端侧指令(tool_calls)、查询走 MCP,这个分工不变。
- 花销按币种分行返回,不做汇率换算;同时给「分」和「元」,模型念元不容易错,
留分是以免它想自己做加减时用浮点。
- get_memory_report 只返回已生成完的:还在生成中的 summary 是空的,
给出去只会让模型编一段话填空。
- 返回给模型的是精简字段,不是整行 DBMemoryItem——client_key/gen_round/extra
这些内部字段对回答问题没用,只占 token 还诱导模型去解释它们。
- date_certain=false 的项带上 date_is_inferred 标记,让模型别把推断出来的日期
当成用户确认过的安排来陈述。
- 模型填错分类时忽略该筛选当作「不限分类」,而不是整次查询失败——它多半会再填错一次。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
66463e53f6
记忆中心客户端:拾忆页接服务端 + 写操作 UI + 本地提醒 + 每日播报
dart analyze 零错误,flutter build apk --debug 通过。
零期修缮
- tabTodo 补齐 41 个语言。此前只有 11 个文件有这个 key,其中 8 个的值是
'Remin'(不是任何语言里的词,像 Reminisce 打了一半),缺 key 的 30 个经 GetX
回退到 en_US 同样显示 'Remin' —— 41 个语言里 38 个是坏的,只是不报错。
- i18n_audit.py 补「缺失 key」检查。它此前**结构上无法发现缺失的 key**:循环遍历
的是该文件已有的 key,且对 baseline 里没有的直接 continue。这正是 tabTodo 在 30
个语言里悄悄丢掉而没人发现的原因。现以 zh_cn 为基准,实测报出 7243 处历史缺失
(40 个语言只有 1 个不缺)。加了 --fail-on-missing/--allow-missing 给 CI 卡「只增
不减」,默认不开——一上来就红会让人直接把检查关掉。
- pubspec 引入 timezone + flutter_timezone。zonedSchedule 只接受 tz.TZDateTime,
没有这两个依赖,提醒功能一行都写不了。
拾忆页从纯展示改成可写(此前最大的工作量遗漏)
页面原来没有 RefreshIndicator、没有 FloatingActionButton、卡片没有 onTap,
于是服务端的 add/update/del/complete 四个写接口在客户端一个调用点都没有,
user_edited 那套「保护用户修改」的机制完全空转。本次补齐:
- MemoryEditSheet:新增/编辑弹层,按分类分流表单(统一表存储 ≠ 统一表单)
- 卡片 onTap 打开编辑、左侧勾选框标记完成(灵感不给勾选框,它没有完成一说)
- 右下角 + 新增、RefreshIndicator 下拉刷新、带引导的空态页
- date_certain=false 的项单独归到「待定日期」小节。一次会抽出 8 条没写截止时间的
待办全堆在会议当天,不分开那一格就没法看了。
数据层
- MemoryItem / MemoryRemindSlot / MemoryReport 手写镜像服务端 pb。
**每个字段都经宽松解析器**(memInt/memBool/memStr),不直接 as int:
服务端删字段或老记录缺列时,non-nullable 会让整个 fromJson 抛 TypeError,
表现是整页空白而不是少一个字段(appconfig_model 已经为此踩过坑)。
单条解析失败只跳过那一条。
- 分类保留原始字符串而不是 enum 解析。服务端加分类只改常量不发包,
客户端 switch 不到就抛等于服务端一加分类就打崩所有存量 App。
CalendarEventCategory 加了 other 作为未知兜底,筛选与卡片分流都改成值比较
而不是穷举 switch。
- MemoryService 只缓存最近 90 天。GetStorage 是把整个 box 读进内存的 JSON,
一年三千多条全量缓存会让每次启动越来越慢。
- 拉取失败保留本地缓存并标 staleFromCache,页面显示行内「当前显示的是本地缓存」,
不清空、不弹红条。
- 勾选完成是乐观更新 + 失败回滚。不回滚会让用户以为已经勾上,下次进页面又变回来。
- memory_today / memory_upcoming 加进 AuthInterceptor 的红条抑制名单:
它们在启动与前台恢复时自动调,用户什么都没点就吃一条英文内部状态词很差。
删除 mock
mockCalendarEvents() 与它引用的 44 个 demo* i18n key 一并删除(41 个文件共 484 行)。
真实数据上线后混着演示数据会让人以为系统出错。
本地提醒(MemoryReminderScheduler)
- 通知 id 占 920000 起的固定区间,**逐条 cancel,绝不 cancelAll()** —— 仓库里已有
两个各自 new 的 FlutterLocalNotificationsPlugin 实例,cancelAll 会顺手干掉正在
录音的前台通知,表现是录音悄悄失去前台身份被系统回收。
- 单独的通知通道:录音提示是常驻低优先级通道,共用会让提醒被降级成静默。
- **只查权限不申请**。申请必须走 PermissionUtil(它收敛了过审要求的 48 小时冷却),
且只允许用户在权限管理页主动发起,这里主动申请等于绕过合规规则。
无权限时降级为「打开 App 才提醒」,不阻塞。
- 精确排期失败(Android 14 起 SCHEDULE_EXACT_ALARM 默认不授予)自动退化成
inexact 再试一次——不精确的提醒也比没有强,日志会记下来。
- 拿不到系统时区时按本机偏移找等价区兜底,不落回 UTC(那会让国内用户全部偏 8 小时)。
每日播报(MemoryDailySheet)
- **不 await 在启动路径上**。冷启动已串四次网络往返,仓库刚做过相反方向的优化
(6a64c3d0 把会议模板拉取改成 unawaited)。这里是进主页后异步拉、拿到再弹。
- 排在隐私政策与强更弹窗之后,且必须在登录态确认之后。
- 通话翻译进行中一律不播报:两者共用同一个 AVAudioSession,在对方刚调好的会话上
再设一次会把通话音频路由弄乱(iOS 那条 phone_call 分支被改坏过三次)。
- 关窗必须真停:speakStream 是队列式播放,只 stop 不清队列会「关了还在念下一句」。
- 播报文案本地拼,不调 LLM。
生命周期与账号
- 切前台的刷新挂在 AppLifecycleManager(仓库已有 emai/translation/permissions
三个各自实现的 observer,本类就是收敛点),全部 unawaited。
- 登出清空记忆数据与已排提醒。不清的话换账号会看到上一个人的日程花销灵感,
提醒也会继续按上一个账号的安排响。
助手闹钟迁移
SET_clock 改为双写:本地 reminders 保留一个版本(上传失败时本机仍可用),
同时写服务端 memory_item(category=alarm)。**幂等键用指令 id** —— 本类的按 id
去重只在本进程内存里,换机/重装/重启后全失效,服务端靠 (uid, client_key)
唯一索引兜住。认不出的重复规则按单次处理,宁可只响一次也不按猜的规则天天响。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
0b0ac16b3c
记忆中心后端:memory 模块 + 会议待办抽取 + 周报月报
按设计文档一~三期的后端部分落地。全量 go build / go vet 通过,新增 22 个测试。
memory 模块(modules/memory,由 home 装载,路径 /api/home/memory_*)
11 个接口:list / today / add / update / del / complete / upcoming / stats /
getreport / confirmreport / listreports。
表由 proto + gorm tag 生成(不是手写 DDL——本仓所有业务表都走
mysql.CreateTable + AutoMigrate)。protobuf 没有 DECIMAL/DATE/DATETIME/null,
逐个落成:金额 int64 存分(与 pay/结算同口径,绝不用 double)、happen_date
string + gorm type:date、next_remind_at int64 unix 秒(0=无)。
几处刻意的选择
- 建表失败只 Errorln 不外传。lego.Run 里 Init 返回非 nil 就 Panicf,而五个服务
同镜像、entrypoint 见任一 pid 退出就全杀,一次建表失败等于全站 502。
allhelp/model.go 那段用命名返回值 err,第二张表失败时会漏出去,是反例。
- client_key 服务端保证非空。它参与 (uid, client_key) 唯一索引,而 MySQL 的唯一
索引不像放行 NULL 那样放行空串,留空会让同一用户的第二条记录直接插不进去。
用 crypto/rand 生成:这个值是去重依据,可预测就可被抢占。
- 重复项的提醒改成「就地展开」。设计文档 v0.1 写的「remind_at 服务端算好客户端
直接用」对重复项不成立——一个字段表达不了「每天」,且没人在它过去后往前推。
现在 memory_upcoming 按项自己的 tz 展开成绝对时刻列表返回,不落库。
单条项按自己的 tz 算而不跟着请求方走:「今天有哪些事」是视图问题可以跟着人走,
「明早 8 点响」是承诺问题不能。
- 花销按币种分行统计,不做汇率换算。把 CNY 和 JPY 加成一个数是错的。
- 统计全部走 SQL,LLM 只把 stat_json 组织成一段适合朗读的话。明细截断到 20 条
(重度用户一周两百条未完成,全塞进 prompt 会顶爆上下文,播报也没人听得完)。
- 查询/删除/更新一律带 uid 作条件,报告确认校验 uid 归属。
- 更新字段走白名单而不是黑名单,漏挡一个 uid/id 就是越权改别人的数据。
选路走编排,不再写死服务商
新增 comm.IEchomeet.ChatLLM,由 echomeet 用它的 PickLLM/GetSummarizer 实现。
memory 的报告生成与会议抽取都经它选模型。allhelp/summary.go 直接 import
sys/doubao 是历史债:绕过后台编排,海外应用照样调国内豆包、后台换模型也换不掉。
新模块不复制这个写法。
会议纪要 → 待办抽取(第三路)
挂在 AIProcess 写完 summary、状态置 Completed、出队之后,另起 goroutine。
prompt 内置代码不放 echomeet_template(1189 条模板改不齐,且输出要被程序 parse)。
输入用已生成的 summary 而非转写全文。注入 <meeting_date>,服务端二次校验
due_date:解析不出或早于会议当天的一律只留 due_raw、落会议当天并置
date_certain=false。抽取失败只记日志,绝不回头改 rec.State。
- gen_round 由 memory 按 source_id 现有最大值 +1 推出,不由调用方传。
echomeet 没有地方存轮次计数器,硬编码成 1 会让第二次「重新生成」的清理条件
gen_round < 1 一条都匹配不上,旧待办从此永远留在库里。
- 会议归属日期只能用 creationtime(上传时间)——DBEchoMeetRecord 上没有「会议实际
发生日期」字段,也没有客户端时区。补录历史录音会算错,是已知偏差,注释已记。
周报月报
cron 建单 → Redis 队列 → N worker。锁与队列 key 都过 redissys.RKey 加应用前缀:
测试机多应用共用一个 Redis,锁 key 不带前缀会让 A 应用挡住 B 应用整周不生成报告,
而且安安静静什么都不报。
补偿比防重更要紧:当前 compose 是单副本,多副本重复是前瞻性风险;真会发生的是
00:30 跑到一半容器重启、cron 不补跑、报告永远停在 pending。所以 memory_getreport
里做超时重新入队(同 echomeet 用 PollTranscribe 兜「转写回调丢了」的思路)。
文案生成失败不算整体失败——统计是完整的,宁可少一句话不可让报告整个没有。
顺带修的既有缺陷
- user 模块 cancel()(账号注销)补上 memory_item / memory_report 两张表,
以及既有遗漏的 useridverify(身份证掩码 + 加盐指纹)。这个清单是硬编码的,
漏了不报错,只是个人数据静静留在库里。
- errorcode.proto 新开 5201-5206 段。复用 ReqParameterError 会让客户端无法分流,
「报告还没生成好」该转圈重试而不是弹错。
测试
comm/memory_test.go 8 个:周日在 Go 里是 0、Dart 里是 7 的换算(错了不报错,
只会让每周日的闹钟在周一响)、ISO 跨年周、闰年二月、非法时区必须回退 time.Local
而不是 UTC(否则静默偏移 8 小时)、client_key 非空且不重复。
modules/memory 14 个:一次性/每天/每周展开、锚点在一年前仍能算出未来时刻、
过期项不排、灵感花销不提醒、全天项按 9:00 不落 0:00、展开结果必须升序
(客户端要按 iOS 64 条上限截断,顺序错了会把明天的排掉留下一周后的)、
项按自己时区算、LLM 输出容错(代码块/前后解释文字)与真失败必须报错。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
4a24ece7d3
记忆中心设计文档:服务端到客户端全局复核,推翻 5 处设计
在 v0.2 复核现状盘点的基础上,从 gateway 逐层过到客户端,又推翻或补充 5 处。
风险表 11 高 / 20 中 / 2 低。
拾忆页是纯展示页,写操作 UI 一个都没有(最大的工作量遗漏)
全页没有 RefreshIndicator、没有 FloatingActionButton/Icons.add、卡片没有
onTap(唯一的 onTap 是范围切换 Tab)。于是 memory_update/del/complete 三个
写接口在客户端没有任何调用点,user_edited + gen_round 这套「保护用户修改」
的机制在没有编辑 UI 之前完全是空转的。分期表 #6 原先只写了「分类筛选、换源、
删 mock」,补上 6b:编辑层 / 勾完成 / 新增表单 / 下拉刷新 / 空态。
每日弹窗不能 await 在 splash——与 6a64c3d0 方向相反
那次提交刚把 _initMeetingTemplate 从启动串行链摘出去改成 unawaited,理由是
「启动时没有任何地方要用它」。文档写「每日弹窗排在启动第三个」等于把刚摘掉的
成本原样加回来,而且这次摘不掉。改成:splash 照常放行导航,进主页后 unawaited
拉 memory_today,拿到再弹。取舍同设备页那处「新耳机晚一两秒出现」。
MCP 工具会造成第二份查询实现
mcp 是独立进程,工具直接 mysql.Table(...).Where(...) 裸查,全模块搜不到一次
RpcCall。5 个新工具 = 过滤条件/分类白名单/权限规则在两个进程各写一遍,改一边
不报错地漂移。且 mcp.yaml 只配了 mysql,没有 redis/postgres。列了两条出路
(RPCX 调 home / 抽共享包),一期就要选,否则三期返工。
cron 的真实风险是漏跑不是重复
docker-compose.yml 没有 replicas/deploy,当前就是单副本,「多副本重复生成」
是前瞻性风险。现在会发生的是:00:30 跑到一半容器重启,cron 不补跑,
memory_report 永远停在 state=0,那周永远没报告且不报错。补上客户端驱动的
补偿(照 echomeet 的 PollTranscribe)。顺带记下 timer_uselog 那个每日 cron
零防重零补偿、靠 Redis Del 巧合幂等,不是可照抄的范例。
「报告挂额度」走不通
既有额度扣减全是「用户主动发起」语义(api_starttask 先查够不够再扣、失败 +=
退还;api_usages 扣 AI 次数)。周报是 cron 主动生成,用户什么都没做就被扣,
余额不足时「报告不生成」也没有合理的交互位置。改成三个可行选项,推荐只给
「上周期有 N 条以上记录」的活跃用户生成。
另补四条中危
- 业务错误会自动弹红色 snackbar(AuthInterceptor 默认行为),抑制名单里只有
user_getinfo/user_binddevice;memory_today/upcoming 是启动路径自动调的。
- code==0 且 data 为空拿到的是 {} 不是 null,与「业务错误」含义相反。
- AppLifecycleManager 已存在且 permanent 注册,前台恢复钩子直接复用,别挂
第四个 WidgetsBindingObserver(已有 emai/translation/permissions 三个)。
- ErrorCode 要新开 5201- 段,复用 ReqParameterError 会让客户端无法分流。
顺带确认无需改动的:/api/:param1/:param2 的 param1 就是服务名,模块挂进 home
后路径直接命中;meettemplates.vue 已能按语言编辑 outline/template;IsSign 时间戳
防重放所有 yaml 都没配(默认 false),并发请求暂不受影响;播报走 TTS 的 normal
支不碰 80c6ea1b 刚修好的 phone_call 路由,但共用同一个 AVAudioSession,判据是
通话翻译活动期间一律不播报。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
7c5c23acaa
记忆中心设计文档 v0.2:复核现状盘点
v0.1 之后主仓有 4 次提交(翻译延迟优化、设备表合表、收拢积压改动、
上传改预签名 URL),本次逐条复核「现状盘点」,修正已不成立的条目,
并记下会绊到本设计的既有缺陷。状态仍为未开工。
1 month ago
Rodger-Wang
6a64c3d008
把两处网络请求移出启动与设备页的关键路径
会议模板不再挡启动与登录
_performParallelTasks 名字叫并行、实际完全串行(网络检查 → 版本检查 →
token 校验,后者内部又是 getLoginToken → getAppConfig → _initMeetingTemplate),
冷启动要串等 5 次网络往返,而 _initializeApp 要等这一整串跑完才允许导航。
其中 ②③ 之间的串行是刻意的(强更弹窗必须先出、把 splash 卡住,见 CLAUDE.md),
不能改回并行;但 _initMeetingTemplate 排在最后、**启动时没有任何地方要用它**
——模板只落 sqlite,由会议模板页 getMeetingTemplateList 消费。改成 unawaited,
模板照常拉取更新,启动不再等它。
登录流程里同一句挡在 Get.offAllNamed(Routes.home) 前面,一并改。
设备页先渲染再补登记
loadDevices 里 `await BesDeviceAuth.verifyConnected()` 串在 getdevices
前面,首次进入白名单没命中时会真的去打 user_binddevice(带 10s 超时),
整个页面干等两次网络往返——就是「点设备页半天没响应」。
- isLoading 改成只在本地无数据时才遮罩:onInit 里 _refreshFromUser() 本来
就已经拿本地列表渲染过一次了,再盖一层纯属让用户对着转圈等。
- verifyConnected 挪到列表渲染之后、改成后台跑。它登记成功时会直接把设备
塞进 User.instance.devices,回来 _refreshFromUser() 刷一次即可,
不必再拉一遍 getdevices。
⚠️ 代价:首次连接的新耳机会晚一两秒才出现在列表里(以前是干等到它出现才
显示整个页面)。这是「页面立刻可见」的必然结果。
⚠️ 两处的实际收益都要在 release 包上量,debug 的 JIT 会把差异淹掉。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 month ago
Rodger-Wang
80c6ea1be3
iOS 真机排查:修同传播报路由、录音时长与电平崩溃
一轮 iPhone 真机联调里发现并修掉的一组问题,多数是既有缺陷,两处是我在
本轮前期改出来的回归(已在注释里标明,避免以后再踩)。
同传播报路由(AzureTtsHelper.swift,phone_call 分支)
这一支被改坏过三次,结论固定在注释里:
① 最早 `.defaultToSpeaker` + `overrideOutputAudioPort(.speaker)`
→ 连了 A2DP 也被强拽到外放;元凶是那次 override(已删)。
② 修 ① 时把 `.defaultToSpeaker` 一并删了 → 删过头:`.playAndRecord` 的
默认输出是**听筒**,没耳机时译文几乎听不见(「翻译出来了但不播报」)。
③ 补回 `.defaultToSpeaker` 且 mode 设成 `.videoChat`(为了 AEC)
→ `.videoChat` 本身偏好内置扬声器,连了耳机又被拽到外放,绕回 ①。
现在按**有没有耳机**分两支:有耳机走 `.default` + 仅 A2DP;没耳机走
`.videoChat` + `.defaultToSpeaker`(外放且开回声消除)。分支口径与采集侧
MicrophoneCapture 启用 voice processing 的条件完全一致,两边不再打架。
没有这个 AEC 时,外放的译文会被自己的麦克风收回去,ASR 当成新一句再翻一遍。
现场录音(bes_recording_service.dart)
- 时长改为按**已写入的数据量**算(32 字节 = 1ms),与 WAV 头一致,
因此界面时长与文件实际播放长度永远相等。墙钟算不准:耳机收到 AA 59 后
要 1.8~6.5 秒才上行首帧,还会把等待期缓存的音频一次性补推,实测数据量
比「首帧到停止」的墙钟窗口还多 21~45%,从哪头计时都对不齐。
真机验证:音频19.15s/墙钟18.88s、音频18.72s/墙钟18.69s,
等效采样率均为 16000Hz 整、0 丢帧。
- 修 `_updateLevel` 的 `asInt16List(offsetInBytes, …)`:原生来的 Uint8List
是带 offset 的视图,偏移为奇数时必抛 RangeError。原来藏在 try 里被吞掉,
表现是波形/电平一直不动(数据本身没丢)。改成按小端手工拼 int16,
与 BesCallRecordingService._writeStereo 同一修法。
⚠️ 本轮我一度把它挪到写入之前且未包 try,一抛异常整个 _onPcm 中断、
音频一帧都写不进去(实测 6990 次未捕获异常)——现在它排在写入成功之后
并单独包 try,波形挂了绝不能影响录音。
- 音频写入排成 Future 队列。Stream.listen 的 async 回调不会被等待,
每秒上百帧必然重叠,而 RandomAccessFile 同一时刻只允许一个异步操作。
(事后核对通话录音同样写法却没丢数据,所以这条是防御性的,不是主因。)
识别异常与状态收口(translation_controller.dart)
- 新增 _abortRecognition:catch / error / canceled 三条异常路径统一回滚,
**把录音一起停掉**。此前 `isRecording` 全文件只有一处 `= true`、从来没被
置回 false,识别挂了右上角还在计时;异常路径更是连 stopRecording 都不调。
标记同时收口进 stopRecording,所有停止路径都覆盖得到。
- startRecognition 点击即点亮 isPreparing(view 早已绑好按钮反馈,只是从来
没人设过它,点下去毫无视觉变化,就是「点了没反应」)。
- `await _modeInitFuture` 补 3 秒超时。它是本轮前期为消除语言竞态加的,
但没有超时——iOS 首次要激活音频会话、连着蓝牙还要等音频路由,一旦迟迟
不返回点击就永久卡住。把「可能用错语言」换成「可能完全卡住」是坏交易,
超时后走兜底 initialize,至少起得来。
- 三段耗时打点提到 warning 级:release 的日志级别就是 warning,用 info 打
在真机上一条都留不下。
其它
- Logger 在非 release 下同时 print 一份:`developer.log` 只进 VM service 的
logging stream,`flutter run` 控制台看不到、调试态下 idevicesyslog 也抓不到,
真机排查时加了一堆打点却一条都看不见。
- 连接握手不再自动跑 probeCommands:每条间隔 400ms、一轮十几秒,期间 SPP
通道被占满,用户点录音/翻译的指令只能排队。需要重扫时手动调。
本次在固件 0.0.3 上的扫描结果记进注释(0x0B/0C/0D/0F/10/19 有回包,
但取值都不像电量百分比,电量命令仍未定位到)。
- 通话状态变化打出触发它的原始字节,便于排查「刚连上就被判成通话中」。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 month ago
Rodger-Wang
8d580a545d
文件上传改走服务端签发预签名 URL,客户端直传阿里云 OSS
客户端不再持有对象存储的长期密钥。
旧实现用腾讯云 COS SDK 直连,bucket 与 SecretId/SecretKey 明文随
user_getappconfig 的 env 下发给每一个 App 用户(config 表的 COS_* 那几个键)。
抓包或反编译就能拿到这对长期密钥,而它对整个桶有读写权限——不只是自己那部分,
User/ 下所有人的录音都能被拉走或删掉。
现在:新增 user_getuploadurl,服务端签发 15 分钟有效、且只对某一个 object key
生效的阿里云 OSS 预签名 PUT URL,客户端拿着它直传。存储与 console 后台传固件/
产品图同一个桶(ymaioss),不再是两个云厂商两套存储。
服务端
- 新接口 modules/user/api_getuploadurl.go:
· object key 由服务端拼死 EAIMAR/User/<uid>/<scene>/<yyyy/MM/dd>/<12位随机>.<ext>,
uid 取自会话不采信客户端,客户端连自己传到谁的目录下都决定不了;
· scene 白名单 LocalAudio / TranslatAudio / ExternalAudio / ChatImages
——加新场景前要把客户端 addUpload 与 UploadOss.upload 的调用点全捞一遍,
少一个值就是那条链路整条报错,且只在真机跑到才发现;
· 扩展名白名单决定 Content-Type,不在表里直接拒,不回退成
application/octet-stream(那等于把桶变成任人上传的网盘);
· 随机名用 crypto/rand 而非 math/rand:key 可枚举就等于别人能猜到录音地址。
- 抽出 comm/ossconf.go:OSS 配置读取、上传根前缀、publicURL 拼接从 console 挪来,
console 改为调用它。两边必须落同一个桶,各写一份迟早漂移。
解密用 ${FIELD_ENCRYPT_KEY},console 与业务服务本就必须配同一把。
客户端
- upload_oss.dart 重写为「要 URL → dio 流式 PUT」,upload() 签名与返回值不变,
MeetingUploadService 不用改。用独立 Dio 实例:项目那个挂着 AuthInterceptor,
会加 token 与业务签名头、还会把响应按业务 JSON 解析,直传 OSS 时全是多余,
签名头还可能与预签名冲突。
- Content-Type 必须用服务端回带的值,它计入了签名,写别的会 403。
- agent 图片的 rootDir 从 'User/<uid>/ChatImages' 改成 'ChatImages':
前缀由服务端加,客户端再拼一层就是重复路径。
真机验证:录音落到
https://oss.ymaikj.com/EAIMAR/User/ <uid>/LocalAudio/2026/09/04/howXMScPdsNP.wav,
下载 212524B 与库里 size 逐字节一致,Content-Type=audio/wav,文件头 RIFF/WAVE。
其余三个 scene 走同一段代码但尚未实跑。
⚠️ 新老客户端并存:老包仍读 COS_* 直传腾讯云,config 表里那 5 个键先别删;
历史录音的 audiourl 是 COS 绝对地址,不会自动搬家。
⚠️ 录音目前仍是公共可读的(bucket 公共读,实测匿名 GET 206、不存在的 key 404),
只靠 12 位随机 key 不可猜测来保护。要锁住得转私有桶 + 播放时也签临时读 URL,未做。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 month ago
Rodger-Wang
24e17b3434
收拢积压改动:恒玄 OTA、发版配置拆表、实名认证、芯片厂商/代工厂管理等
把工作区里积压的多轮改动一次性入库。主要几块:
客户端
- 恒玄 OTA 打通:新增 local_plugins/bes_ota_manager(自包含 BES SDK,
Android Java + iOS ObjC,SPP OTA 2.0),路由切到 BesOtaUpgradeView /
BesOtaUpgradeController,配套下载服务;杰理那套文件保留但已不挂路由。
- 通话翻译新增阿里 3.5 档(alibaba35_language_config.dart,
qwen3.5-livetranslate-flash-realtime + 实时声音复刻)。
- 实名认证接入、游客登录改走服务端 api_sgin 的 Tourists 分支、
功能闸门由 GuestGuard 改为 IdVerifyGuard。
- 41 个语种文案同步(OTA 新增 28 个 key 等)。
- 默认主题改为浅色,不跟随系统。
服务端
- 发版类配置拆表:app_release(版本控制 / 游客显隐,一应用一行)与
channel_app(渠道级下载地址 / 上架状态 / 支付渠道)分家,
新增 comm/apprelease.go 与 console/api_apprelease.go。
- 授权码字符串整体移除:删除 utils/license 包,新增 utils/devcode
(MAC 规范化与 PID 反解),配套迁移 SQL 在 docs/migrations/。
- console 新增芯片厂商、代工厂、用户管理接口与对应 admin 页面。
- 身份证实名核验(sys/idverify)与第三方服务的服务端专用类别。
其它
- .gitignore 补挡 apps/services/lego/sys/gin/log-*.log:gin 子系统跑起来
会按时间戳滚动生成日志(多为 0 字节),与 apps/services/comm/log 同一
性质,不进版本管理。已跟踪的 log-2026-08-20 那个仍在库里,未动。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
d7fad60658
设备表合表:license_<pid> 分表 → 全局 device_mac,只按 MAC 校验
设备的身份是它自己的 MAC,本来就全局唯一,定位一台设备不需要 pid。
原先按产品切分表,pid 既是查询键、又得由客户端猜出来,猜错就报
"这台 MAC 没登记在该产品下"——提示指向数据缺失,实际数据好好的。
触发改造的事故:一台已正确导入的 DEEPVOICE 耳机(MAC 在 license_b022,
产品 45090,status=0 未绑过)连不上、弹"设备连接受限"。根因是产品 45090
的 devicetype 被配成 2(BLE双端)而不是 1(经典蓝牙):客户端
_resolveProductId 第一步就 where(devicetype==1) 把整个产品滤掉,候选只剩
Echo-one,名字互不包含 → 兜底退回 headsets.first.id = 45091,服务端拿错
pid 去 license_b023 查,自然查不到。而且就算 pid 猜对也过不去——服务端
只有 devicetype==1 才走 MAC 反查,2 会走 else 分支从空 code 反解 pid、
回退写死的 45058。
改动:
- 新增全局表 device_mac(comm.TableDeviceMac),主键 code = MAC;
productid 降为归属列,不再是查询键。TableLicense 标记废弃,只留给迁移读。
- registry.go 的 migrateLicenseToDeviceMac 每次启动跑、幂等:
① code := devicemac 修存量 → ② 按两表列名交集做 INSERT ... ON CONFLICT
DO NOTHING(不能 SELECT *:各分表建于不同时期,有的多 factoryid/probatch)
→ ③ productid 为空的用表名里的 pid 补上。孤儿分表里也是真实发出去的设备,
一并搬。搬完刻意不删旧表——不可逆数据,旧表是出问题时唯一的回退依据。
- binddevice / unbinddevice 改为先按 code 再按 MAC 全局定位,命中行的
productid 才是产品。req.Pid 不再采信。杰理链路也顺带不需要 devcode
反解 pid 了。
- 新增 comm.ScopeToProduct:合表后分表那层天然隔离没有了,每个"按产品"
的读写都必须显式带 productid,漏一处就跨产品操作——查询多列几行只是
难看,批量禁用 / 按批次删除漏掉就是删别人产品的数据。它会把调用方的
条件整体括起来,防止 "a=? or b=?" 让 or 把 productid 短路掉。
- 导入查重改成全局:同一个 MAC 出现在两个产品下会让按 MAC 反查出现歧义。
- 客户端删掉 _resolveProductId,binddevice 只报 MAC 不传 pid。
按需求明确不做应用隔离:MAC 存在即放行。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
946204cc3c
翻译延迟优化:接上阿里增量文本、面对面/同传去掉固定等待
通话翻译:阿里百炼端到端协议本来就是流式的(音频 response.audio.delta
一直在逐帧回灌),但增量文本订阅的事件名是错的——代码监听
response.audio_transcript.delta(字段 delta),而实时语音翻译
audio+text 模态实际发的是 response.audio_transcript.text(字段 text),
.delta 是 OpenAI Realtime / 通义 Omni 的写法。于是 onPartialText 一次
都不触发,译文只在 .done 时整句蹦出来,看着像"等说完才翻"。
现在两套名字都认、两个字段都取,并按首次出现的事件名锁定,防止服务端
兼容层双发导致文本累计两遍。Android / iOS 两端同改。
面对面 / 同声翻译(走 ASR → HTTP 机器翻译 → TTS 三段串行):
- 补上 Speech_SegmentationSilenceTimeoutMs=300,原来一行没设走 Azure
默认 500ms,这是"说完到出译文"里唯一一段纯等待;
- 同传 / 音视频改单语 ASR,关掉连续语种识别(它要缓冲够音频、给候选
语言逐个打分才敢出 final)。只有面对面需要判断"谁在说",仍保持双语;
- 中间结果翻译从"每 300ms 无条件发"改成 700ms + 至少多 4 个字 +
CancelToken 撤旧请求 + 序号丢弃过期结果。中间译文只上屏不播报,
原来一句 5 秒的话要打十几次真实服务商调用,还会跟 final 抢链路;
- TTS 提到 _updateExistingHistoryItem 最前,不再排在浮窗 / 写历史 /
记统计那串 await 之后;
- startRecognition 先 await onInit 那次 changeTranslationMode,堵掉
"进页面立刻点开始 → 落到默认 zh-CN/en-US"的竞态。
AuthInterceptor 的 DioExceptionType.cancel 分支不再弹红色"请求已取消"
——取消永远是客户端自己发起的,不改的话中间结果一接上就会一路狂弹。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
27434754a1
记忆中心设计与开发文档
把「代办」页升级为「记忆」页的完整设计:统一 memory_item 模型
承载闹钟/待办/灵感/花销四类且分类可扩展,会议纪要经 AI 抽取自动
落成待办,每日播报、提前 5 分钟提醒、周报月报复盘,以及经 MCP
工具让 EMAI 助手随时问答,形成录入→提醒→复盘→问答的闭环。
盘点后确认在 allhelp 已有的任务体系与异步总结流水线之上演进,
而非另起炉灶;DBTask「仅记录不自动触发」的限制与花销后端空白
是本次要补的主要缺口。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
90abb0ad42
身份证实名核验、翻译与新用户开户礼
本次会话开始前就已存在于工作区的改动,一并提交。主要是三块:
1) 身份证实名核验(sys/idverify + user 模块)
接入阿里云 Id2MetaVerify、腾讯云 IdCardVerification、创蓝三家 provider,
无状态工厂——配置在 svc_config 里按应用作用域存,调用时才解析。
客户端接口 user_idverify / user_getidverify。
凭据是云账号主 AK/SK,故新增服务端专用类别 comm.SvcCatIdVerify=11,
由 svcresolve.go 在下发口整条跳过,svcpool_serveronly_test.go 守着这条底线。
2) 翻译(sys/aliyun/translate + comm/lang.go)
语言码归一与阿里云翻译调用。
3) 新用户开户礼(newuser_gift.go)
配套 api_sgin 建号流程。
注:go test ./modules/user/ 里的 TestApiProbeAppConfigV3 会失败,但与本次改动无关
——那是打线上接口的联调探针,硬编码的域名 app-dev.voitrans.net 已废弃、JWT 也过期了,
该文件自 b2577e16 起未改动过。go test -short 会跳过它,其余全部通过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
0663446f0c
运行日志 apps/services/comm/log 移出版本管理
lego 未配置日志路径时会在工作目录写 ./log,本地跑一次服务或 go test 就被覆写一次,
每次都产生一条无意义 diff(内容是同一条微信支付密钥解密失败的告警,只有时间戳在变)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
2d1ee0b783
apps/client 首次入库(Flutter 移动端)
从 /Users/yunyanzhineng/work/eaimar_client-main 搬入,未带原 .git,历史留在原仓库。
3215 个文件 / 约 118MB,其中约 40MB 是构建必需的预编译 SDK(azure_speech 的
MicrosoftCognitiveServicesSpeech.xcframework、device_jieli 的 JL_BLEKit 等)。
build/ (5.2G)、ios/Pods/ (311M)、android/.gradle/ (120M) 已由 .gitignore 挡掉。
本次入库的代码包含两项刚做完的改动:
1) 游客登录入口接上服务端开关。此前 _checkGuestLoginConfig() 恒定写死 true,
入口根本不读配置。现改为取服务端算好的结论,并把入口所在的 Builder 改成 Obx
——配置是异步拿到的,Builder 只渲染 onInit 时的初值 false,入口再也不会出现。
拿不到配置时按隐藏处理:默认显示会在送审期把不该露的入口露给审核员。
⚠️ 已发布的老客户端关不掉(写死 true),只对本版及以后生效。
2) iOS 上架合规修复:
- 移除 ATT/IDFA:删掉 Info.plist 的 NSUserTrackingUsageDescription、
SKAdNetworkItems、NSAdvertisingAttributionReportEndpoint,删掉 splash 里的
授权请求与 app_tracking_transparency 依赖。本 App 从不读取 IDFA,也没有任何
广告/归因 SDK,弹框纯粹劝退用户,还要在 App Store Connect 申报追踪数据类型。
- ios/Podfile 里那段 config.build_settings.delete('NSUserTrackingUsageDescription')
是无效代码(该 key 在 Info.plist 里,不是 build setting),换成注释说明。
- 通讯录符号(ITMS-90683 被拒原因)随之一并解决:permission_handler 走 SPM 后
按 Info.plist 有无对应 key 决定编不编,判定结果被 SwiftPM 按内容哈希缓存,
改完 plist 必须清 DerivedData 与 manifests 缓存才会重新求值。
另附 GOOGLE_PLAY_RELEASE_CHECKLIST.md:Google Play 上架阻塞项的实测梳理
(targetSdk 36 与 Billing 8 的 2026-08-31 期限、16KB 页对齐不合规的两个 .so、
支付回退逻辑的下架风险)。
⚠️ android/app/sign/ 下的 eaimar.keystore 与 Eaimar.properties(含明文签名密码)
随本次提交入库,与本仓库"私有仓库下接受真实凭据"的既有取舍一致。
仓库若要公开,这两个必须先摘出去。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
db47e0f3f0
游客登录入口改为后台按版本控制
需求:后台一个开关控制游客登录按钮显隐,且只对指定版本号的客户端生效,
其余版本一律隐藏(应用商店送审场景:只给正在过审的那一版开)。
白名单语义(comm.ChannelApp.ShowTourists):
- tourists=false → 全部隐藏
- tourists=true 且 touristsversion 空 → 全部隐藏(没点名任何版本 = 谁都不放行)
- tourists=true 且 touristsversion=X → 只有版本号等于 X 的客户端可见
留空按"全隐藏"而非"全放行"是刻意选的失败方向:判宽的后果是不该露游客入口的版本
露了出去(过审风险、白嫖新用户开户礼),判严只是入口少显示。要全部隐藏就取消勾选,
后台 saveChannelApp 会拒绝"勾了开关却没填版本"这种等于没开的配置。
判定放在服务端,与 NeedForceUpdate/IsReviewing 同规矩:客户端在 user_getchannelapps
的请求里带 version,服务端把每条渠道配置的 tourists 换算成结论再下发。版本比较各端
各写一遍必然出现实现不一致,而这个开关直接关系到过审,判错代价高。
touristsversion 是 channel_app 的后加列,console 启动时用 ADD COLUMN IF NOT EXISTS
补上——CreateTable 对已存在的表跳过 AutoMigrate,不补就永远没这列。
注:db.proto / user_msg.proto / CLAUDE.md 里另有先于本次改动就存在的编辑,一并带上。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
2f81aaef93
构建产物与依赖目录移出版本管理
apps/admin/.output 与 apps/admin/node_modules 此前被纳入 git 跟踪(共 18425 个文件),
每跑一次 nuxt build 就产生数百个哈希文件的增删,真实代码 diff 被完全淹没。
部署不依赖仓库里这份产物:apps/admin/Dockerfile 是 COPY . . 之后 RUN pnpm build
在镜像内重新构建,且 .dockerignore 已排除 node_modules/.nuxt/.output——仓库里这份
从来没被用到过。
只从索引移除(git rm --cached),磁盘文件保留,本地 dev/build 不受影响。
.gitignore 同时补上此前遗漏的 .DS_Store。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
Rodger-Wang
bdfcb1ecbd
站点品牌名改为翊迈SaaS管理平台
- console 的 SiteName 与 admin 的页面标题统一改名,部署模板
(console.yaml.example / env.example) 同步跟上
- 侧栏与登录页标题由后端 api_getsiteinfo 下发,改 CONSOLE_SITE_NAME
即可生效,无需重建镜像;浏览器标签页在 nuxt.config.ts 里,需重建
- dev-deploy.sh 的 along 档案密钥改为 ${DEV_KEY:-原路径},保留原默认值
的同时允许用环境变量覆盖,便于在没有该路径密钥的开发机上部署
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 months ago
liwei1dao
bb3b1c7a3a
上传代码
2 months ago
liwei1dao
b2577e16fa
上传最新的平台代码
2 months ago
liwei1dao
af05ff12ce
上传代码
2 months ago
liwei1dao
1639cf35a0
上传代码
2 months ago
liwei1dao
aca7204e57
上传代码
2 months ago
liwei1dao
8924a3cd3b
上传服务后台代码
2 months ago
liwei1dao
554a4ab501
上传最新的版本代码
2 months ago
liwei1dao
948e368996
上传代码
2 months ago
liwei1dao
59e54eb575
上传发布版本
2 months ago
liwei1dao
6f7cfe3bb2
上传主版本
2 months ago
liwei1dao
a36f0a016a
上传服务端代码
2 months ago
liwei1dao
4896d64366
上传代码
2 months ago
liwei1dao
7660f33258
上传最新代码
2 months ago
liwei1dao
97ecbf9df5
上传新版本的管理后台
2 months ago
liwei1dao
18ffb48466
上传代码
2 months ago
liwei1dao
e270cf89b6
上传新版本管理后台
2 months ago
liwei1dao
6e5da54cf8
上传代码
3 months ago
liwei1dao
1f0f42adb8
移除误提交的 82MB home 服务编译二进制并忽略各服务同名产物
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
liwei1dao
af9e210f9a
会议记录接入后台服务编排:三段独立选路+客户端可选总结模型,移除旧 yaml 四套凭据模式
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
liwei1dao
a25b0c9fc0
上传最新的服务后台代码
3 months ago
liwei1dao
040af15192
上传基础服务代码
3 months ago
liwei1dao
d7445984cb
添加 授权吗 的方案
3 months ago
liwei1dao
197d544062
上传管理后台的系统升级和服务配置逻辑优化
3 months ago