fix(import): avoid deduplicating WeFlow local IDs

This commit is contained in:
digua committed 2026-08-27 17:19:06 +08:00
1 parent a50827f42a
commit af68f13d09
5 files changed
+109 -32

No files matched your search

+43 -26
View File
@@ -49,17 +49,17 @@ fn convert_message_type(type_str: Option<&str>) -> u32 {
}
}
/// JS `String(x)` semantics for the localId field.
fn js_string(value: Option<&Value>) -> String {
match value {
None => "undefined".to_string(),
Some(Value::Null) => "null".to_string(),
Some(Value::String(s)) => s.clone(),
Some(Value::Bool(b)) => b.to_string(),
Some(Value::Number(n)) => n.to_string(),
// Arrays/objects are pathological for an ID field; compact JSON is a
// reasonable stand-in for JS coercion here.
Some(other) => serde_json::to_string(other).unwrap_or_default(),
fn normalize_platform_message_id(value: Option<&Value>) -> Option<String> {
let raw = match value {
Some(Value::String(value)) => value.clone(),
Some(Value::Number(value)) => value.to_string(),
_ => return None,
};
let id = js_trim(&raw);
if id.is_empty() || id == "0" {
None
} else {
Some(id.to_string())
}
}
@@ -272,8 +272,7 @@ pub fn parse_weflow(
};
messages.push(NativeMessage {
// WeFlow always emits an id string (String(localId), JS semantics).
platform_message_id: Some(js_string(obj.get("localId"))),
platform_message_id: normalize_platform_message_id(obj.get("platformMessageId")),
sender_platform_id: sender.to_string(),
sender_account_name: account_name.to_string(),
sender_group_nickname: None,
@@ -330,7 +329,7 @@ mod tests {
"session": {"wxid": "room@chatroom", "nickname": "群名", "displayName": "群显示名", "type": "群聊", "avatar": ""},
"avatars": {"wxid_a": "data:image/jpeg;base64,AAA", "room@chatroom": "data:image/jpeg;base64,ROOM", "wxid_empty": ""},
"messages": [
{"localId": 1, "createTime": 100, "type": "文本消息", "content": " hello ", "isSend": 0, "senderUsername": "wxid_a", "senderDisplayName": "Alice", "senderAvatarKey": "wxid_a"},
{"localId": 1, "platformMessageId": "server-1", "createTime": 100, "type": "文本消息", "content": " hello ", "isSend": 0, "senderUsername": "wxid_a", "senderDisplayName": "Alice", "senderAvatarKey": "wxid_a"},
{"localId": 2, "createTime": 101, "type": "图片消息", "content": "[图片]", "isSend": 1, "senderUsername": "wxid_b", "senderDisplayName": "Bob"},
{"localId": 3, "createTime": 102, "type": "系统消息", "content": "sys", "isSend": null, "senderUsername": "room@chatroom", "senderDisplayName": "群"},
{"localId": 4, "createTime": 103, "type": "未知类型(88)", "content": null, "isSend": 0, "senderUsername": "wxid_a", "senderDisplayName": "Alice2"}
@@ -349,7 +348,11 @@ mod tests {
assert_eq!(out.messages.len(), 3);
assert_eq!(out.messages[0].content.as_deref(), Some("hello"));
assert_eq!(out.messages[0].message_type, 0);
assert_eq!(out.messages[0].platform_message_id.as_deref(), Some("1"));
assert_eq!(
out.messages[0].platform_message_id.as_deref(),
Some("server-1")
);
assert_eq!(out.messages[1].platform_message_id, None);
assert_eq!(out.messages[1].message_type, 1);
assert_eq!(out.messages[2].message_type, 99);
assert_eq!(out.messages[2].content, None);
@@ -386,11 +389,8 @@ mod tests {
assert_eq!(out.messages[1].sender_account_name, "wxid_a");
// Whitespace-only content is normalized to null.
assert_eq!(out.messages[1].content, None);
// localId missing -> String(undefined).
assert_eq!(
out.messages[0].platform_message_id.as_deref(),
Some("undefined")
);
// Missing stable platform IDs remain absent so import can use content fingerprinting.
assert_eq!(out.messages[0].platform_message_id, None);
}
#[test]
@@ -466,12 +466,29 @@ mod tests {
}
#[test]
fn js_string_semantics_for_local_id() {
fn normalizes_stable_platform_message_id() {
let n: Value = serde_json::from_str("3.5").unwrap();
assert_eq!(js_string(Some(&n)), "3.5");
assert_eq!(js_string(Some(&Value::Null)), "null");
assert_eq!(js_string(None), "undefined");
assert_eq!(js_string(Some(&Value::String("abc".into()))), "abc");
assert_eq!(js_string(Some(&Value::Bool(true))), "true");
assert_eq!(
normalize_platform_message_id(Some(&n)).as_deref(),
Some("3.5")
);
assert_eq!(
normalize_platform_message_id(Some(&Value::String(" server-1 ".into()))).as_deref(),
Some("server-1")
);
assert_eq!(
normalize_platform_message_id(Some(&Value::String("0".into()))),
None
);
assert_eq!(
normalize_platform_message_id(Some(&Value::Number(0.into()))),
None
);
assert_eq!(normalize_platform_message_id(Some(&Value::Null)), None);
assert_eq!(normalize_platform_message_id(None), None);
assert_eq!(
normalize_platform_message_id(Some(&Value::Bool(true))),
None
);
}
}
@@ -42,4 +42,46 @@ describe('WeFlow browser-safe JSON parser', () => {
)
assert.equal(progress.at(-1)?.progress, 1)
})
it('does not treat export-local IDs as stable message IDs', async () => {
const content = JSON.stringify({
weflow: { version: '1.0.0' },
session: { wxid: 'group@chatroom', displayName: 'Project Team', type: '群聊' },
messages: [
{
localId: 1,
createTime: 1704164645,
type: '文本消息',
content: 'old message',
senderUsername: 'member-1',
senderDisplayName: 'Member 1',
},
{
localId: 1,
platformMessageId: 'server-2',
createTime: 1767225600,
type: '文本消息',
content: 'new message',
senderUsername: 'member-2',
senderDisplayName: 'Member 2',
},
{
localId: 2,
platformMessageId: '0',
createTime: 1767225601,
type: '文本消息',
content: 'message without a stable ID',
senderUsername: 'member-2',
senderDisplayName: 'Member 2',
},
],
})
const result = await parseWeFlowJson(content, 'project.json')
assert.deepEqual(
result.messages.map((message) => message.platformMessageId),
[undefined, 'server-2', undefined]
)
})
})
+8 -1
View File
@@ -89,8 +89,9 @@ export async function parseWeFlowJson(
members.set(sender, { platformId: sender, accountName, avatar })
}
const platformMessageId = normalizePlatformMessageId(message.platformMessageId)
messages.push({
platformMessageId: String(message.localId),
platformMessageId,
senderPlatformId: sender,
senderAccountName: accountName,
senderGroupNickname: undefined,
@@ -170,6 +171,12 @@ function normalizeContent(value: unknown): string | null {
return content || null
}
function normalizePlatformMessageId(value: unknown): string | undefined {
if (typeof value !== 'string' && typeof value !== 'number') return undefined
const id = String(value).trim()
return id && id !== '0' ? id : undefined
}
function nameFromFilename(fileName: string): string {
const name = fileName
.split(/[\\/]/)
+10 -1
View File
@@ -83,6 +83,7 @@ interface WeFlowSession {
interface WeFlowMessage {
localId: number
platformMessageId?: string | number
createTime: number // Unix 时间戳(秒)
formattedTime: string
type: string // 中文消息类型
@@ -95,6 +96,12 @@ interface WeFlowMessage {
source: string
}
function normalizePlatformMessageId(value: unknown): string | undefined {
if (typeof value !== 'string' && typeof value !== 'number') return undefined
const id = String(value).trim()
return id && id !== '0' ? id : undefined
}
// ==================== 消息类型映射 ====================
/**
@@ -527,8 +534,10 @@ async function* parseWeFlow(options: ParseOptions): AsyncGenerator<ParseEvent, v
content = rawContent.trim() || null
}
const platformMessageId = normalizePlatformMessageId(msg.platformMessageId)
return {
platformMessageId: String(msg.localId), // 消息的平台原始 ID(用于回复关联查询)
platformMessageId,
senderPlatformId: platformId,
senderAccountName: accountName,
// WeFlow 格式没有单独的群昵称字段
@@ -82,6 +82,7 @@ const FULL_FIXTURE = {
messages: [
{
localId: 1,
platformMessageId: 'server-1',
createTime: 1704164645,
formattedTime: '',
type: '文本消息',
@@ -151,7 +152,7 @@ const FULL_FIXTURE = {
senderDisplayName: '',
senderAvatarKey: 'wxid_keyed',
},
// Numeric localId with fraction, emoji content, unicode whitespace trim
// Export-local ID must not be used when no stable platform ID is present.
{
localId: 10.5,
createTime: 1704164750,
@@ -161,7 +162,7 @@ const FULL_FIXTURE = {
senderUsername: 'wxid_owner',
senderDisplayName: 'Me2',
},
// Missing localId -> String(undefined); missing content -> null
// Missing localId and platformMessageId -> no stable ID; missing content -> null
{
createTime: 1704164760,
type: '红包卡片',
@@ -192,8 +193,9 @@ describe('weflow native parser parity', { skip: !nativeAvailable() && 'native mo
assert.equal(nativeResult.messages[2].timestamp, null)
assert.equal(nativeResult.messages[3].content, '{"title":"引用","items":[1,true,null],"nested":{"a":"b"}}')
assert.equal(nativeResult.messages[4].content, null)
assert.equal(nativeResult.messages[5].platformMessageId, '10.5')
assert.equal(nativeResult.messages[6].platformMessageId, 'undefined')
assert.equal(nativeResult.messages[0].platformMessageId, 'server-1')
assert.equal(nativeResult.messages[5].platformMessageId, undefined)
assert.equal(nativeResult.messages[6].platformMessageId, undefined)
const bob = nativeResult.members.find((m) => m.platformId === 'wxid_bob')
assert.equal(bob?.avatar, 'data:image/jpeg;base64,KEYED')
assert.equal(bob?.accountName, 'wxid_bob')