Rodger-Wang
|
c12651e79c
|
语音纪要:录音语言自动识别、转写不翻译、翻译只作用于总结;可选总结模型;详情加 PPT 页签(仅界面)
客户端
- 首页标题「语音记录助手」→「语音纪要」,去掉顶栏右侧的算力摘要
- 「生成」弹窗:去掉录音语言(传空,交给识别服务自动判断);「目标语言」改为「翻译」,
默认「默认」= 不翻译、总结跟随录音语言,选了语言 = 总结/概览用该语言输出;
新增「总结模型」一行(echomeet_getsummaryservices 拉列表,记住上次选择,summary_svc_id 带进 starttask/summary)
- 重新总结选「默认」时传 tolanguage=auto(该接口空串历来表示沿用上次语言)
- 纪要详情在思维导图右边加「PPT」页签:示意图 + 「生成 PPT」按钮,点了提示即将上线
服务端(echomeet)
- 转写不再翻译:TranslateProcess 只做 Translate=Original;StartTask 不再选翻译服务
- 总结输出语言由 summaryLanguageInstruction 追加在模板 system prompt 末尾(空=跟随转写语言)
- 模板解析 ResolveSummaryTemplate:有目标语言先找该语种版本,否则用客户端选中的那条
- formlanguage / tolanguage 的 "auto" 与空串同义;Azure 批量转写不指定语言时带候选语种自动识别,被拒退回 en-US
- console「会议配置 → 会议总结」:默认模型之外可勾选「供用户选择的模型」,勾了即打开应用级「允许客户端选择」
⚠️ 行为变化:服务端部署后老版本 App 也不再翻译转写(原「英文录音→中文」现在是英文转写 + 中文总结)。
验证:dart analyze 无 error;flutter test 224 全过;go build/vet 通过,echomeet/console 测试通过;
admin npm run build 通过。已部署测试服(app/console/admin),release 包装到 ALN-AL80。
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
2 weeks ago |
Rodger-Wang
|
934a8cd287
|
client:底部回到「首页 · 我的」,对话助手放回首页网格;设备页一页到底
- 去掉底部中间的「AI」按钮(连同按设备挑助手的 openAiAssistant),EMAI / EaiCar 车载助手
回到首页网格当普通卡片,拖拽排序改回 service.move
- 首页左上角设备入口:从没连过也没绑过任何设备时显示加号,点了直接进「选择设备」;
有过设备时显示最后连接那台的小图标,连着亮、没连置灰
- 设备页:主图下面直接铺已绑定设备卡片(原「设备管理」页内容,抽成 BoundDeviceCard 两处共用)
和固件升级一行(当前/最新版本,设备版本低于服务端时右边直接「开始升级」,进度在行内走,
升级中返回先确认);「添加设备」挪到页面最下面
- 原「设备管理」「固件升级」页保留(语音纪要「我的」、固件更新提醒弹窗仍在用)
验证:dart analyze 无新增 error/warning;flutter test 224 全过;release 包装到 ALN-AL80
看过首页与设备页(未连接态)。连接态电量/升级按钮、首次加号形态未在真机上看。
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
2 weeks ago |
Rodger-Wang
|
2dacba0e59
|
client:底部改为「首页 · AI · 我的」,设备/拾忆收进首页顶栏,AI 按钮按设备接对应助手
- 底部:原「AI」tab 改名「首页」;中间新增圆形「AI」按钮(不是常驻页,点了才打开对话页,
避免百炼连接和麦克风常驻);右边「我的」。设备、拾忆不再是 tab
- 首页顶栏:左上角产品小图标(DeviceStatusChip,已连接绿圈+绿点+电量,未连接置灰),
点进设备页 /device;右上角「编辑」紧挨「日历」,日历点进拾忆页 /recall。
两个独立页复用原 DeviceTab / TodoTab(standalone 模式带返回箭头,TabScaffold 新增 onBack)
- AI 按钮:openAiAssistant() 按「当前连着、没连就最近连过的设备」→ 品类默认编排里第一个对话助手:
EaiCar 香薰 → EaiCar 车载助手,其余 → navigateToAIAssistant(EMAI / 移动精灵)
- 首页网格不再显示对话助手(AgentModuleService.assistantIds:EMAI、EaiCar),只是不显示,
编排与设备按键唤醒找 agent 不受影响;拖拽排序改走 moveOnHome 换算下标
- 三个扫描页连上后改为回到首页再打开设备页(showDeviceAfterConnect)
验证:dart analyze 无 error;flutter test 221 全过;release 包装到 ALN-AL80 真机点过首页、
日历、设备、AI 按钮(打开 EMAI)。连着 EaiCar 时打开 EaiCar 助手这条未实测。
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
2 weeks ago |
Rodger-Wang
|
2a913260e6
|
车载香薰:会话带上用户位置与当前时间,填百炼提示词里的 ${location} / ${date}
- 定位插件新增 getCurrentPlace:安卓高德定位(华为上高德只回坐标时退回系统 Geocoder),
iOS CoreLocation + CLGeocoder;不在插件里弹权限框,失败回原因
- DeviceLocationService:启动/回前台刷新、10 分钟节流、落盘缓存;香薰首次连上(前台)申请一次
「使用期间」定位权限,拒绝后不再追问;建会话只读缓存,不拖慢唤醒
- BailianMultimodalService.start:仅对登记的应用(车载香薰)带
client_info.location{latitude,longitude,city_name} 与 biz_params.user_prompt_params{location,date},
date 精确到分;拿不到地名时用坐标描述。EMAI 耳机的 Start 报文不变
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
2 weeks ago |
Rodger-Wang
|
4ec8932aef
|
车载香薰 AI 对话:唤醒问候、按键插话、自动回连、退出聊天、前后台与状态同步(均只对香薰生效,恒玄耳机流程不变)
- 会话:服务端放弃回复(requestAccepted)时停播解门控,修掉互等 90s 的卡死;会话中再按唤醒键=打断重听;
唤醒后用应用音色随机回一句「我在」;退出(0x07 03)无应答不再干等 2s;设备上线复位 AI 状态,
防 App 被杀后设备停在 AI 模式、再唤醒 0 帧上行;Android 会话期间开前台服务,防切后台被冻结断 WS
- 指令:extra_info.commands 为 JSON 字符串时也能解出 quit(新车载应用下发形状)
- 连接:香薰断线/冷启动自动回连(带退避、OTA 与主动断开不回连);回连带上记住的 MAC;
设备断开后界面显示最后连接的那台(DeviceHub 落盘 lastUsedVendor)
- iOS 经典蓝牙判断按 MAC 首字节±1/设备名匹配,连着就不再弹配对提示
- Android 播放器:车载会话(playbackOnly)回复间垫静音,防 A2DP 流挂起吞掉「我在」
- 聊天页:设备对话中底部按钮可「改用手机」;离开正显示香薰对话的页面时结束会话并通知设备
- 车载助手百炼应用 ID 改为 mm_127ddb82ba1244a99d2feaef0c1f
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
2 weeks ago |
Rodger-Wang
|
6c9e0d48eb
|
语音纪要:有算力却提示余额不足(客户端按停用的旧会议额度拦截);转写失败按扣费明细原路退还算力
客户端
- 生成总结前不再判旧「会议额度」meetintegral <= 0 就弹余额不足:那个桶已随会员与算力改造停用,
新账号恒为 0,于是有算力也被拦,请求都没发出去。准入交给服务端(没 VIP 回 VipRequired,
算力不足按后台闸门处理),同翻译那次的修法
- 语音纪要页顶栏:原来按旧额度显示剩余时长 + 进度条(恒为 00:00:00),改为显示算力(user_getcompute)
服务端
- 开始任务扣算力后把明细记在记录上(新列 computeuser / computedevices,AutoMigrate 自动加);
转写失败的八条路径与开始任务中途报错,一律经 refundRecordCompute / refundCharge 按明细原路退回:
设备部分减回 computeused、账户部分加回 computebalance;先在库里原子认领明细再退,保证只退一次;
超额部分从未扣过,不退
- 去掉短音频失败时 user.Meetintegral += 秒数 + 整行 updateuser(退进了停用的旧桶,整行写回还可能覆盖并发的算力变动)
- 新增流水类型 UserLogType.ComputeRefund = 6
- comm:ComputeResult 带每台设备的扣费明细(Cuts);新增 EncodeDeviceCharges / DecodeDeviceCharges / RefundComputeUsage,
编解码有单测(MAC 带冒号,按最后一个冒号切)
验证:go build/vet 通过,comm / echomeet / user 测试通过;dart analyze 无 error,flutter test 221 全过;
测试服已部署 app + console,两列已加上、五个服务正常。
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
2 weeks ago |
Rodger-Wang
|
d6892eb3d6
|
后台管理下载落地页;实名认证显隐与游客登录同一个口子
下载落地页(版本与分发页)
- 「下载与说明」区顶部新增「下载页」一行:编辑 / 保存 / 查看 + 地址输入框
- 编辑:CodeMirror 编辑线上 HTML,改动自动存浏览器草稿;保存才发布到分发桶
(服务端 PutObject,强制 text/html;charset=utf-8 + no-cache),地址写入 app_release.download_page
- 查看:右侧抽屉预览,可切「当前编辑内容 / 线上版本」、手机 / 电脑尺寸
- 地址只能改文件名,域名与 /<应用>/ 目录由服务端卡死(TestParseDistPageURL)
- 新接口 api_getdistpage / api_savedistpage;admin 新增 codemirror 依赖
实名认证显隐
- 能看到游客登录的那一版(送审版本)才显示实名认证并按实名限制功能;
其余版本实名整个隐藏,功能不受实名限制。不分 Android / iOS
- 服务端:comm.AppRelease.ShowIdVerify(= ShowTourists)→ DBChannelApp.idverify 下发
- 客户端:VersionUpdateService.showIdVerify → IdVerifyGuard 隐藏即放行;
「我的」页实名行、卡片「未认证」角标跟着显隐;老服务端无 idverify 时退回 tourists
- 后台「游客显隐控制」页说明改为同时控制游客登录与实名认证
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
9f105bf7c8
|
会议纪要分享:链接最后一段改成服务端签发的只读票据,不再放登录 JWT
链接格式 share.ymaikj.com/share/{serverHost}/ymaikj.com/{id}/{ticket}。最后一段
以前放的是 logintoken(完整登录 JWT)——谁拿到链接就拿到了整个账号,转发进群、
被截图、进浏览器历史都等于把账号交出去,而且除了改密码没有任何失效手段。
现在是 HMAC 签名的分享票据:绑定单条记录、只读、默认 7 天过期,密钥由
FIELD_ENCRYPT_KEY 派生(上线不需要新配置项)。
- echomeet_getsharedrecord 必须在 gateway 白名单里——打开链接的人本来就没登录,
漏了是 code:18、表现为「分享链接打开一片空白」。签发端 echomeet_createsharelink
反之,不要加进白名单。
- 落地页嵌在 gateway 二进制里(go:embed),路由 GET /share/*any 必须是 catch-all:
路径里的段是数据不是文件路径,gin.Static 会去磁盘找同名文件、一律 404。
- 落地页的 ALLOWED_HOSTS 白名单不能删:serverHost 来自 URL,不校验就等于「构造
一条链接让别人的浏览器带着票据去请求任意域名」。新增环境要加一行。
- 极简 Markdown 渲染器先 esc() 再套正则,块级语法要匹配转义后的形态(引用块匹配
的是 > 不是 >),写成 > 不报错、只会静默退化成普通段落。
客户端侧:分享前先申请票据并加载提示,拿不到就不分享出一条必然打不开的链接。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
4753d37c69
|
版本更新判定改口径:按平台分版本、最低版本留空=线上版本、渠道未上架不推、地址回退链
判定仍全在服务端算好下发,客户端只拿结论。规则:
1. Android / iOS 各一套版本号:app_release 加 ios_version / ios_min_version
(启动 ALTER ... ADD COLUMN IF NOT EXISTS 幂等),原 version/min_version 只管
Android,平台由渠道号推导(只有 GlobalApple=101 是 iOS)。iOS 没填时**不回退
Android 的**——回退等于又回到共用一个版本号:Android 先发 1.0.3 而 iOS 商店还是
1.0.2 时,iOS 用户会被指向一个商店里没有的版本。
2. 最低支持版本留空 = 线上版本:勾上强更不填最低版本,就是「所有低版本都强更」;
填了则只强更低于它的,介于两者之间的仍是推荐。后台校验相应改成「至少填一个
平台的线上版本」。
3. 渠道行状态不是「已上架」(审核中/已下架)时不推送推荐更新——商店里还是旧包。
强更不受此限,它的地址会由回退链换成官方包。
4. 下载地址回退链:Android 渠道 = 本渠道(须已上架)→ 官方渠道(100) → 默认下载
地址;iOS / Google 只认本渠道,没有就空——那两处如今放的是 apk 直链。
5. 算不出任何地址时两种更新都不提示,强更也不例外:一个点了没反应又关不掉的
弹窗等于把用户锁死。
客户端三处配套:推荐更新的「稍后提醒」记住版本号,同一版本 24 小时内冷启动不再弹
(强更与手动检查不受影响);设置页手动检查绕过 10 分钟缓存;Android 上服务端地址
打不开时回退网页下载页(iOS 不兜底,那页给的是 apk)。
另修后台上传安装包报「必须先选择应用」:X-App-Id 头来自登录态的 currentAppId,
只有绑定了应用的运营/代理账号才有;超管在页面顶部选的应用只是个 app_name 字符串,
从不进请求头。现在 uploadFile 把它放进请求体,服务端没有 X-App-Id 时按 name /
app_name 反查注册表补作用域。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
6f1686a8fd
|
client: EaiCar 设备设置面板(按能力出现、按设备回的 items 渲染)
「设备 → 设备管理」里 EaiCar 那一行整块可点(解绑按钮除外),弹底部面板拉 0x15
显示并可改:启停 0x0D / 音量 0x0B / 亮度 0x0A / 行车感应 0x11 / 摇摆 0x14 /
随机表情频次 0x13 / 随机播一个表情 0x06。
- 面板是厂商无关的:只认 DeviceCapability.settingsPanel 与 DeviceFeatures.settings*
通用键,按 settings.query 返回的 items 画控件,缺哪项不画哪项。EaiCar 插件把
通用键映射成字节(EaiCarSession.settingsCommand,纯函数,金测试逐字节钉死)。
以后耳机/支架要露设置,各自插件声明即可。
- 状态只有 0x15 一个来源:六条写命令只回 0x64 成功码,不回显新值也没有主动上报。
插件写成功后本地回填并派 settings.changed;面板从不自己推断状态。原先
_applySummary 解完 14 字节只留固件和电量,这次整份留下。
- 0x11 双向语义不同:AA 11 是行车感应开关,CC 11 是设备上报 AI 唤醒,按帧头分发。
- 「随机表情」设备只有频次(9 档秒数);「现在随机播一个」没有原生命令,
由 app 在 0x00~0x09 里挑一个发 0x06。
- 未连接的 EaiCar 行也可点(按设备名猜品类),进去是「未连接 / 去连接」;
其它品类的行 onTap 仍为 null,行为一字不差。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
5036ab4c3f
|
client: 修通话翻译「德语/意大利语一开就卡死、只有中英正常」——并发初始化+默认参数兜底
不是语种问题,是竞态。原生阿里初始化天然 >=1.2s(A 路 init → delay(1200) →
B 路 init),期间只要再来一次 initialize/dispose,「代数」就变了,前一次返回
false;Dart 拿到 false 把 _isInitialized 清掉;接着 recognizeCallback() 看到
「未初始化」就调**无参** initialize()——默认值 provider=azure + [zh-CN,en-US],
把刚握手成功的阿里 zh→de 会话整个换成 Azure 中英三段拼装。此后原生 provider
停在 azure、Azure 识别器一直在跑,每次 dispose 都慢几秒,下一轮更容易撞进
1.2s 窗口,**退出重进也不会好**。中英「正常」只是因为默认值恰好是中英。
修在三层,缺一层都会复发:
- azure_ast_service.dart:initialize() 串行化(_initInFlight);记住最近一次的
provider/语言表;recognizeCallback / startContinuousTranslation 走
_ensureInitialized——先等在途初始化,仍没成就**按上次参数**补一次,
永远不用默认值,没有可复用参数就直接报错。
- translation_controller.dart:_initializeCallModeTranslationService 串行链;
isPreparing 期间忽略重复点开始;_attachBesCallTranslationBridge 开桥前先等
初始化真正落地(10s 上限)——原来靠 _modeInitFuture 3s 超时硬往下走,
桥开在原生初始化之前。
- AzureSpeechPlugin.kt/.swift:被新一代取代时打出阶段与代数、A/B 任一失败打
结果,排查先找这两行。iOS 另修一处独有差异:原先 A/B 无论成败都回 true,
Dart 以为就绪、桥照开而实际一路是空的,现在按真实结果回。
Redmi K90 Pro Max 验证:连开 9 场(it/de/en/ja/fr/ko×2/en,含切语言、关闭重开、
退出重进)全部阿里 3.5,初始化各 1.2s,87 句译文,0 错误,断粮 0 次。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
c1dcfcfc60
|
client: 车载香薰 Dart 侧整体改名 Smartcar → EaiCar
目录、类名、文案键、路由、测试全部改成 EaiCar。刻意保留:原生插件 device_smartcar、
持久化厂商键 'smartcar'、模块 id 'smartcarChat'、GetStorage 两个键(改了会丢老装机的编排/白名单)。
广播名准入白名单本来就只认 EaiCar,旧名 Smartcar 在测试里仍作为被拒绝的样本保留。
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
d1db3e5951
|
client(android): 通话翻译三处修复——同页第二场聋、call.stop 偶发丢失、译文积压追赶
全部在 Android 原生(azure_speech / bluetooth_manager 的 Kotlin),iOS 一行未动。
2026-09-22 ALN-AL80 + Echo-one 真机,配合协议栈 RFCOMM 日志逐条定位与复验。
同页第二次开始没反应(azure_speech)
- AST 就绪状态记在插件上(astSessionReady,与 iOS 同义),建桥与重新 enable 时都继承;
BesCallPcmBridge.disable() 不再把 ready 打回 false。
- 原因:markReady 只在 initialize 末尾发一次,同页面再开始只走 startContinuousTranslation,
disable() 清掉的 ready 再也没人置真,上行帧全扣在环形缓冲里。实测进页面第一场好、
之后每场 ready:false / downlinkFrames:0,退出重进又好一场。
结束指令偶发没到耳机(bluetooth_manager)
- 控制命令走单线程队列:距上一条命令 ≥30ms、距上一个实时音频包 ≥500ms 才写;
下行音频改走 writeRealtime 直发。
- 原因:BluetoothSocket 写入经本地管道,协议栈按信用一次把管道里攒着的字节合成一帧
(PORT_WriteData p_len=4 / 172 / 256),耳机一帧只解开头一条命令。8 次收尾丢 3 次,
耳机不回 BB D2、继续推音频,下一次 call.start 应答 BB D1 07 01(已在通话模式)。
修后 8 轮 0 丢,边播译文边点结束也在 0.5s 内收到 D2。
译文积压越说越慢(bluetooth_manager CallTranslationDownlink 重写)
- 积压 >1s 裁掉超过 150ms 的静音;>2s 起 WSOLA 变速不变调,4s 达 1.3x;不丢内容
(兜底丢弃常量默认关)。新增 getCallDownlinkStats 与收尾总账日志。
- 原因:译文音频突发产出、只能 1 倍速播,说话人不停顿时源时长≈原话时长,队列只涨不缩,
实测 A 路积压 6.2s。修后同量压测最大 3.2s,绝大部分时间 <1.5s,光裁静音就省了 10%,
变速最高只用到 1.11x。
其它
- translation_controller 收尾那几行日志提到 warn(release 包能看到 call.stop 有没有发)。
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
7ce56f693d
|
client: 通话翻译音频链路修复(iOS GATT 写入、后台保活、状态竞态)
iOS 上恒玄走的是 GATT over BR/EDR 而非 BLE,之前按 BLE 模型处理导致一连串问题。
本次改动都有真机数据支撑(2026-09-21 压测)。
GATT 写入(bluetooth_manager)
- 写类型改为优先 .withoutResponse。ATT 是请求-响应协议,带应答写每包等 ACK,
追不上下行 50 包/秒,表现为音频越听越滞后。
- canSendWriteWithoutResponse 只作观测、不作门槛。它是 CoreBluetooth 为 BLE
设计的提示,在 BR/EDR GATT 上不反映真实能力(实测 100% 为 false 却零丢弃);
拿它当闸门会死锁:不写就不回调,不回调就永不翻真,实测 548 包只出去 11 个。
- 控制命令无条件直发、不排队。排到积压音频后面会让 AA 09/AA 06/queryCallState
全部发不出去,曾被误判成「固件换了命令号」。
- 不再对标准电池服务 0x180F 做任何 ATT 操作:它在 Echo-one 上是空壳(值恒为 0),
而挂在它上面的读/订阅会把整条 ATT 通道堵死。
后台存活(translation)
- 通话翻译期间持有 BackgroundKeepalive。两路音频全走耳机 GATT,手机侧没有
AVAudioSession 的实际 I/O,UIBackgroundModes=audio 不成立,切后台几十秒后
上行被掐断:WebSocket 还连着,但服务端收不到语音,靠默认 VAD 每 30 秒强切一句,
吐出空译文或幻觉 —— 表现为「切后台后只剩译文、原文全没」。
- BackgroundKeepalive 不再无条件把音频类别改成 .playback,避免踩掉通话翻译
正在跑的 .playAndRecord 录音链路。
状态竞态(azure_speech / device_hub)
- markReady 的就绪状态记在插件上、建桥时继承。建桥与 AST 异步初始化是两条独立
路径,初始化先完成时那句 optional chaining 打在 nil 上静默丢失,之后 ready
恒为 false、上行帧全积在环形缓冲里,表现为「打开通话翻译没反应,再开一次才好」。
- DeviceHub.inCall 改以会话快照为准。事件会随 worker 在断连时被 dispose 而丢失,
重连后 ever 又不回调当前值,且后续重复赋同值不触发,永远不会自愈。
可观测性
- 下行发送器与 GATT 写入的日志从 NSLog 改为 os_log(release/profile 包里
idevicesyslog 只收得到 os_log),并经 DeviceSession.diagnostics() 暴露给 Dart,
写入文件日志后可用 devicectl 直接拉取,不依赖 usbmuxd。
- 新增 inCall 变化、闸门拦截、通话状态翻转、前后台切换四处观测点。
其它
- DeviceSession 新增 diagnostics()(默认空表);SmartcarSession 因是 implements
而非 extends,同步补了实现。
- CLAUDE.md 记录本次全部实测结论,含 BB 8E 为通话状态权威、BB 02/03 是噪声
(已知问题,本次未改判定)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
8b50e52572
|
client: 通话翻译字幕错位/总结残留/退不出去/切后台被冻结,及 B 路说话人门限
真机实测(Android,恒玄耳机)逐条定位后修,原生能改的都在原生层改。
字幕乱(AliyunBailianE2EHelper,Android/iOS 同改)
- 原文流与译文流各自按到达顺序自己发号,两条流的切句本来就不一致,off-by-one
一旦发生就一路错到底。改成认服务端的 item_id:译文帧的 item_id 经 itemParent
(response item → input item)映射回原文那条,取不到才退回本地发号。
- response.audio_transcript 的 text 是累计值、delta 才是增量,此前一律当增量
append,于是出现「Hello,Hello,Hello,」。改成按字段名分支:有 delta 就追加,
只有 text 就整段替换。
- connectionGeneration 守住 onOpen/onMessage/onFailure/onClosed(iOS 用 activeTask
弱引用按任务身份判),旧 socket 的迟到回调不再打进新会话;stopContinuousConversation
同步清 webSocket/isStarted。
翻译结束后「总结内容」还留在屏幕上
- AliyunAstCallback.onSessionFinished 会把整场累计文本当作一条 translated 再发
一次,表现就是一大坨总结。不再发;原文缓存改为按 utteranceId 存,上限 32 条。
结束后无法再翻译、返回按钮像被遮挡
- 退出时那颗延时 Get.delete 会误杀刚重新进来的页面。按 tag 记活跃页面数
(_PageAliveMarker + _aliveCount),还有人用就跳过销毁。
重进页面提示「未在通话中」
- 耳机对 0xD1/0xD2(进/出通话模式)的应答被当成通话状态上报,把 isCallOngoing
改掉了。这两个 type 改为只记日志,不动通话状态。
切后台功能被关掉
- 前台服务类型加 phoneCall:microphone|phoneCall (132),ROM 的进程冻结不再掐断
通话翻译。startForeground 带类型失败时回退到仅 microphone。stopService 改用
context.stopService,后台也能调。
- AST 掉线自动重连(1s/2s/4s,最多 3 次),回到前台按 _astHealthy 补一次;
重连后的新会话用 800ms 宽限期,否则会被原来的 2s 窗口吞掉重试。
B 路说话人门限(PeerSpeechGate,新增 + 11 个单测)
- 声音复刻配的是 frequency=once,只在会话开头刻一次音色。B 路会话一启动就建立,
此时对端多半还没开口,先收到的是机主的串音(实测 B 路 ASR 配 language=en 却
识别出机主刚说的中文),于是把机主音色刻成了「对端音色」,两个人听起来一样。
- 未开门期间音频进环形缓冲,开门瞬间整段补发——对端先开口时第一句的开头一个字
都不能丢。fallbackFrames 到时强行开门:门限太严会让对端整通电话不被翻译,
太松只是音色可能刻错,后者轻得多。
- 每次会话重建(含断线重连)都 reset,否则重连后照样刻到串音。
已知未尽:Android 14+ 会拒绝 phoneCall 这个前台服务类型并回退成仅 microphone,
那里的冻结保护不生效;rmsThreshold=600 尚未在真机上标定(上一轮跑到了兜底强开)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
7aef22905b
|
client: 语音翻译的语言弹窗两侧给同一张表,用过的置顶
此前左右两侧会把对侧已选的那项从列表里删掉(excludeLanguage),看起来像那个
语言没了。语音翻译(同传/面对面/音视频)不再传这个参数,选中对侧语言时由
controller 对调来避免源=目标。文本/图片翻译仍走排除——它们的源语言有「自动
检测」,对调会把这个非法值挪进目标语言。
- 对调实现体拆出 _swapLanguagesInner,不包 _runLanguageSwitch:撞上对侧时是
在已经排队进来的那次切换里调它,再包一层会等自己前面那条链,死锁。
- RecentLanguages(GetStorage)记最近选过的语言并置顶,源/目标各一份列表。
「自动检测」不是语言,不记。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
b61c4c91d7
|
client: 修助手闹钟的「同一条两张卡」与「日期落在昨天」
两处都是真机实测踩到的(2026-09-19):
- 拾忆页重复卡片:双写迁移期同一条闹钟本地与服务端各有一份,投影层只信本地
的 synced 标志,而那只是服务端事实的影子拷贝——上传成功后进程被杀、重装、
清缓存都会把它打回 unsynced,于是凭空多一条(一张能编辑、点另一张说没同步
到服务器)。改用服务端自己带的答案:client_key / source_id 存的就是指令 id,
对上了就不再投影(pendingLocalReminders,两个键都认,历史数据里有只写一个的)。
顺带补上 _uploadReminder 成功后的 reminders.refresh()——改的是 RxList 元素
内部字段,不通知没人知道,syncPending() 那条补传路径本来就 refresh 了。
- 闹钟定到过去:模型下发的 date 偶尔早一天(会话 Start 没告诉百炼今天几号),
客户端原样信了,闹钟落到昨天那一页、而且永远不会响。rollForwardPastDate 把
过去的一次性闹钟挪到就近的未来。只管一次性的——重复闹钟的 date 是锚点,落在
过去是正常的,挪了会把第一次触发推迟一周。治本要在百炼控制台注入当前时间。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
f9b9c8c72d
|
client: 翻译默认归档进语音纪要;列表改本地优先;登录按包收敛;EMAI 走 A2DP
## 通话/同声/面对面翻译 → 语音纪要
三种翻译模式默认边翻边录,结束时把音频**连同已有转写**一起归档
(rtype=VOICETRANSLAT),服务端跳过识别、直接用智能模板(General-Autopilot)总结,
在会议纪要里就能看到。转写段的构造见 translation_export.dart(纯函数,带金测试):
- 时间是**相对录音起点的毫秒**并扣掉暂停区间——面对面是逐句按住说话、松开就暂停的,
不扣的话越往后偏得越多;写成绝对时间戳会让点一句跳到音频末尾之外;
- 说话人用 Speaker_0/Speaker_1,与服务端转写同一口径(转写页直接把这个字段当文案显示,
裸 0/1 会和别的纪要长得不一样,喂给 LLM 抽待办时也会混出 Speaker2 这种负责人);
- AST socket 断开后 _terminateCallOnError 会取消事件订阅,重开时收不到任何转写
(真机上表现为「有日志但转写 0 段」),改成重新订阅。
## 语音纪要列表:服务端是真源、本地 sqlite 是缓存
列表接口改回骨架字段后(服务端同批),客户端改成本地优先渲染 + 后台对齐:
- Synchrodata 重写:拉骨架 → upsert → 删掉「本地有服务端没有」的记录(连本地音频、
上传队列、轮询任务一起清)。**业务错误时 data 是 null 不是空列表**,判空错了
会把本地整个清空;
- 详情进页面时才拉整条;离线操作队列那套上行是死代码(服务端接口整个注释掉了),删除。
## 登录按包收敛
苹果包只留苹果+游客、安卓包只留微信+游客、谷歌包只留邮箱+游客。
判定抽成 resolveLoginMethods 纯函数(login_methods.dart + 8 条测试),
邮箱包还要锁死国家区号选择。
## EMAI 与其它
- 手机侧触发的对话走 A2DP 而不是语音处理通道:iOS 上 A2DP 输出与 voiceChat 模式
互斥,开了后者蓝牙耳机会当场切到手机扬声器。按当前实际输出路由决定要不要开
voiceProcessing/回声消除;
- EMAI「开始对话」闪退:探测输入格式时 new 了一个一次性 AVAudioEngine 去读 inputNode,
配置变更时崩在 AVAudioIONodeImpl(EXC_BAD_ACCESS)。改成直接问 AVAudioSession;
- 语音纪要文件名可改:每行右侧加铅笔图标,复用已有的重命名弹窗(原来只有长按菜单,
实际入口已不可达);
- 拾忆上报 IANA 时区名:DateTime.now().timeZoneName 给的是「CST」这种缩写,
服务端 LoadLocation 解析不了、静默退回容器时区,海外用户的提醒时刻会全按北京时间算。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
41e3a023a3
|
client: 蓝牙名白名单(EaiRec/EaiCar);连接即按账号登记绑定;iOS 崩溃加固;弱网翻译超时
- 设备名闸门:支架只认 EaiRec/EAIMAR/Pad Note,车载香薰只认 EaiCar,固件名对不上直接不连;
一次性清掉此前被归一化名字污染的配对记录;Smartcar 资源与 agent 命名统一改为 EaiCar
- 绑定登记:新增 DeviceBindRegistry(按 uid|MAC 去重),支架也进 verifyConnected,
登录态从无到有时补报一次,换账号不再漏切绑定
- iOS 崩溃加固:补 NSPhotoLibraryAddUsageDescription(保存思维导图/统计图必崩);
StsAgent 先配音频会话再 installTap 并校验格式;支架录音两处 installTap 加 0Hz 守卫;
AzureTtsHelper 播放队列改加锁队列(三线程裸改 Array,release 下崩);
MicrophoneCapture 主线程不再用信号量等权限弹窗(看门狗误杀)
- 网络:connectionError 给中文文案;实时翻译请求 12s 超时,不再跟 30s 默认值等
- 附:AI 助手中枢设计文档 v0.2(未动代码)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
4 weeks ago |
Rodger-Wang
|
c14580d7db
|
client: 设备默认图去背;AI 页编排跟随设备;香薰 AI 回复走蓝牙;OTA 状态如实显示
设备默认图
- default.png / default_icon.png 是 assets/images/devices 里仅有的两张**不透明带底图**
(892×1152 竖版海报 + 浅蓝渐变底),而设备页展示区是 1.14:1 的横宽框,
BoxFit.contain 后成了中间一条窄的蓝色卡片、左右大片留白,深色模式下尤其突兀。
改成去背透明(948×828 / 176×154),主体自己铺满框、周围就是页面背景,代码零适配。
- device_images.dart 记下这条约定,免得以后又丢一张竖版带底图进来。
AI 页按设备铺编排(连着香薰却显示全量功能)
- _load() 的一次性迁移会改 enabledIds 却**不更新基线 _lastAppliedKey**,
尤其 _migrateDefaultLayout 会把 defaultEnabled 里缺的全部 append 回来 ——
香薰编排铺成 3 项,重启后被追加成 11 项,基线还停在 3 项,
于是 applyDeviceDefaults 判定「用户动过编排」,从此再也不铺,且怎么重连都恢复不了。
现在:当前编排若是自动铺的就整个跳过这些迁移;已经坏掉的装机由
_rebaseDeviceDefaultsBaseline 一次性把基线抬到现状(判据收紧到迁移留下的特征形态)。
- AgentDefaults.bind 补跑一次当前状态:GetX 的 ever 拿不到绑定那一刻已有的值,
而设备是被动回连的,完全可能赶在 bind 之前就绪。
- 断开设备后回退到 DeviceHub.lastKnownVendor:设备图和「设备管理」在断连后都显示
最近那台设备,AI 页却停在上一台铺的编排里,于是出现「图是耳机、功能是香薰」。
为此给 DeviceHub 加 lastKnownVendor,并把它与 lastKnownName 抽成同一个来源 ——
各算各的就还会自相矛盾。
- test/agent_module_defaults_test.dart 6 个用例守这条规则。
车载香薰唤醒 AI 后回复从手机扬声器出来
- 下行本来就设计成走经典蓝牙 A2DP,坏在共用的音频会话上:defaultToSpeaker 把输出
钉在内置扬声器,选项里只有 allowBluetooth(HFP)而没有 allowBluetoothA2dp
(playAndRecord 下 A2DP 输出默认是关的)。而这条链路采音在设备上走 BLE opus,
手机压根不录音,用 playAndRecord 本身就是错配。
- EmaiAudioSession 分档:香薰走新的 playbackOnly(.playback 类别,输出跟随系统路由),
EMAI 页「按住说话」仍是 playAndRecord(去掉 defaultToSpeaker 会退回听筒,更糟),
只补上 allowBluetoothA2dp。原生 PcmSpeakerPlayer 明确不碰 AVAudioSession,改这一处即可。
固件升级页
- 后台没给产品配过固件时,页面显示的是绿色的「已是最新」——这是假话,会让人以为
手上固件是最新的而不再去查。改成三态:找不到产品配置 / 服务端暂无固件版本 / 有新版本·已是最新。
新文案 otaNoServerVersion 已补齐 41 个语言文件(i18n_audit total_missing=0)。
- 产品找到但版本为空时打一条点名 warn,不再和「拉取失败」混在一起。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
4 weeks ago |
Rodger-Wang
|
e00b617025
|
services,admin,client: 服务配置按应用隔离、应用参数下线、客户端凭据走加密 v3
后台
- 服务配置去掉「全局默认」层,svc_config / svc_region_override / echomeet_orch
/ echomeet_orch_setting / echomeet_template 五张表统一按 app_name 隔离,
启动迁移 migrate_scope.go 幂等分配(首个应用原地改名保 id,其余拷贝);
写接口 app_name 必填(scope_check.go)。
- 类别落库 svc_category,可增删改;内置 11 个 id 不可改不可删,运行时语义仍只挂 comm.SvcCat*。
- 「应用参数」标签页下线:业务库 config 表不再随 user_getappconfig 下发,
migrate_appparams.go 把 AGENT_TYPE / MOBILE_ELF_* 迁到服务 llm_mobile_elf、
iapCustomerServiceQQ 迁到 app_params,COMPUTE_*/NEWUSER_GIFT_* 留在 config 表不下发,
无人读的音乐/公码/导航/COS_*/MeetServers 删除,其余键保留并在启动日志告警。
- 删除 console appcfg/* 与 api 模块的 get/add/update/delconfig、唤醒音四个接口(无调用方)。
- 修 getmcpservers 仍查全局层导致 MCP 类服务永远读不到。
客户端
- 凭据改走结构化通道:user_getappconfig_v3 整体 AES-CBC 加密(api_crypto.dart,
拦截器须排在 AuthInterceptor 之前),AppConfig.cred(svcId, field, envKey) 取值,
老键名只作兜底;lib 内 AppConfig.env( 已降到 0 处调用。
- 图片翻译的 ALIBABA_VL_MODEL / ENDPOINT 改挂 ast_alibaba。
启动日志
- 删除 sys/auth/firebase(google_auth 的重复实现,零调用方,且同一错误刷两行)。
- api 模块不再每次启动无条件插入默认超管(Duplicate entry 'admin')。
- sys/auth/* 五个包的 logger 名从复制来的 sys.tavily 各改各名;
google_auth 启动时即判断配置是否可用。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
4 weeks ago |
Rodger-Wang
|
27cabe1bde
|
client: 设备内核抽象、Smartcar 车载香薰接入与语音纪要等一批改动
工作区里累积的 Flutter 客户端改动,随本轮一并入库。主要涉及:
- lib/devices/ 设备内核:DeviceHub / DevicePlugin / DeviceSession 抽象,
恒玄(bes)、EaiRec 支架(holder)、Smartcar 车载香薰(smartcar) 各自的插件实现,
业务层不再直接依赖任何厂商类;分层边界由 test/devices/ 下的测试守着
- local_plugins/device_plugin_interface、device_smartcar、recorder_holder:
纯 Dart 契约与原生侧实现
- lib/modules/ 能力模块注册表、语音纪要、OTA 升级等模块
- test/ 新增设备协议、Ogg Opus 时长、模块注册表、路由等金测试
- ios/sync_spm_platform.sh:修 Xcode 直接构建时 SPM 平台版本被打回 13.0 的问题
验证:dart analyze lib 无 error(264 条 warning 均为 HEAD 上既有的 unused_import /
avoid_print 等 lint,非本次引入)。未跑真机与打包。
注:这批不是本次会话中由 Claude 编写的,此处仅做入库。
|
4 weeks ago |
Rodger-Wang
|
c95996c251
|
修 iOS 上切到同传/面对面时字幕小窗关不掉、且中断 changeTranslationMode
_floatingWindowManager 是 late 且只在 onInit 的 `Platform.isAndroid` 分支里赋值
(iOS 走 initPlatformState 那套 SubtitlePip)。全项目 5 处引用里 4 处都带平台守卫,
**只有 _disableFloatingWindow() 那处没有** —— 而它恰恰会被 changeTranslationMode 调到
(从多媒体/通话翻译切到同传或面对面时,小窗不受支持要自动关掉)。
iOS 上后果是连锁的,且没有一条指向真因:LateInitializationError 从
changeTranslationMode 中途抛出,后面的 _reinitializeAsrService() 再也走不到;
小窗又没真的停掉,继续浮在同传/面对面页上盖住顶部的标题与按钮;偏偏小窗开关按钮
只在 call/audioVideo 两个模式才渲染,用户连关都关不掉。
- _disableFloatingWindow() 分平台,并收敛成 toggle / onClose / changeTranslationMode
三处共用的唯一出口;Android 分支补 Get.isRegistered 守卫。
- 切到不支持小窗的模式时改成**无条件**关一次,不再依赖 isFloatingWindowEnabled:
iOS 上这个标志位由 initPlatformState 里那次异步的 SubtitlePip.isActive() 回填,
和 onInit 紧接着调用的 changeTranslationMode 是竞态,回填晚一步就整个跳过。
没开着时多关一次是无害的。
真机验证(iPhone 12 / iOS 26.6.2):改前在运行中的 App 里直接取 _floatingWindowManager
确实抛 LateInitializationError;改后走完整路径(多媒体翻译 → 开小窗 → 切同传),
changeTranslationMode 正常返回、pip=false、widget 树里 ErrorWidget 数为 0。
安卓侧(华为 ALN-AL80,debug + release)同传/面对面两页渲染不受影响。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
4 weeks ago |
Rodger-Wang
|
63634532ce
|
语音纪要等待态换成自绘动效;修 tasktype 存两张表导致重进后等待态消失
## 等待态
原来是 assets/lottie/meeting_wait.json —— 一张「JOIN」对话气泡插画,和本页在做的事
(把音频变成文稿)没有任何关系,配色也与全站的 222° 冷蓝灰不搭,深色主题下更是一块
突兀的白。改成自绘:上面一排声波表示音频正在被读,下面几条带流光的骨架行预告即将
生成的文稿形状,两者共用同一个 AnimationController。
自绘而不是再换一个 Lottie:① 不用为一个等待态再背一份 json 资源;② 颜色全部走
AppPalette,明暗主题自动跟随,插画做不到;③ 四个 tab(概览/发言/全文/思维导图)
共用同一个等待态,自绘好调。meeting_wait.json 一并删除(全仓零引用;assets/lottie/
在 pubspec 里是按目录声明的,不用改配置)。
## 重进后等待态消失 —— tasktype 在两张表里各存一份
真机上那条记录的实际状态是 meeting.tasktype=1 / meetingdetails.tasktype=0:
列表页读前者,显示「转写中」;详情页读后者,拿到 0,于是什么等待态都没有。
两张表分头写,而且中间态漏了:
- submitTask / refreshTask / readTask 只调 editMeetingTitle,**只写 meeting**
- _queryTask 里写 meetingdetails 的两次都藏在里程碑分支里(>=3、==5/6),
1→2(等待转写→转写中)这一跳、以及失败态 10002,meetingdetails 一次都不写
首次进入之所以有动画,靠的是详情控制器里那句内存赋值 meetingDetails.value.tasktype=1;
控制器一销毁,重进读库就是 0 —— 表现为「退出重进特效就不见了」。
改法:
1. SqfliteApi.setMeetingTaskType(id, type):一个事务同时写两张表,成为写 tasktype 的
**唯一入口**。⚠️ 别再直接 editMeetingTitle({'tasktype': x}),那只写一张。
2. 轮询里的状态推进从两个里程碑分支里提到分支之后无条件执行 —— 数据先落、状态最后推,
详情页的 ever(tasktype) 监听读到新状态时数据已经就位。tasktype.value 同样不再只在
分支里更新,1→2 这一跳现在也会通知 UI。
3. 详情页以 meeting 表那份为准(它是历史上唯一每次状态变化都会写的),给**存量**错位
数据兜底:不用等下一次状态跳变就能立刻正常。
真机验证(华为 ALN-AL80):用那条 details=0/meeting=1 的错位记录进详情,等待态正常
(改前是空白页);退出重进,等待态仍在;退出后查库两张表都是 1;轮询把状态推到失败态后
两张表都是 10002(改前 details 恒为 0);终态下等待态正确消失、显示「分析失败」。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
4 weeks ago |
Rodger-Wang
|
110cbcb026
|
prod-deploy: 阿龙段的 SSH 私钥路径改为可被环境变量覆盖
三个 prod-deploy.sh 的 along 段都写死 /Users/liwei1dao/liwei/密钥/...,
该路径只在原作者机器上存在,换一台机就跑不了部署。改成
${DEPLOY_KEY:-<原路径>},默认值不变(原机器行为不受影响),
其它机器用 DEPLOY_KEY=~/xxx.pem ./prod-deploy.sh 即可。
写法与 dev-deploy.sh 的 along 段保持一致。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
abd0922d78
|
设备图改成按设备名从本地取;修同传页 GetX 报错遮挡与 AI 页 2px 溢出
## 设备图:关键字映射 → 按名字取图
原来是一张写死的映射表(名字里含 echo-one/deepvoice/buds… 就用 earphone.png),
加一个产品就要回代码里加一条。改成直接按设备名找
`assets/images/devices/<设备名>.png` 与 `<设备名>_icon.png`,
加新产品只要按这个命名把图丢进目录——pubspec 里该目录是**按目录**声明的,
新文件自动打包,代码和配置都不用动。
四个展示点统一到 DeviceImages:设备页主图、设备管理列表小图标、
恒玄固件升级页、杰理固件升级页。其中:
- 设备管理小图标原来是 Material 图标 + 紫色渐变块,现在换成产品实拍图,
底色仍区分连接状态(换成图之后原来靠渐变色传达的「这台连着」就没了)。
⚠️ 这一页取图用的是**服务端下发的 devicename** 而不是当前蓝牙广播名——
它列的是绑定关系,可能包含此刻没连上的设备,用当前连接名会让所有行显示成同一台。
- 杰理固件升级页原来拉服务端 productimage 网络图,一并去掉:产品图统一走本地,
留着会出现「同一台设备在设备页显示本地图、在升级页显示另一张网络图」。
匹配三级,从严到宽:完全相同 → 忽略大小写 → 最长前缀(兜 `Echo-one_L`
这类带后缀的广播名)。第三级必须取**最长**匹配,否则目录里同时有 `Echo` 和
`Echo-one` 时前者会把后者的设备抢走。
启动时读一次 AssetManifest(main.dart)记下目录里实际有哪些图。不做这一步就只能
靠 Image.asset 的 errorBuilder 在渲染失败时兜底,那是**静默**的——美术漏出一张图
或命名对不上,界面直接变通用图,没有任何地方看得出来。现在会打 warn。
资源侧:earphone.png → Echo-one.png(同一张耳机实拍图,只是改名以适配新规则),
补上缺失的 default_icon.png,删掉已无引用的 earphone*/record_card*(省约 1.6MB)。
## 修同传/面对面页「一层阴影盖住标题和返回键」
不是阴影,是 Flutter 的错误控件。translation_view 的 AppBar 里有个 Obx 写成
`Platform.isIOS && controller.currentMode.value == ...`,安卓上 && 短路,
右半边根本不执行,这个 Obx 一个可观测变量都没订阅到,GetX 判「用法不当」抛错,
错误控件顶替了整条 AppBar(debug 红框、release 灰框)。iOS 上 Platform.isIOS
为真会继续求值,所以只有安卓复现。把可观测变量提到平台判断之前读即可。
全仓扫过,同样写法只此一处。
## 修 AI 页三张卡片 RIGHT OVERFLOWED BY 2.0 PIXELS
VIP 区容器有一圈 Border.all()(默认宽 1.0),Border 画在 padding 内侧、左右各占
1px,而列宽计算只扣了 padding 没扣 border,于是正好多出 2px。把 border 宽度提成
具名常量与计算共用,免得改一处又错位。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
714a05b6d4
|
修 iOS EMAI 没有语音播报、连续对话上行断供;接入 MCP 令牌
iOS 上助手一直不出声,和这次改连续对话无关,是播报本来就坏的。三轮真机对照
实测:just_audio 喂的是「长度未知的直播 WAV 流」,AVPlayer 只发一次
`Range: bytes=0-1` 的嗅探请求就判定资源不可播,回 -11850 且 processingState
直接落 idle,一次都没真正起播。声明长度、支持 range、把收到的字节全缓存下来
允许重放,三种都试过,结论一样。ExoPlayer 不发这个嗅探请求,所以安卓一直是
好的——这就是「安卓有声、苹果没声」的全部原因。
iOS 改走原生 AVAudioEngine 排队播 PCM(PcmSpeakerPlayer),安卓不动。
验证方式:一次性灌 3 秒音频,7ms 喂完但 3708ms 才播完,证明是实时渲染不是丢弃。
连续对话的上行也是断的,服务端一直回 ClientAudioTimeout。两个原因:
- _ensureContinuousMic 被 Started 事件和 _enterContinuous 并发调用,两边都在
`_micRunning` 置位前通过守卫,同一个 recorder 被 startStream 两遍,第二次把
第一次那条流顶掉。控制器侧改成同步置位,MicPcmStreamer 内部再加一层 in-flight 去重。
- 开麦前没配好并激活音频会话。会话停在 soloAmbient 且未激活时,startStream
会**成功返回**、引擎也起得来,但输入 tap 一个 buffer 都不回调,客户端毫无报错。
麦克风和播放器统一到 EmaiAudioSession 一份会话配置:各配各的会在连续对话下
互相覆盖。iOS 固定 voiceChat——record 的 echoCancel 会打开 voice-processing 并
把 mode 改成 voiceChat 持有着,播放这边写回 default 就是两边互相掀桌子。
androidAudioAttributes 仍是 media,别改成 voiceCommunication(华为会静音)。
连续对话交互按要求改:进页面只建会话不开麦,短按开始、再按结束、可反复,
退出即停,10 秒无对话往来自动结束。计时依据必须是对话事件不是音频包——
麦克风没人说话也在稳定出包,拿它续命这个超时永远不会触发。
另修 _enterContinuous 在 reconnect() 一返回就判 `!= ready`:ready 要等服务端回
Started(一次网络往返),几乎每次都命中,于是刚打开的连续对话被当场关掉,
而 Started 稍后到达又会把麦开起来,留下一个界面上看不见的常开麦克风。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
6299920145
|
点结束时自动关掉屏幕直播;拾忆页去掉手动新增的 + 号
1) 多媒体翻译点「结束」后,系统那条屏幕直播不再挂着
主 App 对「用户从系统面板发起的广播」没有任何控制权——RPBroadcastController
只管 App 内 API 起的那种。唯一能终止它的是扩展自己调
finishBroadcastWithError。所以改成让扩展感知主 App 是否还在:
- stopAll() → _teardownCallModeAudio() → stopScreenCapture() 关掉 socket
(这条链原本就有)
- 扩展侧:曾经连上过、又断开超过 3 秒连不回来 → 判定主 App 收工,结束广播。
没连上过就断开不算——那是「先开广播、还没点开始翻译」的正常情况。
宽限期不能太短,主 App 瞬断会误杀用户正在用的广播。
- 另加 1 秒心跳轮询:只靠 processSampleBuffer 里检查是不够的,视频停了或者
没有任何 App 出声时 audioApp 帧可能长时间不来,那样永远发现不了主 App
已经走了,屏幕上那条红条会一直挂着。
⚠️ finishBroadcastWithError 必须传 NSError,系统会把 localizedDescription
弹给用户,所以给的是一句正常说明而不是错误信息。
2) 拾忆页去掉右下角的 + 号
内容应当由助手/会议纪要自动落进来,手动新建不是当前需要的能力。
卡片点开编辑保留(TodoScopeList 里)——那是改助手记错的时间/内容用的,
与「手动新建」不是一回事。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
9abed0008b
|
字幕小窗:加暂停/继续、去掉快进快退、大小与透明度可调
- 暂停/继续:实现 AVPictureInPictureSampleBufferPlaybackDelegate 的
setPlaying / isPlaybackPaused。语义定成「冻结当前这条字幕」而不是
「暂停翻译」——用户在别的 App 里看视频、想把一句话定住多看两眼,这是小窗
那颗按钮唯一说得通的含义,而且完全可逆、不影响正在跑的识别与翻译。
冻结期间新文案只暂存,恢复时补上最新一条。
⚠️ setPlaying 里必须调 invalidatePlaybackState(),否则按钮图标不跟着变,
点了「暂停」它还画着暂停图标,看着像没生效。
- 快进快退:requiresLinearPlayback = true 就是这个开关(原本已设,这次补了
注释说明它的作用,免得以后被当成无关配置删掉)。
- 大小 / 透明度:翻译页新增一颗 tune 按钮,弹出设置面板,两个滑块,
GetStorage 持久化。新增 3 个 i18n key(pipSettings/pipSize/pipOpacity),
41 个语言文件全部补齐,主要语种给了本地化文案、其余回退英文。
三条平台限制已写进注释与面板说明,避免以后重复尝试:
- 小窗**实际大小由用户捏合手势决定**,App 无权直接设定。「大小」滑块改的是
画布宽高比,系统按新形状重新布局,观感上是变宽/变窄。
- 透明度**做不到透出下面的 App**。iOS 的 PiP 窗口本身不透明,系统未开放接口。
滑块只改卡片底色深浅。要少挡内容只能把小窗调小。
- 播放/暂停按钮的**尺寸不可控**,那是系统绘制的 chrome,随小窗一起缩放——
所以「按钮小一点」等价于「把小窗调小」。pip 那个 pub 包用私有 API
_setControlsStyle 去动控件,上架有被拒风险,不采用。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
56aa55773d
|
字幕小窗改成自绘:pip 包只喂了一块纯黑像素,字幕根本进不去小窗
用户反馈「悬浮弹幕上没有显示翻译内容」,根因在 pub 包 pip 0.0.4:
PipView.m: const int pixel = 0xFF000000; // BGRA 全黑
填满 pixelBuffer → enqueue 进 AVSampleBufferDisplayLayer
小窗显示的就是这一帧纯黑占位。真正的字幕卡片它是靠
insertContentViewIfNeeded 塞进 activeKeyWindow 的,而 PiP 窗口由系统进程托管、
**不在 UIApplication.connectedScenes 的窗口列表里**,那个 keyWindow 永远是
App 自己的窗口。于是字幕落在 App 身上(上一版还因此盖住整个屏幕点不动),
小窗里只可能是黑的。PipView.m 全文件 0 处引用 contentView,可以佐证。
改成自己实现:SubtitlePipController 把 PlayerView 画进 CVPixelBuffer,
逐帧喂给自己的 AVSampleBufferDisplayLayer,再用 sampleBufferDisplayLayer
这一路 content source 起 AVPictureInPictureController。小窗显示的就是它。
Dart 侧不再依赖 pip 包(import 与相关字段一并删除)。
同时回答另外两个问题:
- 大小:PiP 小窗的实际尺寸由用户捏合手势决定,App 无权直接设定。能控的是
**宽高比**(= 画布宽高比),setPipWidthScale 改的是这个,系统会按新形状
重新布局。另外卡片有内容时自动变高、没内容时收成一条,也走同一条路。
- 透明度:**做不到透过小窗看见下面的 App**。iOS 的 PiP 窗口本身不透明,
系统没有开放任何接口。setPipOpacity 只作用于卡片自己的底色(在小窗那块底
之上显得深浅不同)。真要少挡内容只能把小窗做小或拖到别处。
这一条在 Dart 与原生两侧的注释里都写明了,免得以后有人再试一遍。
几处实现要点:
- 心跳定时器每秒补一帧。只在文案变化时推帧的话,长时间没有新帧会让
AVSampleBufferDisplayLayer 被判定停滞并清空,表现是「翻译停一会儿小窗就黑」。
- start() 之前先推一帧:没有任何已入队帧时 AVPictureInPictureController
会判定内容源为空,直接回调 failed。
- 渲染上下文要做一次 y 轴翻转,否则字幕上下颠倒(UIKit 原点左上、CG 左下)。
- renderView 必须挂在真实窗口层级里且尺寸非零,脱离层级的 layer 不被 PiP 采纳。
- 时间轴返回无限区间,让系统按直播流处理,小窗上不出现进度条和快进按钮。
⚠️ 构建注意:给本地 pod 加新文件后必须重跑 pod install,否则文件不进 Pods
工程,报 "Cannot find 'SubtitlePipController' in scope"。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
8377745992
|
iOS 多媒体翻译接入 ReplayKit 广播扩展,真正拿系统音频
iOS 不允许一个 App 读另一个 App 的音频,Broadcast Upload Extension 是系统
留的唯一口子。链路:
其他 App 播放 → 系统录屏广播 → BroadcastExtension(独立进程)
收 RPSampleBufferType.audioApp → 转 16k/mono/Int16
→ App Group 容器下的 Unix domain socket
→ 主 App BroadcastAudioReceiver
→ azureAsrHelper.audioStream.saveAudioDataTo() ← 已有的 external 通路
→ ASR → 翻译 → 弹幕
新增:
- ios/BroadcastExtension/{SampleHandler,BroadcastAudioSocket}.swift、Info.plist、
entitlements、Extension.xcconfig
- azure_speech 插件的 BroadcastAudioReceiver.swift + 三个 method channel
(startBroadcastAudioReceiver / stopBroadcastAudioReceiver / presentBroadcastPicker)
- Runner.entitlements 加 App Group;xcodeproj 加 BroadcastExtension target
并在 Runner 里内嵌
对上层复用已有的 requestScreenCapture/stopScreenCapture 接缝(Android 是
MediaProjection,iOS 是广播扩展),控制器里两端走同一条分支。
几处踩过的坑,都写进了注释:
- SOCK_STREAM 不保留消息边界,帧必须带 4 字节长度前缀,否则喂给 ASR 的是
错位样本。
- CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer 不拷贝数据,只把
mData 指向 blockBuffer 的内存;blockBuffer 出作用域即释放,交出去就是野指针。
改成在 withExtendedLifetime 里 memcpy 出来。
- AudioBufferList 的 buffer 个数要按 interleaved 与否算,写死 1 会让非交错
立体声整帧拿不到。
- sun_path 只有 104 字节,socket 只能放在 App Group 容器根下,不能建子目录。
- 扩展侧 connect 失败要限流重试(1s),每帧重连会把扩展 CPU 打满,而它的
内存上限只有 50MB。
- SO_NOSIGPIPE:主 App 挂掉时写 socket 默认会直接杀死扩展进程。
- media 这个 ASR 模式的会话配置必须排在 audioSourceType 判断之前,且要
.mixWithOthers —— 否则我们一 setActive 就把正在播放的那个 App 掐了。
- Embed App Extensions 阶段必须插在 Thin Binary 之前。追加到最后会和
Flutter/CocoaPods 的脚本阶段构成依赖环,Xcode 报 Cycle inside Runner。
⚠️ 尚未打通签名:需要在苹果开发者后台登记 App Group group.com.yimai.eaimar、
给 com.yimai.eaimar 开 App Groups capability、并为
com.yimai.eaimar.BroadcastExtension 建 App ID。Xcode 当前还登录不上账号
(-1200)。在这之前 iOS 包签不出来。代码已用 --no-codesign 验证可编译、
appex 也正确内嵌进 PlugIns/。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
5e2b4f5431
|
修 iOS 多媒体翻译一个字都翻不出来:ASR 在等一份永远没人推的 PCM
根因是 iOS 分支还停在杰理时代的假设上。_configureAudioVideoMode 的非 Android
分支做的是 openDecoder() → 等 isCodecActive → 音频源置 external,语义是
「从耳机的 A2DP 解码器拿手机正在播放的音乐」。本项目只有恒玄耳机,没有这个
能力;而 openDecoder() 如今是个乐观桩,只把 _isCodecActive 置 true 就返回。
于是 iOS 的实际行为是:ASR 以 external 源启动,然后等一份永远不会有人推的
PCM——不报错、不提示,就是一个字都翻不出来。Android 早已整条换成
MediaProjection(isAndroidAudioVideo 分支),iOS 这条没跟着改。
iOS 平台层面也没有「抓其他 App 音频」的能力,唯一合规途径是 ReplayKit
Broadcast Upload Extension(需新建 extension target + App Group,且要用户
手动发起录屏广播)——那是产品决策,本次没做。先退化成手机麦克风外放收音,
让功能是通的:
- _configureAudioVideoMode 的 iOS 分支不再碰杰理解码器;
_audioSourceType = Platform.isAndroid(Android=external/MediaProjection,
iOS=麦克风)。
- 启动守卫里排除 iOS 音视频。那个守卫要求 isCodecActive,而它只由
openEncoder/openDecoder 这两个杰理血统的入口置位——不排除的话直接 return,
识别根本不会启动。
- iOS 音视频用新的 'media' ASR 模式,原生据此把 AVAudioSession 配成
.playAndRecord + .default + [.mixWithOthers, .defaultToSpeaker,
.allowBluetoothA2DP]:
· .mixWithOthers 是前提——不加的话我们一 setActive 就把正在播放的那个 App
掐了,点开翻译视频就哑了;
· mode 用 .default 不用 .voiceChat——后者会让 MicrophoneCapture 打开
setVoiceProcessingEnabled,而要收的恰恰是本机扬声器的声音,AEC 会把它
当回声精确消掉,结果一片安静。
iOS 侧只有两处 currentRecognitionMode 比较(phone_call / push_to_talk),
新增的 media 落到 normal 的默认分支,不影响其它模式。
- enableRecord 的音频源跟着改成 microphone。留 systemAudio 的话原生把
index>0 一律当 external,会建好文件然后等没人推的 PCM,存下来是个空录音。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
8841e76e59
|
修 iOS 多媒体翻译开弹幕后整屏点不动
三段代码叠出来的,安卓不受影响(走的是系统悬浮窗,另一条路):
1. pip 包(pub: pip 0.0.4)在 PiP 启动回调里,把 contentView insertSubview 进
**App 自己的 keyWindow** 并 `frame = window.frame` 铺满全屏。它的
activeKeyWindow 只按「前台活跃场景的 keyWindow」找父视图,而我们是在前台
主动 _pip.start() 的,于是必然落到 App 自己的窗口上。
2. PlayerView 是普通 UIView,isUserInteractionEnabled 默认 YES,背景还是
rgba(40,40,40,0.35)。铺满之后=一层吃掉所有触摸的半透明黑罩子。
3. 关掉弹幕也恢复不了:插件的 restoreContentViewIfNeeded 在
originalParentView == nil 时直接 return,而 PlayerView 是 createPipContentView
里凭空 new 出来的、从来没有过 superview,那条还原分支永远走不到。
罩子会一直留到进程被杀。
改动(都在我们自己这边,没有 fork pub 包):
- PlayerView.isUserInteractionEnabled = false。字幕卡片是纯展示,任何情况下
都不该吃触摸——这一条单独就能解掉「点不动」。
- 视觉(底色/圆角/描边/阴影)从 self 搬到内层 cardView,self 透明;
cardView 只钉 top/leading/trailing + 固定高度,不钉 bottom。真 PiP 小窗里
窗口高度就等于这个高度、看着是铺满的;万一又被铺到全屏窗口上,也只占顶部
一条,下面保持透明。
- disposePipContentView 里补 removeFromSuperview(插件不会替我们摘)。
- Dart 关闭弹幕/PiP 失败时调 _releasePipContentView(),并把 _pipContentView
清零,避免复用一个已经被搬走、再也摘不下来的 view。
⚠️ 未验证:PiP 小窗本身能不能正确显示字幕。pip 包的 PipView 只往
sampleBufferDisplayLayer 里塞自己造的 buffer,全文件没有一处引用 contentView,
所以「切出 App 后小窗里有没有字」这件事需要真机确认,本次没测。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
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
|
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
|
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
|
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
|
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
|
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 |