mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(import): avoid deduplicating WeFlow local IDs
This commit is contained in:
1 parent
a50827f42a
commit
af68f13d09
5 files changed
+109
-32
No files matched your search
@@ -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]
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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(/[\\/]/)
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user