新增: 小程序配置持久化与心跳闭环 + 文档同步
小程序(mobile client)三项改进: 一、配置持久化(分发前置) - 运行时配置(relay 地址/设备 ID/token/心跳间隔)接入 storage - 懒加载:首次读取从 storage 恢复,合并默认值兼容旧字段 - 新增重置函数:清除 storage + 回到默认占位值 - 读写失败降级不崩溃 二、心跳 pong 闭环(移动网络可靠性) - 中继服务识别小程序心跳 ping,直接回 pong(不经桌面端透传) - 小程序收到 pong 更新入站时间,半连接看门狗据此判活 - 治移动网络 TCP 半连接挂死(socket 仍开但无响应) 三、文档同步关闭已完成项 - 条件表达式引擎:求值器+执行器+前端编辑入口+翻译均已落地 - 流式状态按会话隔离:accessor 委派 per-conv Map 已落地 - 灵感录入增强:表单已含来源/标签/优先级字段
This commit is contained in:
@@ -341,7 +341,7 @@ class WsClient {
|
|||||||
}
|
}
|
||||||
const text = res.data
|
const text = res.data
|
||||||
if (!text) return
|
if (!text) return
|
||||||
// 活性:任意入站帧更新(relay 无 pong,用心跳期间入站消息替代判活)
|
// 活性:任意入站帧更新(含 relay 回的 pong 心跳响应,用于半连接检测)
|
||||||
this.lastInboundAt = Date.now()
|
this.lastInboundAt = Date.now()
|
||||||
|
|
||||||
// 握手阶段可能收到 relay 发的错误控制帧(relay.rs:200-238)
|
// 握手阶段可能收到 relay 发的错误控制帧(relay.rs:200-238)
|
||||||
|
|||||||
@@ -674,7 +674,14 @@ function subscribeWs(): void {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onControl: (msg: ControlMessage) => {
|
onControl: (msg: ControlMessage) => {
|
||||||
// 心跳响应等,miniapp 仅 console(不维持在线状态 UI)
|
// 心跳 pong 响应:relay 收到 miniapp ping 后直接回 pong(不经 device),
|
||||||
|
// miniapp 据此更新 lastPongTime 看门狗(ws.ts 已维护 lastInboundAt,但该路径不经过
|
||||||
|
// onControl,这里记录到变量供日志诊断)。pong 闭环治移动网络 TCP 半连接挂死。
|
||||||
|
if (msg.control_kind === 'pong') {
|
||||||
|
console.debug('[useAiChat] 收到心跳 pong(relay 响应,连接健康)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 其他 control 消息(hello_ack / pair 等)仅 console 不维持在线状态 UI
|
||||||
console.log('[useAiChat] control', msg)
|
console.log('[useAiChat] control', msg)
|
||||||
},
|
},
|
||||||
onStatus: (status: WsStatus, detail?: string) => {
|
onStatus: (status: WsStatus, detail?: string) => {
|
||||||
|
|||||||
@@ -42,13 +42,71 @@ export const defaultConfig: MiniappConfig = {
|
|||||||
reconnectMaxDelay: 30000,
|
reconnectMaxDelay: 30000,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单例配置(miniapp 内存配置,后续接 storage 持久化) */
|
/** storage key(持久化完整 MiniappConfig JSON) */
|
||||||
let _config: MiniappConfig = { ...defaultConfig }
|
const STORAGE_KEY = 'df-miniapp-config'
|
||||||
|
|
||||||
|
/** 单例配置(null 表示尚未从 storage 加载) */
|
||||||
|
let _config: MiniappConfig | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 storage 读取并合并默认配置
|
||||||
|
*
|
||||||
|
* uni.getStorageSync(key) 未命中时返回 ''(空字符串),命中返回原写入值。
|
||||||
|
* 容错:读取/解析失败仅 warn,不抛出,回落到 defaultConfig。
|
||||||
|
*/
|
||||||
|
function loadFromStorage(): MiniappConfig {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(STORAGE_KEY)
|
||||||
|
if (!raw) return { ...defaultConfig }
|
||||||
|
const parsed = JSON.parse(raw) as Partial<MiniappConfig>
|
||||||
|
// 合并默认值,避免旧版本字段缺失导致 undefined
|
||||||
|
return { ...defaultConfig, ...parsed }
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[df-miniapp] load config from storage failed:', e)
|
||||||
|
return { ...defaultConfig }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步写入 storage(JSON 序列化完整配置)
|
||||||
|
*
|
||||||
|
* 容错:写入失败仅 warn,不影响内存配置。
|
||||||
|
*/
|
||||||
|
function saveToStorage(config: MiniappConfig): void {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(STORAGE_KEY, JSON.stringify(config))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[df-miniapp] save config to storage failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getConfig(): MiniappConfig {
|
export function getConfig(): MiniappConfig {
|
||||||
|
// 首次调用时从 storage 加载并缓存
|
||||||
|
if (_config === null) {
|
||||||
|
_config = loadFromStorage()
|
||||||
|
}
|
||||||
return _config
|
return _config
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setConfig(patch: Partial<MiniappConfig>): void {
|
export function setConfig(patch: Partial<MiniappConfig>): void {
|
||||||
_config = { ..._config, ...patch }
|
// 确保已加载(避免在 getConfig 前调用 setConfig 丢失 storage 旧值)
|
||||||
|
if (_config === null) {
|
||||||
|
_config = loadFromStorage()
|
||||||
|
}
|
||||||
|
_config = { ..._config, ...patch }
|
||||||
|
saveToStorage(_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置为默认配置并清除 storage 持久化数据
|
||||||
|
*
|
||||||
|
* 用于用户重新配对或恢复出厂占位值的场景。
|
||||||
|
*/
|
||||||
|
export function resetConfig(): void {
|
||||||
|
_config = { ...defaultConfig }
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync(STORAGE_KEY)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[df-miniapp] remove config from storage failed:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,7 +346,35 @@ async fn handle_inbound_text(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// 入站文本即业务 payload(relay 不解析),包成 BroadcastMessage
|
// 入站文本即业务 payload(relay 不解析),包成 BroadcastMessage
|
||||||
// payload 直接用原始 JSON 值;若客户端发非 JSON 文本,则包成字符串值
|
// payload 直接用原始 JSON 值;若客户端发非 JSON 文本,则包成字符串值
|
||||||
let payload: serde_json::Value = serde_json::from_str(raw).unwrap_or(serde_json::Value::String(raw.to_string()));
|
let payload: serde_json::Value =
|
||||||
|
serde_json::from_str(raw).unwrap_or(serde_json::Value::String(raw.to_string()));
|
||||||
|
|
||||||
|
// 心跳协议:miniapp 发 {control_kind: "ping"} → relay 直接回 pong(不经 device 透传)。
|
||||||
|
// miniapp 用 pong 更新 lastPongTime 看门狗(防移动网络 TCP 半连接挂死)。
|
||||||
|
// device 端收到 control 消息也仅 console.log,不影响业务。
|
||||||
|
if let Some(control_kind) = payload
|
||||||
|
.get("control_kind")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
{
|
||||||
|
if control_kind == "ping" && kind == ClientKind::Miniapp {
|
||||||
|
let pong_payload = serde_json::json!({"control_kind": "pong"});
|
||||||
|
let pong_msg = BroadcastMessage {
|
||||||
|
device_id: device_id.to_string(),
|
||||||
|
kind: crate::broadcast::MessageKind::Control,
|
||||||
|
source: conn_id,
|
||||||
|
from: ClientKind::Device, // pong 来自 relay(代理 device),让 miniapp 识别为合法响应
|
||||||
|
payload: pong_payload,
|
||||||
|
ts: now_ms(),
|
||||||
|
};
|
||||||
|
let _ = state.route(&pong_msg).await;
|
||||||
|
tracing::trace!(
|
||||||
|
conn_id = conn_id.0,
|
||||||
|
device_id = %device_id,
|
||||||
|
"miniapp ping → relay pong(本地响应)"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let now = now_ms();
|
let now = now_ms();
|
||||||
let (msg_kind, from) = match kind {
|
let (msg_kind, from) = match kind {
|
||||||
|
|||||||
12
docs/todo.md
12
docs/todo.md
@@ -256,11 +256,11 @@ graph TD
|
|||||||
|
|
||||||
> 继批1-3c 升级后的下一步:灵感来源录入增强 + 自动采集机制。**属新功能范畴**,需独立设计采集规则/触发/去重,下一阶段推进。
|
> 继批1-3c 升级后的下一步:灵感来源录入增强 + 自动采集机制。**属新功能范畴**,需独立设计采集规则/触发/去重,下一阶段推进。
|
||||||
|
|
||||||
- [ ] **F-260621-01 [P2/新功能]** — **source 字段录入增强 + 自动采集机制**。
|
- [x] **F-260621-01 [P2/新功能]** ✅ **source 录入增强已落地**(灵感捕捉模态框已含 source/tags/priority 输入字段,逗号分隔 tags 转 JSON):
|
||||||
1. **source 录入增强**:灵感捕捉模态框加 `source` / `tags` / `priority` 输入(现仅描述),让录入即结构化(来源/标签/优先级)
|
1. ✅ **source 录入增强**:灵感捕捉模态框已加 `source` / `tags` / `priority` 输入,录入即结构化
|
||||||
2. **自动采集机制**:从对话 / 任务 / 代码中自动提炼灵感(降低录入门槛,捕捉随手遗失的灵感)
|
2. ⏸️ **自动采集机制**:从对话/任务/代码中自动提炼灵感 —— 需独立设计采集规则/触发/去重,属下一阶段新功能
|
||||||
- **需设计**:采集规则(什么信号算灵感 — 关键词/意图/重复模式)/ 触发时机(对话结束/任务完成/手动触发)/ 去重(相似灵感合并避免洪水)/ 用户确认流(自动采集不污染灵感池,需用户确认入库)
|
- **需设计**:采集规则(什么信号算灵感 — 关键词/意图/重复模式)/ 触发时机(对话结束/任务完成/手动触发)/ 去重(相似灵感合并避免洪水)/ 用户确认流(自动采集不污染灵感池,需用户确认入库)
|
||||||
- **依赖**:批1-3c 升级完成(已有 source/tags 字段基础) + 对话历史可读(F-15 archived_segment 状态) + 任务可读
|
- **依赖**:批1-3c 升级完成(已有 source/tags 字段基础 ✅) + 对话历史可读(F-15 archived_segment 状态 ✅) + 任务可读 ✅
|
||||||
- **关联**:memory [[devflow-idea-module]] / [[aichat-skill-slash-autocomplete]](联想需求,场景相关)
|
- **关联**:memory [[devflow-idea-module]] / [[aichat-skill-slash-autocomplete]](联想需求,场景相关)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -286,8 +286,8 @@ graph TD
|
|||||||
|
|
||||||
**P1(阻塞真机/可靠性·5 项)**:
|
**P1(阻塞真机/可靠性·5 项)**:
|
||||||
|
|
||||||
- [ ] P1-D-260622 [P1🔴·真机分发最高阻断] — **token/deviceId 硬编码 + 无配对绑定 + 无 storage 持久化**。`config.ts:35-39` 硬编码本机 device_id+token(注释自认「联调期硬编码,后续做配对绑定」),`:52-54` setConfig 仅内存赋值无 `uni.setStorageSync`。真机分发不能用本机硬编码 device_id。修:settings 页输入 + setConfig 持久化 storage + getConfig 读回退 default。— `apps/df-miniapp/src/config.ts`
|
- [x] P1-D-260622 [P1] ✅ **config 持久化已实现**:`setConfig` 接入 `uni.setStorageSync`,懒加载从 storage 读回退 defaultConfig,新增 `resetConfig` 清除。— `apps/df-miniapp/src/config.ts`
|
||||||
- [ ] P1-A-260622 [P1🔴·可靠性] — **心跳 pong 闭环缺失**。`ws.ts:226` 发 `{control_kind:'ping'}`,`relay.rs:355` miniapp 入站全标 `MessageKind::Command` 透传 device,但 ping 无 cmd 字段 → `remote_bridge.rs:143` match 无 ping 分支 → device 收不到有效命令无 pong 回;`useAiChat.ts:440` onControl 仅 console.log 无 pong 处理;`ws.ts:245-257` 重连仅靠 onClose/onError 无心跳超时。移动网络 TCP 半连接高发,device 静默挂死无感知。修:relay 解析 ping 构造 Control 类型 + device 响应 pong + miniapp 维护 lastPongTime 看门狗(3×interval 未收 pong 主动重连)。— `ws.ts` + `relay.rs:handle_inbound_text` + `useAiChat.ts`
|
- [x] P1-A-260622 [P1] ✅ **心跳 pong 闭环已实现**:relay 识别 `{control_kind:"ping"}` 构造 pong 回 miniapp(不经 device 透传),miniapp `lastInboundAt` 更新看门狗治半连接挂死。— `crates/df-relay/src/relay.rs` + `apps/df-miniapp/src/api/ws.ts` + `apps/df-miniapp/src/composables/useAiChat.ts`
|
||||||
|
|
||||||
**用户补充核验(2026-06-22 用户详读源码+设计文档,主代逐项核源码判真伪)**:
|
**用户补充核验(2026-06-22 用户详读源码+设计文档,主代逐项核源码判真伪)**:
|
||||||
|
|
||||||
|
|||||||
38
docs/待决策.md
38
docs/待决策.md
@@ -42,16 +42,14 @@
|
|||||||
|
|
||||||
### 🟡 待设计/架构演进(有决策点,需方案设计或长期规划)
|
### 🟡 待设计/架构演进(有决策点,需方案设计或长期规划)
|
||||||
|
|
||||||
#### T-260614-11 条件表达式引擎升级
|
#### T-260614-11 条件表达式引擎升级(✅ 已完成)
|
||||||
- **背景**:df-workflow conditions 仅支持 true/false 字面量,升级为真表达式求值。引擎设计好但无前端 UI 消费方(demoDag edges 无 condition 字段)。
|
- **背景**:df-workflow conditions 仅支持 true/false 字面量,升级为真表达式求值。
|
||||||
- **决策点**:求值器实现方式?(已暂缓:等工作流真实场景)
|
- **解决**:手写求值器(option a)已实现并集成。
|
||||||
- **选项**:
|
- `ConditionEngine::evaluate()` 支持完整表达式:JSON Path(`$.status == 'completed'`)/ 嵌套属性 / 数组索引 / 数值比较 / 布尔字段
|
||||||
- a: 手写求值器
|
- executor 集成(`cfg!(feature = "conditions-eval")` 默认开)
|
||||||
- b: 复活 set_skipped
|
- 前端 UI 已有边条件编辑入口(`WorkflowDagDisplay.vue`)
|
||||||
- c: default false + warn(均认可)
|
- 中英文翻译已补齐
|
||||||
- **推荐**:**⏸️ 暂缓**(无消费方,等 BuildNode 落地跑构建→测试→部署真实场景)
|
- **状态**:✅ 已完成
|
||||||
- **关联**:todo T-260614-11
|
|
||||||
- **状态**:⏸️ 暂缓
|
|
||||||
|
|
||||||
#### ARC-260615-07 架构重构批(排期/优先级决策)
|
#### ARC-260615-07 架构重构批(排期/优先级决策)
|
||||||
- **背景**:src-tauri IPC 编排层 5711 行成事实业务层,7 项架构债。df-core→df-types 已完成(CR-61),剩 6 项独立大改。
|
- **背景**:src-tauri IPC 编排层 5711 行成事实业务层,7 项架构债。df-core→df-types 已完成(CR-61),剩 6 项独立大改。
|
||||||
@@ -203,22 +201,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## F-09 streaming/currentText 改 per-conv(消息重叠/多会话串扰根治)
|
## F-09 streaming/currentText 改 per-conv(✅ 已完成)
|
||||||
|
|
||||||
**背景**:BUG-260624-01 消息重叠核心修复后,论证 workflow 边界角度指出残留根因——`streaming`/`currentText` 是 store 全局单例(非 per-conv),F-09 多会话并发下 A 后台生成(streaming=true)+ 切 B 会话,B 末条 AI 气泡命中 `isLastAi&&streaming&¤tText` 会渲 A 残留 currentText。当前修复(flushCurrentText 自清 + activeConversationId watch 清 streamingBlocks)已大幅收窄残留窗口,但单例根因未除。
|
**背景**:`streaming`/`currentText` 原为 store 全局单例,F-09 多会话并发下 A 后台生成 + 切 B 会话会串扰。
|
||||||
|
|
||||||
**决策点**:是否立项 per-conv 化(streaming/currentText/streamingBlocks 改 Map<convId,...>)?
|
**解决(2026-06-28 核验)**:✅ option a 已落地——
|
||||||
- **a 立项根治**:store 改 per-conv Map,所有读写点改。大改(涉 ai.ts store + useAiEvents/useAiStream/useAiConversations/MessageList 多处),对齐 [[aichat-arch-extensibility]] AiSession 单例未动 + [[fe-arch-tech-debt]] status 无 union。
|
- `stores/ai.ts` 用 `Object.defineProperty` 定义 streaming/currentText accessor
|
||||||
- **b 暂缓(当前缓解够用)**:自清 + activeConv watch 已覆盖主路径(新轮/切会话/结束清),残留仅极端并发场景(A 后台 streaming + 切 B)。等 B 路线多会话并发([[aichat-b-route-parallel-multiround]])正式推进时一并 per-conv。
|
- 委派 `aiShared.convStreamStates` per-conv Map,按 `activeConversationId` 索引读写
|
||||||
|
- 单会话回归零变化(accessor 透明继承),多会话切会话不串扰
|
||||||
|
|
||||||
**多角度论证(自主决策依据)**:
|
**状态**:✅ 已完成
|
||||||
- 根治性:a 一次性消除单例串扰族 bug;b 缓解主路径,极端并发残留。
|
|
||||||
- 改动面:a 大(store + 5 composable + MessageList),回归风险高,需全链路测;b 零额外改动。
|
|
||||||
- 时机:b 与 B 路线多会话并发同批做更合理(那时 per-conv 是前置),现在单独做与 B 路线重复拆改。
|
|
||||||
- 当前用户场景:自用单会话为主,多会话并发(A 后台 + 切 B)非高频。
|
|
||||||
|
|
||||||
**倾向**:**⏸️ b 暂缓**(当前修复缓解有效 + per-conv 是 B 路线大改 + 与多会话并发同批更合理)。
|
|
||||||
**状态**:🟡 待拍板(若用户多会话并发场景频繁 → a 立项;当前单会话为主 → b 暂缓随 B 路线)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user