mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
Merge pull request #43 from sinanyuntu/09-11-onetalk-structured-message-information-collection-classification
fix: 名片消息仅保留标记并按会话补全资料
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trade-message-center/chrome-extension",
|
||||
"version": "0.8.26",
|
||||
"version": "0.8.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -43,10 +43,6 @@ export type OneTalkMessageContentSource =
|
||||
subType: unknown;
|
||||
originalData: unknown;
|
||||
cardType: unknown;
|
||||
contactName: unknown;
|
||||
companyName: unknown;
|
||||
countryCode: unknown;
|
||||
avatarUrl: unknown;
|
||||
orderId: unknown;
|
||||
bizCode: unknown;
|
||||
contractId: unknown;
|
||||
@@ -309,10 +305,6 @@ const normalizeFile = (payload: Record<string, unknown>): OneTalkRawContentDecod
|
||||
: anomaly("media_invalid_schema", "file");
|
||||
};
|
||||
|
||||
const nullableCardText = (value: unknown): string | null | unknown => {
|
||||
return value === undefined || value === null || value === "" ? null : value;
|
||||
};
|
||||
|
||||
const nullableOrderIdentifier = (value: unknown): string | number | null | unknown => {
|
||||
return value === undefined || value === null ? null : value;
|
||||
};
|
||||
@@ -333,15 +325,11 @@ const matchesBusinessCard = (
|
||||
};
|
||||
|
||||
const normalizeBusinessCard = (
|
||||
source: Extract<OneTalkMessageContentSource, { source: "sdk_flat_history" }>,
|
||||
_source: Extract<OneTalkMessageContentSource, { source: "sdk_flat_history" }>,
|
||||
): OneTalkRawContentDecodeResult => {
|
||||
const normalized = {
|
||||
version: ONETALK_CONTENT_VERSION,
|
||||
kind: "business_card" as const,
|
||||
contactName: nullableCardText(source.contactName),
|
||||
companyName: nullableCardText(source.companyName),
|
||||
countryCode: nullableCardText(source.countryCode),
|
||||
avatarUrl: nullableCardText(source.avatarUrl),
|
||||
};
|
||||
return isOneTalkMessageContent(normalized)
|
||||
? { status: "decoded", content: normalized }
|
||||
|
||||
@@ -49,7 +49,6 @@ const flatHistoryMessage = (
|
||||
participantIds: string[],
|
||||
): Record<string, unknown> => {
|
||||
const {
|
||||
contact: _contact,
|
||||
content: _displayContent,
|
||||
conversationCode,
|
||||
messageId,
|
||||
@@ -71,7 +70,6 @@ const flatHistoryMessage = (
|
||||
|
||||
const historyCardSource = (item: Record<string, unknown>): OneTalkMessageContentSource => {
|
||||
const originalData = isObjectRecord(item.originalData) ? item.originalData : undefined;
|
||||
const contact = isObjectRecord(item.contact) ? item.contact : undefined;
|
||||
const orderParams = isObjectRecord(originalData?.params) ? originalData.params : undefined;
|
||||
return {
|
||||
source: "sdk_flat_history",
|
||||
@@ -82,10 +80,6 @@ const historyCardSource = (item: Record<string, unknown>): OneTalkMessageContent
|
||||
subType: item.subType,
|
||||
originalData: item.originalData,
|
||||
cardType: originalData?.cardType,
|
||||
contactName: contact?.name,
|
||||
companyName: contact?.companyName,
|
||||
countryCode: contact?.complianceCountryCode,
|
||||
avatarUrl: contact?.fullPortrait,
|
||||
orderId: orderParams?.orderId,
|
||||
bizCode: orderParams?.bizCode,
|
||||
contractId: orderParams?.contractId,
|
||||
|
||||
@@ -198,21 +198,19 @@ test("normalizes exact SDK flat history media through the shared media contract"
|
||||
);
|
||||
});
|
||||
|
||||
test("classifies only complete flat-history business-card tuples and whitelists the snapshot", () => {
|
||||
test("classifies only complete flat-history business-card tuples without collecting profile data", () => {
|
||||
const result = decodeOneTalkMessageContent(flatBusinessCard());
|
||||
assert.deepEqual(result, {
|
||||
status: "decoded",
|
||||
content: {
|
||||
version: 1,
|
||||
kind: "business_card",
|
||||
contactName: "Buyer Name",
|
||||
companyName: "Buyer Company",
|
||||
countryCode: "CN",
|
||||
avatarUrl: "https://cdn.example.test/avatar/buyer.jpg",
|
||||
},
|
||||
});
|
||||
assert.equal(JSON.stringify(result).includes("originalData"), false);
|
||||
assert.equal(JSON.stringify(result).includes("sign"), false);
|
||||
assert.equal(JSON.stringify(result).includes("Buyer Name"), false);
|
||||
assert.equal(JSON.stringify(result).includes("avatarUrl"), false);
|
||||
for (const overrides of [
|
||||
{ messageType: "send" },
|
||||
{ type: 2 },
|
||||
|
||||
@@ -474,9 +474,12 @@ test("adapts only approved structured card projections through the sole history
|
||||
["history-order", "order"],
|
||||
],
|
||||
);
|
||||
assert.deepEqual(batch.messages[0]?.content, { version: 1, kind: "business_card" });
|
||||
assert.equal(JSON.stringify(batch).includes("secret-sign"), false);
|
||||
assert.equal(JSON.stringify(batch).includes("secret-account"), false);
|
||||
assert.equal(JSON.stringify(batch).includes(orderSummary), false);
|
||||
assert.equal(JSON.stringify(batch).includes("Buyer Name"), false);
|
||||
assert.equal(JSON.stringify(batch).includes("buyer.jpg"), false);
|
||||
});
|
||||
|
||||
test("isolates invalid flat history items without dropping valid siblings", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trade-message-center/mind-test-harness",
|
||||
"version": "0.8.26",
|
||||
"version": "0.8.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -26,6 +26,9 @@ export const harnessMessagesScript = String.raw` const messageKey = (
|
||||
return '<div class="message-content"><p>图片</p><p class="message-meta">' + metadata + '</p>' + preview + '</div>';
|
||||
}
|
||||
if (content.kind === 'business_card') {
|
||||
if (!Object.hasOwn(content, 'contactName')) {
|
||||
return '<div class="message-content"><p>名片</p><p class="message-meta">客户资料暂未提供。</p></div>';
|
||||
}
|
||||
const fields = [content.contactName, content.companyName, content.countryCode]
|
||||
.filter((value) => value !== null)
|
||||
.map(escapeHtml)
|
||||
@@ -33,7 +36,7 @@ export const harnessMessagesScript = String.raw` const messageKey = (
|
||||
const avatar = content.avatarUrl
|
||||
? '<img class="message-image" src="' + escapeHtml(content.avatarUrl) + '" alt="联系人头像" onerror="this.hidden=true">'
|
||||
: '';
|
||||
return '<div class="message-content"><p>名片资料快照</p><p class="message-meta">' + (fields || '未提供资料字段') + '</p>' + avatar + '</div>';
|
||||
return '<div class="message-content"><p>名片关联客户资料</p><p class="message-meta">' + (fields || '未提供资料字段') + '</p>' + avatar + '</div>';
|
||||
}
|
||||
if (content.kind === 'inquiry') {
|
||||
return '<div class="message-content"><p>询盘卡片</p><p class="message-meta">本期只提供分类,不展示未批准的业务字段。</p></div>';
|
||||
|
||||
@@ -46,12 +46,14 @@ export const harnessValidatorsScript = String.raw` const isRecord = (
|
||||
&& isNonNegativeInteger(value.sizeBytes) && isNullableString(value.md5) && isNullableString(value.previewUrl)
|
||||
&& isNullableString(value.thumbnailUrl) && isNullableString(value.downloadUrl)
|
||||
&& (value.downloadState === 'available' || value.downloadState === 'not_provided') && value.urlScope === 'onetalk_session';
|
||||
if (value.kind === 'business_card') return hasExactKeys(value, ['avatarUrl', 'companyName', 'contactName', 'countryCode', 'kind', 'version'])
|
||||
&& value.version === contentVersion
|
||||
&& isNullableText(value.contactName)
|
||||
&& isNullableText(value.companyName)
|
||||
&& isNullableText(value.countryCode)
|
||||
&& (value.avatarUrl === null || isAvatarUrl(value.avatarUrl));
|
||||
if (value.kind === 'business_card') return (hasExactKeys(value, ['kind', 'version'])
|
||||
&& value.version === contentVersion)
|
||||
|| (hasExactKeys(value, ['avatarUrl', 'companyName', 'contactName', 'countryCode', 'kind', 'version'])
|
||||
&& value.version === contentVersion
|
||||
&& isNullableText(value.contactName)
|
||||
&& isNullableText(value.companyName)
|
||||
&& isNullableText(value.countryCode)
|
||||
&& (value.avatarUrl === null || isAvatarUrl(value.avatarUrl)));
|
||||
if (value.kind === 'inquiry') return hasExactKeys(value, ['kind', 'version']) && value.version === contentVersion;
|
||||
if (value.kind === 'order') return hasExactKeys(value, ['actions', 'bizCode', 'contractId', 'id', 'kind', 'orderAmount', 'orderAmountCurrency', 'orderId', 'paymentAmount', 'paymentAmountCurrency', 'statusMessageKey', 'tenant', 'version'])
|
||||
&& value.version === contentVersion
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trade-message-center/onetalk-contract",
|
||||
"version": "0.8.26",
|
||||
"version": "0.8.27",
|
||||
"private": true,
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -50,10 +50,14 @@ export type OneTalkFileContent = {
|
||||
urlScope: "onetalk_session";
|
||||
};
|
||||
|
||||
/** 消息时点的会话联系人资料快照,不是当前联系人 profile。 */
|
||||
/** 已观察到的名片类别;客户资料由读取端按会话关联,不写入消息事实。 */
|
||||
export type OneTalkBusinessCardContent = {
|
||||
version: typeof ONETALK_CONTENT_VERSION;
|
||||
kind: "business_card";
|
||||
};
|
||||
|
||||
/** Mind 读取端按会话当前客户资料补全的名片展示字段。 */
|
||||
export type OneTalkBusinessCardViewContent = OneTalkBusinessCardContent & {
|
||||
contactName: string | null;
|
||||
companyName: string | null;
|
||||
countryCode: string | null;
|
||||
@@ -96,6 +100,8 @@ export type OneTalkMessageContent =
|
||||
| OneTalkInquiryContent
|
||||
| OneTalkOrderContent;
|
||||
|
||||
export type OneTalkCenterMessageContent = OneTalkMessageContent | OneTalkBusinessCardViewContent;
|
||||
|
||||
/** Mind HTTP 会话与 conversation.updated 共用的公开会话读取模型。 */
|
||||
export type CenterConversation = {
|
||||
conversationId: string;
|
||||
@@ -114,7 +120,7 @@ export type OneTalkCenterMessage = {
|
||||
participantIds: string[];
|
||||
direction: OneTalkDirection;
|
||||
sentAtMs: number;
|
||||
content: OneTalkMessageContent;
|
||||
content: OneTalkCenterMessageContent;
|
||||
};
|
||||
|
||||
export type OneTalkMessageContentDecodeResult =
|
||||
@@ -157,7 +163,7 @@ const CONTENT_KEYS = {
|
||||
"downloadState",
|
||||
"urlScope",
|
||||
],
|
||||
business_card: ["version", "kind", "contactName", "companyName", "countryCode", "avatarUrl"],
|
||||
business_card: ["version", "kind"],
|
||||
inquiry: ["version", "kind"],
|
||||
order: [
|
||||
"version",
|
||||
@@ -409,6 +415,23 @@ const isOneTalkBusinessCardContent = (
|
||||
return (
|
||||
hasExactKeys(value, CONTENT_KEYS.business_card) &&
|
||||
value.version === ONETALK_CONTENT_VERSION &&
|
||||
value.kind === "business_card"
|
||||
);
|
||||
};
|
||||
|
||||
const isOneTalkBusinessCardViewContent = (
|
||||
value: Record<string, unknown>,
|
||||
): value is OneTalkBusinessCardViewContent => {
|
||||
return (
|
||||
hasExactKeys(value, [
|
||||
"version",
|
||||
"kind",
|
||||
"contactName",
|
||||
"companyName",
|
||||
"countryCode",
|
||||
"avatarUrl",
|
||||
]) &&
|
||||
value.version === ONETALK_CONTENT_VERSION &&
|
||||
value.kind === "business_card" &&
|
||||
isNullableText(value.contactName) &&
|
||||
isNullableText(value.companyName) &&
|
||||
@@ -478,6 +501,14 @@ export const isOneTalkMessageContent = (value: unknown): value is OneTalkMessage
|
||||
return decodeOneTalkMessageContent(value).ok;
|
||||
};
|
||||
|
||||
/** 判断值是否为可供 Mind 读取的消息内容;名片可以附带当前会话客户资料。 */
|
||||
export const isOneTalkCenterMessageContent = (
|
||||
value: unknown,
|
||||
): value is OneTalkCenterMessageContent => {
|
||||
if (isOneTalkMessageContent(value)) return true;
|
||||
return isPlainRecord(value) && isOneTalkBusinessCardViewContent(value);
|
||||
};
|
||||
|
||||
/** 判断值是否为 exact-shape 的 Mind 公开会话读取模型。 */
|
||||
export const isCenterConversation = (value: unknown): value is CenterConversation => {
|
||||
if (
|
||||
@@ -515,6 +546,6 @@ export const isOneTalkCenterMessage = (value: unknown): value is OneTalkCenterMe
|
||||
typeof value.direction === "string" &&
|
||||
ONETALK_DIRECTIONS.includes(value.direction as OneTalkDirection) &&
|
||||
isNonNegativeSafeInteger(value.sentAtMs) &&
|
||||
isOneTalkMessageContent(value.content)
|
||||
isOneTalkCenterMessageContent(value.content)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -167,6 +167,10 @@ const pdfContent = {
|
||||
const businessCardContent = {
|
||||
version: ONETALK_CONTENT_VERSION,
|
||||
kind: "business_card" as const,
|
||||
};
|
||||
|
||||
const businessCardViewContent = {
|
||||
...businessCardContent,
|
||||
contactName: "Buyer Name",
|
||||
companyName: "Buyer Company",
|
||||
countryCode: "CN",
|
||||
@@ -1125,13 +1129,27 @@ test("validates normalized text, JPEG, ZIP, and PDF content with exact metadata"
|
||||
assert.equal(imageSendCommand.type, "send.command");
|
||||
});
|
||||
|
||||
test("validates exact business-card, inquiry, and order content without raw card fields", () => {
|
||||
test("stores exact business-card markers and only exposes customer fields in public messages", () => {
|
||||
for (const content of [businessCardContent, inquiryContent, orderContent]) {
|
||||
assert.equal(isOneTalkMessageContent(content), true);
|
||||
assert.deepEqual(decodeOneTalkMessageContent(content), { ok: true, content });
|
||||
assert.equal(isOneTalkMessage({ ...observedMessage, content }), true);
|
||||
}
|
||||
|
||||
const publicBusinessCardMessage = { ...centerMessage, content: businessCardViewContent };
|
||||
assert.equal(isOneTalkMessageContent(businessCardViewContent), false);
|
||||
assert.equal(isOneTalkCenterMessage(publicBusinessCardMessage), true);
|
||||
assert.equal(
|
||||
decodeOneTalkFrame({
|
||||
...frameBase,
|
||||
connectionType: "mind_page",
|
||||
scope: mindScope,
|
||||
type: "message.created",
|
||||
payload: { message: publicBusinessCardMessage },
|
||||
}).ok,
|
||||
true,
|
||||
);
|
||||
|
||||
for (const content of [
|
||||
{ ...businessCardContent, contactName: "" },
|
||||
{ ...businessCardContent, avatarUrl: " https://cdn.example.test/avatar/buyer.jpg" },
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "onetalk_message" DROP CONSTRAINT "onetalk_message_content_v1_chk";--> statement-breakpoint
|
||||
UPDATE "onetalk_message" SET "content" = '{"version":1,"kind":"business_card"}'::jsonb WHERE "content" ->> 'kind' = 'business_card';--> statement-breakpoint
|
||||
ALTER TABLE "onetalk_message" ADD CONSTRAINT "onetalk_message_content_v1_chk" CHECK (jsonb_typeof("onetalk_message"."content") = 'object' and ("onetalk_message"."content" ->> 'version') = '1' and ("onetalk_message"."content" ->> 'kind') in ('text', 'image', 'file', 'business_card', 'inquiry', 'order') and (("onetalk_message"."content" ->> 'kind') <> 'business_card' or "onetalk_message"."content" = '{"version":1,"kind":"business_card"}'::jsonb));
|
||||
@@ -0,0 +1,850 @@
|
||||
{
|
||||
"id": "bf5d1038-fa8c-416d-8142-898614bd4bf3",
|
||||
"prevId": "e2524a45-905a-4575-a57a-49b6f1c0c30a",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.onetalk_buyer_fact": {
|
||||
"name": "onetalk_buyer_fact",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"channel_account_id": {
|
||||
"name": "channel_account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"buyer_tags": {
|
||||
"name": "buyer_tags",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"buyer_features": {
|
||||
"name": "buyer_features",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"registration_date": {
|
||||
"name": "registration_date",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"company_website": {
|
||||
"name": "company_website",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tags_state": {
|
||||
"name": "tags_state",
|
||||
"type": "onetalk_buyer_fact_source_state",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"features_state": {
|
||||
"name": "features_state",
|
||||
"type": "onetalk_buyer_fact_source_state",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"contact_details_state": {
|
||||
"name": "contact_details_state",
|
||||
"type": "onetalk_buyer_fact_source_state",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tags_last_attempted_at": {
|
||||
"name": "tags_last_attempted_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"features_last_attempted_at": {
|
||||
"name": "features_last_attempted_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"contact_details_last_attempted_at": {
|
||||
"name": "contact_details_last_attempted_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tags_confirmed_at": {
|
||||
"name": "tags_confirmed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"features_confirmed_at": {
|
||||
"name": "features_confirmed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"contact_details_confirmed_at": {
|
||||
"name": "contact_details_confirmed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tags_error_code": {
|
||||
"name": "tags_error_code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"features_error_code": {
|
||||
"name": "features_error_code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"contact_details_error_code": {
|
||||
"name": "contact_details_error_code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"fact_fingerprint": {
|
||||
"name": "fact_fingerprint",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"received_at": {
|
||||
"name": "received_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"onetalk_buyer_fact_channel_account_id_conversation_id_pk": {
|
||||
"name": "onetalk_buyer_fact_channel_account_id_conversation_id_pk",
|
||||
"columns": ["channel_account_id", "conversation_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.onetalk_contact_profile": {
|
||||
"name": "onetalk_contact_profile",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"channel_account_id": {
|
||||
"name": "channel_account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ali_id": {
|
||||
"name": "ali_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"login_id": {
|
||||
"name": "login_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"company_name": {
|
||||
"name": "company_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"country_code": {
|
||||
"name": "country_code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"current_time_zone": {
|
||||
"name": "current_time_zone",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"service_type": {
|
||||
"name": "service_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"avatar_url": {
|
||||
"name": "avatar_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"observed_at_ms": {
|
||||
"name": "observed_at_ms",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"profile_fingerprint": {
|
||||
"name": "profile_fingerprint",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"observation_status": {
|
||||
"name": "observation_status",
|
||||
"type": "onetalk_contact_profile_observation_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"received_at": {
|
||||
"name": "received_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"onetalk_contact_profile_channel_account_id_conversation_id_pk": {
|
||||
"name": "onetalk_contact_profile_channel_account_id_conversation_id_pk",
|
||||
"columns": ["channel_account_id", "conversation_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.onetalk_conversation": {
|
||||
"name": "onetalk_conversation",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"channel_account_id": {
|
||||
"name": "channel_account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"participant_ids": {
|
||||
"name": "participant_ids",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"biz_type": {
|
||||
"name": "biz_type",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"conversation_type": {
|
||||
"name": "conversation_type",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"conversation_kind": {
|
||||
"name": "conversation_kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"join_time_ms": {
|
||||
"name": "join_time_ms",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"modify_time_ms": {
|
||||
"name": "modify_time_ms",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_message_at_ms": {
|
||||
"name": "last_message_at_ms",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_contact_time_ms": {
|
||||
"name": "last_contact_time_ms",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"message_preview": {
|
||||
"name": "message_preview",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"sync_phase": {
|
||||
"name": "sync_phase",
|
||||
"type": "onetalk_sync_phase",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'initial'"
|
||||
},
|
||||
"sync_result": {
|
||||
"name": "sync_result",
|
||||
"type": "onetalk_sync_result",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'incomplete'"
|
||||
},
|
||||
"latest_message_id": {
|
||||
"name": "latest_message_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"history_complete": {
|
||||
"name": "history_complete",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"message_count": {
|
||||
"name": "message_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"first_discovered_at": {
|
||||
"name": "first_discovered_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"last_observed_at": {
|
||||
"name": "last_observed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"anchor_updated_at": {
|
||||
"name": "anchor_updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"onetalk_conversation_last_observed_idx": {
|
||||
"name": "onetalk_conversation_last_observed_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "channel_account_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "last_observed_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"onetalk_conversation_contact_time_idx": {
|
||||
"name": "onetalk_conversation_contact_time_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "channel_account_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "last_contact_time_ms",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "conversation_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"onetalk_conversation_channel_account_id_conversation_id_pk": {
|
||||
"name": "onetalk_conversation_channel_account_id_conversation_id_pk",
|
||||
"columns": ["channel_account_id", "conversation_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.onetalk_message": {
|
||||
"name": "onetalk_message",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"channel_account_id": {
|
||||
"name": "channel_account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"message_id": {
|
||||
"name": "message_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"sender_id": {
|
||||
"name": "sender_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"binding": {
|
||||
"name": "binding",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"mind_user_id": {
|
||||
"name": "mind_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"direction": {
|
||||
"name": "direction",
|
||||
"type": "onetalk_message_direction",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"observation_type": {
|
||||
"name": "observation_type",
|
||||
"type": "onetalk_observation_type",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"sent_at_ms": {
|
||||
"name": "sent_at_ms",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"participant_ids": {
|
||||
"name": "participant_ids",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"read_status": {
|
||||
"name": "read_status",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"message_status": {
|
||||
"name": "message_status",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"unread_count": {
|
||||
"name": "unread_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"first_observed_at": {
|
||||
"name": "first_observed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"last_observed_at": {
|
||||
"name": "last_observed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"onetalk_message_conversation_time_idx": {
|
||||
"name": "onetalk_message_conversation_time_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "channel_account_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "conversation_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "sent_at_ms",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "message_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"onetalk_message_sender_idx": {
|
||||
"name": "onetalk_message_sender_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "sender_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"onetalk_message_channel_account_id_conversation_id_message_id_pk": {
|
||||
"name": "onetalk_message_channel_account_id_conversation_id_message_id_pk",
|
||||
"columns": ["channel_account_id", "conversation_id", "message_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {
|
||||
"onetalk_message_content_v1_chk": {
|
||||
"name": "onetalk_message_content_v1_chk",
|
||||
"value": "jsonb_typeof(\"onetalk_message\".\"content\") = 'object' and (\"onetalk_message\".\"content\" ->> 'version') = '1' and (\"onetalk_message\".\"content\" ->> 'kind') in ('text', 'image', 'file', 'business_card', 'inquiry', 'order') and ((\"onetalk_message\".\"content\" ->> 'kind') <> 'business_card' or \"onetalk_message\".\"content\" = '{\"version\":1,\"kind\":\"business_card\"}'::jsonb)"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.onetalk_message_anomaly": {
|
||||
"name": "onetalk_message_anomaly",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"fingerprint": {
|
||||
"name": "fingerprint",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"anomaly_type": {
|
||||
"name": "anomaly_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"binding": {
|
||||
"name": "binding",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"mind_user_id": {
|
||||
"name": "mind_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"channel_account_id": {
|
||||
"name": "channel_account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"missing_fields": {
|
||||
"name": "missing_fields",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"observation_source": {
|
||||
"name": "observation_source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"payload": {
|
||||
"name": "payload",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "onetalk_anomaly_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'open'"
|
||||
},
|
||||
"occurrence_count": {
|
||||
"name": "occurrence_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"first_seen_at": {
|
||||
"name": "first_seen_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"last_seen_at": {
|
||||
"name": "last_seen_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"resolved_at": {
|
||||
"name": "resolved_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"onetalk_message_anomaly_fingerprint_uidx": {
|
||||
"name": "onetalk_message_anomaly_fingerprint_uidx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "fingerprint",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"onetalk_message_anomaly_scope_idx": {
|
||||
"name": "onetalk_message_anomaly_scope_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "channel_account_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "conversation_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "status",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.onetalk_anomaly_status": {
|
||||
"name": "onetalk_anomaly_status",
|
||||
"schema": "public",
|
||||
"values": ["open", "resolved", "ignored"]
|
||||
},
|
||||
"public.onetalk_buyer_fact_source_state": {
|
||||
"name": "onetalk_buyer_fact_source_state",
|
||||
"schema": "public",
|
||||
"values": ["pending", "confirmed", "failed"]
|
||||
},
|
||||
"public.onetalk_contact_profile_observation_status": {
|
||||
"name": "onetalk_contact_profile_observation_status",
|
||||
"schema": "public",
|
||||
"values": ["confirmed", "partial"]
|
||||
},
|
||||
"public.onetalk_message_direction": {
|
||||
"name": "onetalk_message_direction",
|
||||
"schema": "public",
|
||||
"values": ["sent", "received"]
|
||||
},
|
||||
"public.onetalk_observation_type": {
|
||||
"name": "onetalk_observation_type",
|
||||
"schema": "public",
|
||||
"values": ["new", "history"]
|
||||
},
|
||||
"public.onetalk_sync_phase": {
|
||||
"name": "onetalk_sync_phase",
|
||||
"schema": "public",
|
||||
"values": ["initial", "incremental"]
|
||||
},
|
||||
"public.onetalk_sync_result": {
|
||||
"name": "onetalk_sync_result",
|
||||
"schema": "public",
|
||||
"values": ["succeeded", "succeeded_with_anomalies", "failed", "incomplete"]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,13 @@
|
||||
"when": 1789120804420,
|
||||
"tag": "0010_wooden_naoko",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "7",
|
||||
"when": 1789130586184,
|
||||
"tag": "0011_mushy_baron_strucker",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trade-message-center/server",
|
||||
"version": "0.8.26",
|
||||
"version": "0.8.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -113,7 +113,7 @@ export const onetalkMessage = pgTable(
|
||||
index("onetalk_message_sender_idx").on(table.senderId),
|
||||
check(
|
||||
"onetalk_message_content_v1_chk",
|
||||
sql`jsonb_typeof(${table.content}) = 'object' and (${table.content} ->> 'version') = '1' and (${table.content} ->> 'kind') in ('text', 'image', 'file', 'business_card', 'inquiry', 'order')`,
|
||||
sql`jsonb_typeof(${table.content}) = 'object' and (${table.content} ->> 'version') = '1' and (${table.content} ->> 'kind') in ('text', 'image', 'file', 'business_card', 'inquiry', 'order') and ((${table.content} ->> 'kind') <> 'business_card' or ${table.content} = '{"version":1,"kind":"business_card"}'::jsonb)`,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
CenterConversation,
|
||||
OneTalkReadConversation,
|
||||
OneTalkReadConversationRow,
|
||||
OneTalkReadCustomerProfile,
|
||||
OneTalkReadMessageRow,
|
||||
} from "./read-model.ts";
|
||||
|
||||
@@ -92,9 +93,30 @@ type OneTalkCenterMessageInput = Pick<
|
||||
| "content"
|
||||
>;
|
||||
|
||||
/** 从同一 normalized JSONB 事实投影 Mind-facing 消息;不重新解释 raw content。 */
|
||||
const projectMessageContent = (
|
||||
content: OneTalkMessage["content"],
|
||||
customerProfile?: Pick<
|
||||
OneTalkReadCustomerProfile,
|
||||
"name" | "avatarUrl" | "countryCode" | "companyName"
|
||||
>,
|
||||
): OneTalkCenterMessage["content"] => {
|
||||
if (content.kind !== "business_card" || customerProfile === undefined) return { ...content };
|
||||
return {
|
||||
...content,
|
||||
contactName: customerProfile?.name ?? null,
|
||||
companyName: customerProfile?.companyName ?? null,
|
||||
countryCode: customerProfile?.countryCode ?? null,
|
||||
avatarUrl: customerProfile?.avatarUrl ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
/** 从 normalized JSONB 事实和会话客户资料投影 Mind-facing 消息。 */
|
||||
export const toOneTalkCenterMessage = (
|
||||
message: OneTalkCenterMessageInput,
|
||||
customerProfile?: Pick<
|
||||
OneTalkReadCustomerProfile,
|
||||
"name" | "avatarUrl" | "countryCode" | "companyName"
|
||||
>,
|
||||
): OneTalkCenterMessage => {
|
||||
if (!isOneTalkMessageContent(message.content)) {
|
||||
throw new Error("Invalid persisted OneTalk message content");
|
||||
@@ -106,9 +128,14 @@ export const toOneTalkCenterMessage = (
|
||||
participantIds: [...message.participantIds],
|
||||
direction: message.direction,
|
||||
sentAtMs: message.sentAtMs,
|
||||
content: { ...message.content },
|
||||
content: projectMessageContent(message.content, customerProfile),
|
||||
};
|
||||
};
|
||||
|
||||
export const projectCenterMessage = (row: OneTalkReadMessageRow): OneTalkCenterMessage =>
|
||||
toOneTalkCenterMessage(row);
|
||||
export const projectCenterMessage = (
|
||||
row: OneTalkReadMessageRow,
|
||||
customerProfile?: Pick<
|
||||
OneTalkReadCustomerProfile,
|
||||
"name" | "avatarUrl" | "countryCode" | "companyName"
|
||||
>,
|
||||
): OneTalkCenterMessage => toOneTalkCenterMessage(row, customerProfile);
|
||||
|
||||
@@ -192,7 +192,18 @@ export const createOneTalkReadService = (
|
||||
const hasMore = rows.length > limit;
|
||||
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
||||
const cursorRow = pageRows.at(-1);
|
||||
const messages = [...pageRows].reverse().map(projectCenterMessage);
|
||||
const customerProfileFields = {
|
||||
name: conversation.name,
|
||||
avatarUrl: conversation.avatarUrl,
|
||||
countryCode: conversation.countryCode ?? null,
|
||||
companyName: conversation.companyName ?? null,
|
||||
};
|
||||
const customerProfile = Object.values(customerProfileFields).some((value) => value !== null)
|
||||
? customerProfileFields
|
||||
: undefined;
|
||||
const messages = [...pageRows]
|
||||
.reverse()
|
||||
.map((row) => projectCenterMessage(row, customerProfile));
|
||||
return {
|
||||
status: "accepted",
|
||||
conversationId: input.conversationId,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// 验证 OneTalk 名片资料从消息 JSONB 中移除的迁移边界
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { migrationDirectory } from "../src/database/migration-config.ts";
|
||||
|
||||
const migrationPath = resolve(process.cwd(), migrationDirectory, "0011_mushy_baron_strucker.sql");
|
||||
|
||||
test("normalizes every persisted business-card payload before enforcing the marker-only check", async () => {
|
||||
const sql = await readFile(migrationPath, "utf8");
|
||||
|
||||
assert.match(sql, /DROP CONSTRAINT\s+"onetalk_message_content_v1_chk"/i);
|
||||
assert.match(sql, /UPDATE\s+"onetalk_message"\s+SET\s+"content"\s*=/i);
|
||||
assert.match(sql, /WHERE\s+"content"\s*->>\s*'kind'\s*=\s*'business_card'/i);
|
||||
assert.match(sql, /\{"version":1,"kind":"business_card"\}/);
|
||||
assert.match(sql, /ADD CONSTRAINT\s+"onetalk_message_content_v1_chk"/i);
|
||||
assert.equal((sql.match(/\bUPDATE\b/gi) ?? []).length, 1);
|
||||
assert.equal((sql.match(/--> statement-breakpoint/g) ?? []).length, 2);
|
||||
});
|
||||
@@ -459,6 +459,47 @@ test("projects normalized text, image, and file content without raw reinterpreta
|
||||
}
|
||||
});
|
||||
|
||||
test("projects business cards from the linked customer profile and leaves missing profiles as markers", async () => {
|
||||
const businessCard = { version: 1 as const, kind: "business_card" as const };
|
||||
const profileHarness = createRepositoryHarness({
|
||||
conversation: () =>
|
||||
conversation({
|
||||
name: "Customer One",
|
||||
avatarUrl: "https://cdn.example.test/customer.jpg",
|
||||
countryCode: "US",
|
||||
companyName: "Customer One LLC",
|
||||
}),
|
||||
messageRows: () => [message({ content: businessCard })],
|
||||
});
|
||||
const profileResult = await createOneTalkReadService(profileHarness.repository).readHistory({
|
||||
scope,
|
||||
conversationId: "conversation-1",
|
||||
});
|
||||
|
||||
assert.equal(profileResult.status, "accepted");
|
||||
if (profileResult.status !== "accepted") return;
|
||||
assert.deepEqual(profileResult.messages[0]?.content, {
|
||||
...businessCard,
|
||||
contactName: "Customer One",
|
||||
companyName: "Customer One LLC",
|
||||
countryCode: "US",
|
||||
avatarUrl: "https://cdn.example.test/customer.jpg",
|
||||
});
|
||||
assert.equal(profileHarness.historyQueries.length, 1);
|
||||
|
||||
const missingProfileHarness = createRepositoryHarness({
|
||||
messageRows: () => [message({ content: businessCard })],
|
||||
});
|
||||
const missingProfileResult = await createOneTalkReadService(
|
||||
missingProfileHarness.repository,
|
||||
).readHistory({ scope, conversationId: "conversation-1" });
|
||||
|
||||
assert.equal(missingProfileResult.status, "accepted");
|
||||
if (missingProfileResult.status !== "accepted") return;
|
||||
assert.deepEqual(missingProfileResult.messages[0]?.content, businessCard);
|
||||
assert.deepEqual(businessCard, { version: 1, kind: "business_card" });
|
||||
});
|
||||
|
||||
test("projects one persisted media fact identically for history and message.created", () => {
|
||||
const fact: OneTalkMessage = {
|
||||
messageId: "image-1",
|
||||
@@ -545,10 +586,15 @@ test("CenterMessage accepts normalized media and structured-card values", () =>
|
||||
actions: [{ name: "pay", messageKey: "order.pay", payStep: "deposit" }],
|
||||
},
|
||||
} satisfies CenterMessage;
|
||||
const verifiedBusinessCard = {
|
||||
...messageFieldsForProjectionTest(),
|
||||
content: { version: 1, kind: "business_card" },
|
||||
} satisfies CenterMessage;
|
||||
|
||||
assert.equal(verifiedImage.content.kind, "image");
|
||||
assert.equal(verifiedFile.content.fileName, "quote.pdf");
|
||||
assert.equal(verifiedOrder.content.statusMessageKey, "order.pending_payment");
|
||||
assert.equal(verifiedBusinessCard.content.kind, "business_card");
|
||||
});
|
||||
|
||||
test("rejects persisted image content with retired dimensions at the read boundary", () => {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trade-message-center",
|
||||
"version": "0.8.26",
|
||||
"version": "0.8.27",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*"
|
||||
|
||||
Reference in New Issue
Block a user