fix(parser): align ChatLab JSONL validation

Keep automatic format detection consistent with accepted JSONL syntax and reject unresolved owner metadata. Regenerate the standalone skill validators from the canonical implementation.
This commit is contained in:
digua committed 2026-07-19 15:16:29 +08:00
1 parent b0380a6689
commit 1d183cbe6a
7 files changed
+89 -9

No files matched your search

@@ -72,6 +72,34 @@ test('validates a complete ChatLab JSONL file without exposing message content',
}
})
test('rejects an ownerId outside inferred JSONL members', async () => {
const root = makeTempDir()
const filePath = path.join(root, 'unknown-owner.jsonl')
writeJsonl(filePath, [
{
_type: 'header',
chatlab: { version: CHATLAB_FORMAT_VERSION, exportedAt: 1_711_468_800 },
meta: { name: 'Example', platform: 'custom', type: 'group', ownerId: 'missing' },
},
{
_type: 'message',
sender: 'alice',
accountName: 'Alice',
timestamp: 1_711_468_800,
type: 0,
content: 'Hello',
},
])
try {
const report = await validateChatLabFile(filePath)
assert.equal(report.valid, false)
assert.ok(report.issues.some((issue) => issue.code === 'UNKNOWN_OWNER'))
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
test('accepts the initial ChatLab JSON format version', async () => {
const root = makeTempDir()
const filePath = path.join(root, 'legacy.json')
+8 -3
View File
@@ -357,12 +357,17 @@ function finalizeState(
}
}
if (state.memberIds.size > 0) {
const ownerMemberIds =
state.memberIds.size > 0 ? state.memberIds : requireExplicitMembers ? undefined : state.senderIds
if (ownerMemberIds) {
for (const ownerId of state.ownerIds) {
if (!state.memberIds.has(ownerId)) {
addIssue(state, 'error', 'UNKNOWN_OWNER', 'meta.ownerId must reference an explicit member platformId.')
if (!ownerMemberIds.has(ownerId)) {
addIssue(state, 'error', 'UNKNOWN_OWNER', 'meta.ownerId must reference a member platformId.')
}
}
}
if (state.memberIds.size > 0) {
for (const senderId of state.senderIds) {
if (!state.memberIds.has(senderId)) {
addIssue(
@@ -5,7 +5,7 @@ import path from 'node:path'
import test from 'node:test'
import { MessageType } from '@openchatlab/shared-types'
import { findEntryFileInDirectory, parseFileWithFormat } from '../index'
import { detectFormat, findEntryFileInDirectory, parseFileWithFormat } from '../index'
function makeTempDir(): string {
const baseDir = process.env.CHATLAB_TEST_TMPDIR ?? (fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir())
@@ -96,3 +96,46 @@ test('directory entry detection accepts a top-level ChatLab JSONL file', () => {
fs.rmSync(root, { recursive: true, force: true })
}
})
test('auto-detects ChatLab JSONL after leading comments and blank lines', () => {
const root = makeTempDir()
const filePath = path.join(root, 'commented-chat.jsonl')
writeChatLabJsonl(filePath, 1)
fs.writeFileSync(filePath, `# generated locally\n\n${fs.readFileSync(filePath, 'utf8')}`, 'utf8')
try {
assert.equal(detectFormat(filePath)?.id, 'chatlab-jsonl')
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
test('auto-detects ChatLab JSONL regardless of header field order', () => {
const root = makeTempDir()
const filePath = path.join(root, 'reordered-header.jsonl')
writeChatLabJsonl(filePath, 1)
const [headerLine, ...remainingLines] = fs.readFileSync(filePath, 'utf8').trimEnd().split('\n')
const header = JSON.parse(headerLine) as Record<string, unknown>
const reorderedHeader = { chatlab: header.chatlab, meta: header.meta, _type: header._type }
fs.writeFileSync(filePath, `${JSON.stringify(reorderedHeader)}\n${remainingLines.join('\n')}\n`, 'utf8')
try {
assert.equal(detectFormat(filePath)?.id, 'chatlab-jsonl')
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
test('auto-detects ChatLab JSONL with a UTF-8 BOM', () => {
const root = makeTempDir()
const filePath = path.join(root, 'bom-chat.jsonl')
writeChatLabJsonl(filePath, 1)
fs.writeFileSync(filePath, `\uFEFF${fs.readFileSync(filePath, 'utf8')}`, 'utf8')
try {
assert.equal(detectFormat(filePath)?.id, 'chatlab-jsonl')
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
+4 -2
View File
@@ -117,8 +117,10 @@ export const feature: FormatFeature = {
priority: 51, // 低优先级,让其他格式先匹配
extensions: ['.jsonl'],
signatures: {
// 第一行必须是 header 类型,包含 chatlab 信息
head: [/^\s*\{.*"_type"\s*:\s*"header".*"chatlab"/],
// 跳过格式允许的空行和注释后,分别匹配首条数据中的 header 类型和 chatlab 对象。
head: [
/^\uFEFF?(?:[ \t]*(?:#[^\r\n]*)?\r?\n)*[ \t]*\{(?=[^\r\n]*"_type"\s*:\s*"header")(?=[^\r\n]*"chatlab"\s*:)[^\r\n]*\}/,
],
},
}
+1 -1
View File
@@ -100,7 +100,7 @@ try {
'--target=node20',
'--minify',
`--outfile=${outputPath}`,
'--banner:js=#!/usr/bin/env node\n// Generated from the canonical ChatLab validator. Do not edit directly.',
'--banner:js=#!/usr/bin/env node\n// Generated from the canonical ChatLab validator. Do not edit directly.\n/* eslint-disable */',
],
{ cwd: rootDir, stdio: 'inherit' }
)
@@ -1,4 +1,5 @@
#!/usr/bin/env node
// Generated from the canonical ChatLab validator. Do not edit directly.
import{existsSync as U}from"node:fs";import z from"node:path";import{createReadStream as O}from"node:fs";import{readFile as P,stat as k}from"node:fs/promises";import{extname as V}from"node:path";import{createInterface as D}from"node:readline";var E="0.0.2",S=["0.0.1",E];function C(e){return S.includes(e)}var h=(s=>(s[s.TEXT=0]="TEXT",s[s.IMAGE=1]="IMAGE",s[s.VOICE=2]="VOICE",s[s.VIDEO=3]="VIDEO",s[s.FILE=4]="FILE",s[s.EMOJI=5]="EMOJI",s[s.LINK=7]="LINK",s[s.LOCATION=8]="LOCATION",s[s.RED_PACKET=20]="RED_PACKET",s[s.TRANSFER=21]="TRANSFER",s[s.POKE=22]="POKE",s[s.CALL=23]="CALL",s[s.SHARE=24]="SHARE",s[s.REPLY=25]="REPLY",s[s.FORWARD=26]="FORWARD",s[s.CONTACT=27]="CONTACT",s[s.SYSTEM=80]="SYSTEM",s[s.RECALL=81]="RECALL",s[s.OTHER=99]="OTHER",s))(h||{});var v=100,M=1e11,G=new Set(Object.values(h).filter(e=>typeof e=="number"));function A(e,r){return{format:e,fileSize:r,errorCount:0,warningCount:0,issues:[],stats:{headerCount:0,memberCount:0,messageCount:0,uniqueSenderCount:0,commentLineCount:0,blankLineCount:0},memberIds:new Set,ownerIds:new Set,senderIds:new Set,messageIds:new Set,replyTargetIds:new Set}}function a(e,r,n,t,o={}){r==="error"?e.errorCount++:e.warningCount++,e.issues.length<v&&e.issues.push({severity:r,code:n,message:t,...o})}function d(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function c(e,r){return Object.prototype.hasOwnProperty.call(e,r)}function g(e,r){return{...e,path:r}}function u(e,r,n,t){return typeof r=="string"&&r.trim().length>0?!0:(a(e,"error","INVALID_REQUIRED_STRING","Expected a non-empty string.",g(t,n)),!1)}function m(e,r,n,t,o){c(r,n)&&typeof r[n]!="string"&&a(e,"error","INVALID_OPTIONAL_STRING","Expected a string.",g(o,t))}function y(e,r,n,t){return typeof r=="number"&&Number.isInteger(r)&&r>=0&&r<M?!0:(a(e,"error","INVALID_UNIX_SECONDS","Expected a non-negative integer Unix timestamp in seconds, not milliseconds.",g(t,n)),!1)}function x(e,r,n,t,o=""){if(e.stats.headerCount++,!d(r))a(e,"error","INVALID_CHATLAB_HEADER","Expected a chatlab object.",{...t,path:`${o}chatlab`});else{let i=`${o}chatlab.version`;typeof r.version!="string"?a(e,"error","INVALID_VERSION","Expected a string format version.",{...t,path:i}):(e.version=r.version,C(r.version)||a(e,"error","UNSUPPORTED_VERSION",`Supported ChatLab format versions: ${S.join(", ")}.`,{...t,path:i})),y(e,r.exportedAt,`${o}chatlab.exportedAt`,t),m(e,r,"generator",`${o}chatlab.generator`,t),m(e,r,"description",`${o}chatlab.description`,t)}if(!d(n)){a(e,"error","INVALID_META","Expected a meta object.",{...t,path:`${o}meta`});return}u(e,n.name,`${o}meta.name`,t),u(e,n.platform,`${o}meta.platform`,t),n.type!=="group"&&n.type!=="private"&&a(e,"error","INVALID_CHAT_TYPE",'Expected chat type "group" or "private".',{...t,path:`${o}meta.type`}),m(e,n,"groupId",`${o}meta.groupId`,t),m(e,n,"groupAvatar",`${o}meta.groupAvatar`,t),c(n,"ownerId")&&u(e,n.ownerId,`${o}meta.ownerId`,t)&&e.ownerIds.add(n.ownerId),m(e,n,"sourceSessionId",`${o}meta.sourceSessionId`,t)}function N(e,r,n,t){if(e.stats.memberCount++,!d(r)){a(e,"error","INVALID_MEMBER","Expected a member object.",g(t,n));return}u(e,r.platformId,`${n}.platformId`,t)&&(e.memberIds.has(r.platformId)?a(e,"error","DUPLICATE_MEMBER_ID","Member platformId must be unique.",{...t,path:`${n}.platformId`}):e.memberIds.add(r.platformId)),u(e,r.accountName,`${n}.accountName`,t),m(e,r,"groupNickname",`${n}.groupNickname`,t),m(e,r,"avatar",`${n}.avatar`,t),c(r,"aliases")&&(!Array.isArray(r.aliases)||r.aliases.some(o=>typeof o!="string"))&&a(e,"error","INVALID_ALIASES","Expected aliases to be an array of strings.",{...t,path:`${n}.aliases`}),c(r,"roles")&&(Array.isArray(r.roles)?r.roles.forEach((o,i)=>{let p=`${n}.roles[${i}]`;if(!d(o)){a(e,"error","INVALID_ROLE","Expected a role object.",{...t,path:p});return}u(e,o.id,`${p}.id`,t),m(e,o,"name",`${p}.name`,t)}):a(e,"error","INVALID_ROLES","Expected roles to be an array.",{...t,path:`${n}.roles`}))}function _(e,r,n,t){if(e.stats.messageCount++,!d(r)){a(e,"error","INVALID_MESSAGE","Expected a message object.",g(t,n));return}u(e,r.sender,`${n}.sender`,t)&&e.senderIds.add(r.sender),u(e,r.accountName,`${n}.accountName`,t),y(e,r.timestamp,`${n}.timestamp`,t)&&(e.lastTimestamp!==void 0&&r.timestamp<e.lastTimestamp&&a(e,"warning","OUT_OF_ORDER_TIMESTAMP","Messages should be ordered by timestamp ascending.",g(t,`${n}.timestamp`)),e.lastTimestamp=r.timestamp),(typeof r.type!="number"||!Number.isInteger(r.type)||!G.has(r.type))&&a(e,"error","INVALID_MESSAGE_TYPE","Expected a supported numeric ChatLab message type.",{...t,path:`${n}.type`}),(!c(r,"content")||typeof r.content!="string"&&r.content!==null)&&a(e,"error","INVALID_MESSAGE_CONTENT","Expected content to be a string or null.",{...t,path:`${n}.content`}),m(e,r,"groupNickname",`${n}.groupNickname`,t),c(r,"platformMessageId")&&u(e,r.platformMessageId,`${n}.platformMessageId`,t)&&(e.messageIds.has(r.platformMessageId)?a(e,"error","DUPLICATE_MESSAGE_ID","platformMessageId must be unique.",{...t,Line truncated
/* eslint-disable */
import{existsSync as U}from"node:fs";import z from"node:path";import{createReadStream as O}from"node:fs";import{readFile as P,stat as k}from"node:fs/promises";import{extname as V}from"node:path";import{createInterface as D}from"node:readline";var E="0.0.2",S=["0.0.1",E];function C(e){return S.includes(e)}var h=(s=>(s[s.TEXT=0]="TEXT",s[s.IMAGE=1]="IMAGE",s[s.VOICE=2]="VOICE",s[s.VIDEO=3]="VIDEO",s[s.FILE=4]="FILE",s[s.EMOJI=5]="EMOJI",s[s.LINK=7]="LINK",s[s.LOCATION=8]="LOCATION",s[s.RED_PACKET=20]="RED_PACKET",s[s.TRANSFER=21]="TRANSFER",s[s.POKE=22]="POKE",s[s.CALL=23]="CALL",s[s.SHARE=24]="SHARE",s[s.REPLY=25]="REPLY",s[s.FORWARD=26]="FORWARD",s[s.CONTACT=27]="CONTACT",s[s.SYSTEM=80]="SYSTEM",s[s.RECALL=81]="RECALL",s[s.OTHER=99]="OTHER",s))(h||{});var v=100,M=1e11,G=new Set(Object.values(h).filter(e=>typeof e=="number"));function A(e,r){return{format:e,fileSize:r,errorCount:0,warningCount:0,issues:[],stats:{headerCount:0,memberCount:0,messageCount:0,uniqueSenderCount:0,commentLineCount:0,blankLineCount:0},memberIds:new Set,ownerIds:new Set,senderIds:new Set,messageIds:new Set,replyTargetIds:new Set}}function o(e,r,n,t,a={}){r==="error"?e.errorCount++:e.warningCount++,e.issues.length<v&&e.issues.push({severity:r,code:n,message:t,...a})}function c(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function d(e,r){return Object.prototype.hasOwnProperty.call(e,r)}function g(e,r){return{...e,path:r}}function u(e,r,n,t){return typeof r=="string"&&r.trim().length>0?!0:(o(e,"error","INVALID_REQUIRED_STRING","Expected a non-empty string.",g(t,n)),!1)}function m(e,r,n,t,a){d(r,n)&&typeof r[n]!="string"&&o(e,"error","INVALID_OPTIONAL_STRING","Expected a string.",g(a,t))}function y(e,r,n,t){return typeof r=="number"&&Number.isInteger(r)&&r>=0&&r<M?!0:(o(e,"error","INVALID_UNIX_SECONDS","Expected a non-negative integer Unix timestamp in seconds, not milliseconds.",g(t,n)),!1)}function x(e,r,n,t,a=""){if(e.stats.headerCount++,!c(r))o(e,"error","INVALID_CHATLAB_HEADER","Expected a chatlab object.",{...t,path:`${a}chatlab`});else{let i=`${a}chatlab.version`;typeof r.version!="string"?o(e,"error","INVALID_VERSION","Expected a string format version.",{...t,path:i}):(e.version=r.version,C(r.version)||o(e,"error","UNSUPPORTED_VERSION",`Supported ChatLab format versions: ${S.join(", ")}.`,{...t,path:i})),y(e,r.exportedAt,`${a}chatlab.exportedAt`,t),m(e,r,"generator",`${a}chatlab.generator`,t),m(e,r,"description",`${a}chatlab.description`,t)}if(!c(n)){o(e,"error","INVALID_META","Expected a meta object.",{...t,path:`${a}meta`});return}u(e,n.name,`${a}meta.name`,t),u(e,n.platform,`${a}meta.platform`,t),n.type!=="group"&&n.type!=="private"&&o(e,"error","INVALID_CHAT_TYPE",'Expected chat type "group" or "private".',{...t,path:`${a}meta.type`}),m(e,n,"groupId",`${a}meta.groupId`,t),m(e,n,"groupAvatar",`${a}meta.groupAvatar`,t),d(n,"ownerId")&&u(e,n.ownerId,`${a}meta.ownerId`,t)&&e.ownerIds.add(n.ownerId),m(e,n,"sourceSessionId",`${a}meta.sourceSessionId`,t)}function N(e,r,n,t){if(e.stats.memberCount++,!c(r)){o(e,"error","INVALID_MEMBER","Expected a member object.",g(t,n));return}u(e,r.platformId,`${n}.platformId`,t)&&(e.memberIds.has(r.platformId)?o(e,"error","DUPLICATE_MEMBER_ID","Member platformId must be unique.",{...t,path:`${n}.platformId`}):e.memberIds.add(r.platformId)),u(e,r.accountName,`${n}.accountName`,t),m(e,r,"groupNickname",`${n}.groupNickname`,t),m(e,r,"avatar",`${n}.avatar`,t),d(r,"aliases")&&(!Array.isArray(r.aliases)||r.aliases.some(a=>typeof a!="string"))&&o(e,"error","INVALID_ALIASES","Expected aliases to be an array of strings.",{...t,path:`${n}.aliases`}),d(r,"roles")&&(Array.isArray(r.roles)?r.roles.forEach((a,i)=>{let p=`${n}.roles[${i}]`;if(!c(a)){o(e,"error","INVALID_ROLE","Expected a role object.",{...t,path:p});return}u(e,a.id,`${p}.id`,t),m(e,a,"name",`${p}.name`,t)}):o(e,"error","INVALID_ROLES","Expected roles to be an array.",{...t,path:`${n}.roles`}))}function _(e,r,n,t){if(e.stats.messageCount++,!c(r)){o(e,"error","INVALID_MESSAGE","Expected a message object.",g(t,n));return}u(e,r.sender,`${n}.sender`,t)&&e.senderIds.add(r.sender),u(e,r.accountName,`${n}.accountName`,t),y(e,r.timestamp,`${n}.timestamp`,t)&&(e.lastTimestamp!==void 0&&r.timestamp<e.lastTimestamp&&o(e,"warning","OUT_OF_ORDER_TIMESTAMP","Messages should be ordered by timestamp ascending.",g(t,`${n}.timestamp`)),e.lastTimestamp=r.timestamp),(typeof r.type!="number"||!Number.isInteger(r.type)||!G.has(r.type))&&o(e,"error","INVALID_MESSAGE_TYPE","Expected a supported numeric ChatLab message type.",{...t,path:`${n}.type`}),(!d(r,"content")||typeof r.content!="string"&&r.content!==null)&&o(e,"error","INVALID_MESSAGE_CONTENT","Expected content to be a string or null.",{...t,path:`${n}.content`}),m(e,r,"groupNickname",`${n}.groupNickname`,t),d(r,"platformMessageId")&&u(e,r.platformMessageId,`${n}.platformMessageId`,t)&&(e.messageIds.has(r.platformMessageId)?o(e,"error","DUPLICATE_MESSAGE_ID","platformMessageId must be unique.",{...t,Line truncated
`)}if(!L)f({ok:!1,command:"validate",error:{code:"MISSING_FILE",message:"Usage: node validate-chatlab.mjs <file>"},meta:{apiVersion:1,standalone:!0}}),process.exitCode=2;else{let e=z.resolve(L);if(!U(e))f({ok:!1,command:"validate",error:{code:"FILE_NOT_FOUND",message:`File not found: ${e}`,hint:"Pass an absolute path or a path relative to the current working directory."},meta:{apiVersion:1,standalone:!0}}),process.exitCode=3;else try{let r=await T(e);f(r.valid?{ok:!0,command:"validate",data:r,meta:{apiVersion:1,standalone:!0}}:{ok:!1,command:"validate",data:r,error:{code:"INVALID_CHATLAB_FORMAT",message:`Validation found ${r.errorCount} error(s).`,hint:"Fix every error before importing the file into ChatLab."},meta:{apiVersion:1,standalone:!0}}),r.valid||(process.exitCode=1)}catch(r){f({ok:!1,command:"validate",error:{code:"VALIDATION_FAILED",message:r instanceof Error?r.message:String(r)},meta:{apiVersion:1,standalone:!0}}),process.exitCode=1}}
@@ -1,4 +1,5 @@
#!/usr/bin/env node
// Generated from the canonical ChatLab validator. Do not edit directly.
import{existsSync as U}from"node:fs";import z from"node:path";import{createReadStream as O}from"node:fs";import{readFile as P,stat as k}from"node:fs/promises";import{extname as V}from"node:path";import{createInterface as D}from"node:readline";var E="0.0.2",S=["0.0.1",E];function C(e){return S.includes(e)}var h=(s=>(s[s.TEXT=0]="TEXT",s[s.IMAGE=1]="IMAGE",s[s.VOICE=2]="VOICE",s[s.VIDEO=3]="VIDEO",s[s.FILE=4]="FILE",s[s.EMOJI=5]="EMOJI",s[s.LINK=7]="LINK",s[s.LOCATION=8]="LOCATION",s[s.RED_PACKET=20]="RED_PACKET",s[s.TRANSFER=21]="TRANSFER",s[s.POKE=22]="POKE",s[s.CALL=23]="CALL",s[s.SHARE=24]="SHARE",s[s.REPLY=25]="REPLY",s[s.FORWARD=26]="FORWARD",s[s.CONTACT=27]="CONTACT",s[s.SYSTEM=80]="SYSTEM",s[s.RECALL=81]="RECALL",s[s.OTHER=99]="OTHER",s))(h||{});var v=100,M=1e11,G=new Set(Object.values(h).filter(e=>typeof e=="number"));function A(e,r){return{format:e,fileSize:r,errorCount:0,warningCount:0,issues:[],stats:{headerCount:0,memberCount:0,messageCount:0,uniqueSenderCount:0,commentLineCount:0,blankLineCount:0},memberIds:new Set,ownerIds:new Set,senderIds:new Set,messageIds:new Set,replyTargetIds:new Set}}function a(e,r,n,t,o={}){r==="error"?e.errorCount++:e.warningCount++,e.issues.length<v&&e.issues.push({severity:r,code:n,message:t,...o})}function d(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function c(e,r){return Object.prototype.hasOwnProperty.call(e,r)}function g(e,r){return{...e,path:r}}function u(e,r,n,t){return typeof r=="string"&&r.trim().length>0?!0:(a(e,"error","INVALID_REQUIRED_STRING","Expected a non-empty string.",g(t,n)),!1)}function m(e,r,n,t,o){c(r,n)&&typeof r[n]!="string"&&a(e,"error","INVALID_OPTIONAL_STRING","Expected a string.",g(o,t))}function y(e,r,n,t){return typeof r=="number"&&Number.isInteger(r)&&r>=0&&r<M?!0:(a(e,"error","INVALID_UNIX_SECONDS","Expected a non-negative integer Unix timestamp in seconds, not milliseconds.",g(t,n)),!1)}function x(e,r,n,t,o=""){if(e.stats.headerCount++,!d(r))a(e,"error","INVALID_CHATLAB_HEADER","Expected a chatlab object.",{...t,path:`${o}chatlab`});else{let i=`${o}chatlab.version`;typeof r.version!="string"?a(e,"error","INVALID_VERSION","Expected a string format version.",{...t,path:i}):(e.version=r.version,C(r.version)||a(e,"error","UNSUPPORTED_VERSION",`Supported ChatLab format versions: ${S.join(", ")}.`,{...t,path:i})),y(e,r.exportedAt,`${o}chatlab.exportedAt`,t),m(e,r,"generator",`${o}chatlab.generator`,t),m(e,r,"description",`${o}chatlab.description`,t)}if(!d(n)){a(e,"error","INVALID_META","Expected a meta object.",{...t,path:`${o}meta`});return}u(e,n.name,`${o}meta.name`,t),u(e,n.platform,`${o}meta.platform`,t),n.type!=="group"&&n.type!=="private"&&a(e,"error","INVALID_CHAT_TYPE",'Expected chat type "group" or "private".',{...t,path:`${o}meta.type`}),m(e,n,"groupId",`${o}meta.groupId`,t),m(e,n,"groupAvatar",`${o}meta.groupAvatar`,t),c(n,"ownerId")&&u(e,n.ownerId,`${o}meta.ownerId`,t)&&e.ownerIds.add(n.ownerId),m(e,n,"sourceSessionId",`${o}meta.sourceSessionId`,t)}function N(e,r,n,t){if(e.stats.memberCount++,!d(r)){a(e,"error","INVALID_MEMBER","Expected a member object.",g(t,n));return}u(e,r.platformId,`${n}.platformId`,t)&&(e.memberIds.has(r.platformId)?a(e,"error","DUPLICATE_MEMBER_ID","Member platformId must be unique.",{...t,path:`${n}.platformId`}):e.memberIds.add(r.platformId)),u(e,r.accountName,`${n}.accountName`,t),m(e,r,"groupNickname",`${n}.groupNickname`,t),m(e,r,"avatar",`${n}.avatar`,t),c(r,"aliases")&&(!Array.isArray(r.aliases)||r.aliases.some(o=>typeof o!="string"))&&a(e,"error","INVALID_ALIASES","Expected aliases to be an array of strings.",{...t,path:`${n}.aliases`}),c(r,"roles")&&(Array.isArray(r.roles)?r.roles.forEach((o,i)=>{let p=`${n}.roles[${i}]`;if(!d(o)){a(e,"error","INVALID_ROLE","Expected a role object.",{...t,path:p});return}u(e,o.id,`${p}.id`,t),m(e,o,"name",`${p}.name`,t)}):a(e,"error","INVALID_ROLES","Expected roles to be an array.",{...t,path:`${n}.roles`}))}function _(e,r,n,t){if(e.stats.messageCount++,!d(r)){a(e,"error","INVALID_MESSAGE","Expected a message object.",g(t,n));return}u(e,r.sender,`${n}.sender`,t)&&e.senderIds.add(r.sender),u(e,r.accountName,`${n}.accountName`,t),y(e,r.timestamp,`${n}.timestamp`,t)&&(e.lastTimestamp!==void 0&&r.timestamp<e.lastTimestamp&&a(e,"warning","OUT_OF_ORDER_TIMESTAMP","Messages should be ordered by timestamp ascending.",g(t,`${n}.timestamp`)),e.lastTimestamp=r.timestamp),(typeof r.type!="number"||!Number.isInteger(r.type)||!G.has(r.type))&&a(e,"error","INVALID_MESSAGE_TYPE","Expected a supported numeric ChatLab message type.",{...t,path:`${n}.type`}),(!c(r,"content")||typeof r.content!="string"&&r.content!==null)&&a(e,"error","INVALID_MESSAGE_CONTENT","Expected content to be a string or null.",{...t,path:`${n}.content`}),m(e,r,"groupNickname",`${n}.groupNickname`,t),c(r,"platformMessageId")&&u(e,r.platformMessageId,`${n}.platformMessageId`,t)&&(e.messageIds.has(r.platformMessageId)?a(e,"error","DUPLICATE_MESSAGE_ID","platformMessageId must be unique.",{...t,Line truncated
/* eslint-disable */
import{existsSync as U}from"node:fs";import z from"node:path";import{createReadStream as O}from"node:fs";import{readFile as P,stat as k}from"node:fs/promises";import{extname as V}from"node:path";import{createInterface as D}from"node:readline";var E="0.0.2",S=["0.0.1",E];function C(e){return S.includes(e)}var h=(s=>(s[s.TEXT=0]="TEXT",s[s.IMAGE=1]="IMAGE",s[s.VOICE=2]="VOICE",s[s.VIDEO=3]="VIDEO",s[s.FILE=4]="FILE",s[s.EMOJI=5]="EMOJI",s[s.LINK=7]="LINK",s[s.LOCATION=8]="LOCATION",s[s.RED_PACKET=20]="RED_PACKET",s[s.TRANSFER=21]="TRANSFER",s[s.POKE=22]="POKE",s[s.CALL=23]="CALL",s[s.SHARE=24]="SHARE",s[s.REPLY=25]="REPLY",s[s.FORWARD=26]="FORWARD",s[s.CONTACT=27]="CONTACT",s[s.SYSTEM=80]="SYSTEM",s[s.RECALL=81]="RECALL",s[s.OTHER=99]="OTHER",s))(h||{});var v=100,M=1e11,G=new Set(Object.values(h).filter(e=>typeof e=="number"));function A(e,r){return{format:e,fileSize:r,errorCount:0,warningCount:0,issues:[],stats:{headerCount:0,memberCount:0,messageCount:0,uniqueSenderCount:0,commentLineCount:0,blankLineCount:0},memberIds:new Set,ownerIds:new Set,senderIds:new Set,messageIds:new Set,replyTargetIds:new Set}}function o(e,r,n,t,a={}){r==="error"?e.errorCount++:e.warningCount++,e.issues.length<v&&e.issues.push({severity:r,code:n,message:t,...a})}function c(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function d(e,r){return Object.prototype.hasOwnProperty.call(e,r)}function g(e,r){return{...e,path:r}}function u(e,r,n,t){return typeof r=="string"&&r.trim().length>0?!0:(o(e,"error","INVALID_REQUIRED_STRING","Expected a non-empty string.",g(t,n)),!1)}function m(e,r,n,t,a){d(r,n)&&typeof r[n]!="string"&&o(e,"error","INVALID_OPTIONAL_STRING","Expected a string.",g(a,t))}function y(e,r,n,t){return typeof r=="number"&&Number.isInteger(r)&&r>=0&&r<M?!0:(o(e,"error","INVALID_UNIX_SECONDS","Expected a non-negative integer Unix timestamp in seconds, not milliseconds.",g(t,n)),!1)}function x(e,r,n,t,a=""){if(e.stats.headerCount++,!c(r))o(e,"error","INVALID_CHATLAB_HEADER","Expected a chatlab object.",{...t,path:`${a}chatlab`});else{let i=`${a}chatlab.version`;typeof r.version!="string"?o(e,"error","INVALID_VERSION","Expected a string format version.",{...t,path:i}):(e.version=r.version,C(r.version)||o(e,"error","UNSUPPORTED_VERSION",`Supported ChatLab format versions: ${S.join(", ")}.`,{...t,path:i})),y(e,r.exportedAt,`${a}chatlab.exportedAt`,t),m(e,r,"generator",`${a}chatlab.generator`,t),m(e,r,"description",`${a}chatlab.description`,t)}if(!c(n)){o(e,"error","INVALID_META","Expected a meta object.",{...t,path:`${a}meta`});return}u(e,n.name,`${a}meta.name`,t),u(e,n.platform,`${a}meta.platform`,t),n.type!=="group"&&n.type!=="private"&&o(e,"error","INVALID_CHAT_TYPE",'Expected chat type "group" or "private".',{...t,path:`${a}meta.type`}),m(e,n,"groupId",`${a}meta.groupId`,t),m(e,n,"groupAvatar",`${a}meta.groupAvatar`,t),d(n,"ownerId")&&u(e,n.ownerId,`${a}meta.ownerId`,t)&&e.ownerIds.add(n.ownerId),m(e,n,"sourceSessionId",`${a}meta.sourceSessionId`,t)}function N(e,r,n,t){if(e.stats.memberCount++,!c(r)){o(e,"error","INVALID_MEMBER","Expected a member object.",g(t,n));return}u(e,r.platformId,`${n}.platformId`,t)&&(e.memberIds.has(r.platformId)?o(e,"error","DUPLICATE_MEMBER_ID","Member platformId must be unique.",{...t,path:`${n}.platformId`}):e.memberIds.add(r.platformId)),u(e,r.accountName,`${n}.accountName`,t),m(e,r,"groupNickname",`${n}.groupNickname`,t),m(e,r,"avatar",`${n}.avatar`,t),d(r,"aliases")&&(!Array.isArray(r.aliases)||r.aliases.some(a=>typeof a!="string"))&&o(e,"error","INVALID_ALIASES","Expected aliases to be an array of strings.",{...t,path:`${n}.aliases`}),d(r,"roles")&&(Array.isArray(r.roles)?r.roles.forEach((a,i)=>{let p=`${n}.roles[${i}]`;if(!c(a)){o(e,"error","INVALID_ROLE","Expected a role object.",{...t,path:p});return}u(e,a.id,`${p}.id`,t),m(e,a,"name",`${p}.name`,t)}):o(e,"error","INVALID_ROLES","Expected roles to be an array.",{...t,path:`${n}.roles`}))}function _(e,r,n,t){if(e.stats.messageCount++,!c(r)){o(e,"error","INVALID_MESSAGE","Expected a message object.",g(t,n));return}u(e,r.sender,`${n}.sender`,t)&&e.senderIds.add(r.sender),u(e,r.accountName,`${n}.accountName`,t),y(e,r.timestamp,`${n}.timestamp`,t)&&(e.lastTimestamp!==void 0&&r.timestamp<e.lastTimestamp&&o(e,"warning","OUT_OF_ORDER_TIMESTAMP","Messages should be ordered by timestamp ascending.",g(t,`${n}.timestamp`)),e.lastTimestamp=r.timestamp),(typeof r.type!="number"||!Number.isInteger(r.type)||!G.has(r.type))&&o(e,"error","INVALID_MESSAGE_TYPE","Expected a supported numeric ChatLab message type.",{...t,path:`${n}.type`}),(!d(r,"content")||typeof r.content!="string"&&r.content!==null)&&o(e,"error","INVALID_MESSAGE_CONTENT","Expected content to be a string or null.",{...t,path:`${n}.content`}),m(e,r,"groupNickname",`${n}.groupNickname`,t),d(r,"platformMessageId")&&u(e,r.platformMessageId,`${n}.platformMessageId`,t)&&(e.messageIds.has(r.platformMessageId)?o(e,"error","DUPLICATE_MESSAGE_ID","platformMessageId must be unique.",{...t,Line truncated
`)}if(!L)f({ok:!1,command:"validate",error:{code:"MISSING_FILE",message:"Usage: node validate-chatlab.mjs <file>"},meta:{apiVersion:1,standalone:!0}}),process.exitCode=2;else{let e=z.resolve(L);if(!U(e))f({ok:!1,command:"validate",error:{code:"FILE_NOT_FOUND",message:`File not found: ${e}`,hint:"Pass an absolute path or a path relative to the current working directory."},meta:{apiVersion:1,standalone:!0}}),process.exitCode=3;else try{let r=await T(e);f(r.valid?{ok:!0,command:"validate",data:r,meta:{apiVersion:1,standalone:!0}}:{ok:!1,command:"validate",data:r,error:{code:"INVALID_CHATLAB_FORMAT",message:`Validation found ${r.errorCount} error(s).`,hint:"Fix every error before importing the file into ChatLab."},meta:{apiVersion:1,standalone:!0}}),r.valid||(process.exitCode=1)}catch(r){f({ok:!1,command:"validate",error:{code:"VALIDATION_FAILED",message:r instanceof Error?r.message:String(r)},meta:{apiVersion:1,standalone:!0}}),process.exitCode=1}}