@tbox.cn/app-module-customer-service 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +16 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/chunk-PN4SPFWE.js +277 -0
- package/dist/client/index.d.ts +15 -0
- package/dist/client/index.js +86 -0
- package/dist/customer-service-UTQ5AVTL.css +141 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +12 -0
- package/dist/server/index.d.ts +106 -0
- package/dist/server/index.js +14 -0
- package/package.json +69 -0
- package/skills/customer-service/SKILL.md +82 -0
- package/src/client/cards/customer-service-faq/index.tsx +78 -0
- package/src/client/cards/index.ts +1 -0
- package/src/client/index.ts +21 -0
- package/src/client/styles/customer-service.css +141 -0
- package/src/index.ts +1 -0
- package/src/server/cards/customer-service-faq/meta.ts +16 -0
- package/src/server/index.ts +115 -0
- package/src/server/ports/index.ts +77 -0
- package/src/server/ports/local-adapters.ts +107 -0
- package/src/server/projections/index.ts +28 -0
- package/src/server/tools/index.ts +89 -0
- package/tbox.module.json +48 -0
- package/tests/cards-render.test.tsx +77 -0
- package/tests/config-merge.test.ts +50 -0
- package/tests/customer-service.test.ts +136 -0
- package/tests/skill.test.ts +48 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +35 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as _mastra_core_tools from '@mastra/core/tools';
|
|
2
|
+
import * as _tbox_cn_app_sdk_server from '@tbox.cn/app-sdk/server';
|
|
3
|
+
import { ServerContext, PreToolDef } from '@tbox.cn/app-sdk/server';
|
|
4
|
+
import * as _tbox_cn_app_contracts from '@tbox.cn/app-contracts';
|
|
5
|
+
import { RequestContext } from '@tbox.cn/app-contracts';
|
|
6
|
+
import * as zod from 'zod';
|
|
7
|
+
import { CustomerServiceQueryPort } from '@tbox.cn/app-contracts-mall';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 客服承接 Port 解析:本地知识库域恒本地装配(与 provider 供给面正交)。
|
|
11
|
+
* - app-config 服务(integrations v5 modules 节内存源)注入时按 install id 取资源
|
|
12
|
+
* (ref 展开已在 resolver 构造期完成——消费端只见直填形状);
|
|
13
|
+
* - 无服务/缺资源键 → 占位等值回退(模块单测与未装配环境不破坏,离线零行为变化)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface CustomerServicePorts {
|
|
17
|
+
customerService: CustomerServiceQueryPort;
|
|
18
|
+
}
|
|
19
|
+
/** mall-directory 服务消费接口(015 S7:模板注册,deployment.malls 单一来源;模块只依赖此结构类型) */
|
|
20
|
+
interface MallDirectoryService {
|
|
21
|
+
contactPhone(mallId: string): string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** mall scope 的 contactPhone(联系客服卡拨打条目数据源;mallId 显式 reqCtx、目录经 ctx 解析) */
|
|
24
|
+
declare function currentContactPhone(reqCtx: RequestContext | undefined, ctx: ServerContext): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* 解析客服承接 Port 集合(运行期调用;register 只注册不解析——铁律)。
|
|
27
|
+
* 恒本地知识库装配;TBOX_API_KEY 未配置 → 空 FAQ(静默降级)。
|
|
28
|
+
*/
|
|
29
|
+
declare function resolveCustomerServicePorts(reqCtx: RequestContext, ctx: ServerContext): CustomerServicePorts;
|
|
30
|
+
|
|
31
|
+
type CustomerServiceToolIds = 'customer-service-faq';
|
|
32
|
+
declare function createCustomerServiceTools(_opts: {
|
|
33
|
+
getCtx: () => ServerContext;
|
|
34
|
+
}): {
|
|
35
|
+
readonly customerServiceFaq: _mastra_core_tools.Tool<{
|
|
36
|
+
query: string;
|
|
37
|
+
topK?: string | undefined;
|
|
38
|
+
}, unknown, unknown, unknown, _mastra_core_tools.ToolExecutionContext<unknown, unknown, unknown>, "customer-service-faq", unknown>;
|
|
39
|
+
};
|
|
40
|
+
type CustomerServiceTools = ReturnType<typeof createCustomerServiceTools>;
|
|
41
|
+
|
|
42
|
+
declare const serverModule: {
|
|
43
|
+
declaration: {
|
|
44
|
+
moduleId: string;
|
|
45
|
+
};
|
|
46
|
+
cards: {
|
|
47
|
+
'customer-service-faq': _tbox_cn_app_contracts.CardMeta<zod.ZodObject<{
|
|
48
|
+
query: zod.ZodString;
|
|
49
|
+
faqs: zod.ZodArray<zod.ZodObject<{
|
|
50
|
+
question: zod.ZodString;
|
|
51
|
+
answer: zod.ZodString;
|
|
52
|
+
category: zod.ZodOptional<zod.ZodString>;
|
|
53
|
+
relevanceScore: zod.ZodNumber;
|
|
54
|
+
referenceUrl: zod.ZodOptional<zod.ZodString>;
|
|
55
|
+
}, "strip", zod.ZodTypeAny, {
|
|
56
|
+
question: string;
|
|
57
|
+
answer: string;
|
|
58
|
+
relevanceScore: number;
|
|
59
|
+
category?: string | undefined;
|
|
60
|
+
referenceUrl?: string | undefined;
|
|
61
|
+
}, {
|
|
62
|
+
question: string;
|
|
63
|
+
answer: string;
|
|
64
|
+
relevanceScore: number;
|
|
65
|
+
category?: string | undefined;
|
|
66
|
+
referenceUrl?: string | undefined;
|
|
67
|
+
}>, "many">;
|
|
68
|
+
contactPhone: zod.ZodOptional<zod.ZodString>;
|
|
69
|
+
noResultsReason: zod.ZodOptional<zod.ZodString>;
|
|
70
|
+
}, "strip", zod.ZodTypeAny, {
|
|
71
|
+
query: string;
|
|
72
|
+
faqs: {
|
|
73
|
+
question: string;
|
|
74
|
+
answer: string;
|
|
75
|
+
relevanceScore: number;
|
|
76
|
+
category?: string | undefined;
|
|
77
|
+
referenceUrl?: string | undefined;
|
|
78
|
+
}[];
|
|
79
|
+
noResultsReason?: string | undefined;
|
|
80
|
+
contactPhone?: string | undefined;
|
|
81
|
+
}, {
|
|
82
|
+
query: string;
|
|
83
|
+
faqs: {
|
|
84
|
+
question: string;
|
|
85
|
+
answer: string;
|
|
86
|
+
relevanceScore: number;
|
|
87
|
+
category?: string | undefined;
|
|
88
|
+
referenceUrl?: string | undefined;
|
|
89
|
+
}[];
|
|
90
|
+
noResultsReason?: string | undefined;
|
|
91
|
+
contactPhone?: string | undefined;
|
|
92
|
+
}>>;
|
|
93
|
+
};
|
|
94
|
+
skills: {
|
|
95
|
+
[k: string]: _tbox_cn_app_sdk_server.LocalSkill;
|
|
96
|
+
};
|
|
97
|
+
preTools: {
|
|
98
|
+
'customer-service-faq': PreToolDef<_mastra_core_tools.Tool<{
|
|
99
|
+
query: string;
|
|
100
|
+
topK?: string | undefined;
|
|
101
|
+
}, unknown, unknown, unknown, _mastra_core_tools.ToolExecutionContext<unknown, unknown, unknown>, "customer-service-faq", unknown>>;
|
|
102
|
+
};
|
|
103
|
+
register(ctx: ServerContext): void;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export { type CustomerServicePorts, type CustomerServiceToolIds, type CustomerServiceTools, type MallDirectoryService, createCustomerServiceTools, currentContactPhone, serverModule as default, resolveCustomerServicePorts, serverModule };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCustomerServiceTools,
|
|
3
|
+
currentContactPhone,
|
|
4
|
+
resolveCustomerServicePorts,
|
|
5
|
+
serverModule,
|
|
6
|
+
server_default
|
|
7
|
+
} from "../chunk-PN4SPFWE.js";
|
|
8
|
+
export {
|
|
9
|
+
createCustomerServiceTools,
|
|
10
|
+
currentContactPhone,
|
|
11
|
+
server_default as default,
|
|
12
|
+
resolveCustomerServicePorts,
|
|
13
|
+
serverModule
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tbox.cn/app-module-customer-service",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "商圈客服承接模块:服务政策 FAQ 知识检索(命中正文作答/无结果联系客服卡)。",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./server": {
|
|
12
|
+
"types": "./dist/server/index.d.ts",
|
|
13
|
+
"import": "./dist/server/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./client": {
|
|
16
|
+
"types": "./dist/client/index.d.ts",
|
|
17
|
+
"import": "./dist/client/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@tbox.cn/app-sdk": "0.17.0",
|
|
23
|
+
"@tbox.cn/app-contracts": "0.9.0",
|
|
24
|
+
"@tbox.cn/app-contracts-mall": "0.9.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"zod": "3.25.76",
|
|
28
|
+
"react": "18.3.1",
|
|
29
|
+
"@phosphor-icons/react": "2.1.10"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/react": "18.3.31",
|
|
33
|
+
"@types/node": "24.13.3",
|
|
34
|
+
"typescript": "5.9.3",
|
|
35
|
+
"vitest": "4.1.10",
|
|
36
|
+
"zod": "3.25.76",
|
|
37
|
+
"tsup": "8.5.1",
|
|
38
|
+
"happy-dom": "20.11.2",
|
|
39
|
+
"react-dom": "18.3.1",
|
|
40
|
+
"@types/react-dom": "18.3.7"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"src",
|
|
45
|
+
"tests",
|
|
46
|
+
"skills",
|
|
47
|
+
"tbox.module.json",
|
|
48
|
+
"tsconfig.json",
|
|
49
|
+
"tsup.config.ts",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"AGENTS.md"
|
|
53
|
+
],
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20.0.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"sideEffects": [
|
|
62
|
+
"**/*.css"
|
|
63
|
+
],
|
|
64
|
+
"scripts": {
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"build": "tsup"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: customer-service
|
|
3
|
+
description: 商圈客服 SOP——服务政策问答、FAQ 转述、联系客服兜底与转人工承接的决策与回答规范。
|
|
4
|
+
routingTerms:
|
|
5
|
+
- 客服
|
|
6
|
+
- 退换货
|
|
7
|
+
- 退货
|
|
8
|
+
- 退款
|
|
9
|
+
- 售后
|
|
10
|
+
- 发票
|
|
11
|
+
- 投诉
|
|
12
|
+
- 服务政策
|
|
13
|
+
- 营业时间
|
|
14
|
+
- 开门
|
|
15
|
+
- 关门
|
|
16
|
+
- 开闭店
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# 商圈客服 SOP
|
|
20
|
+
|
|
21
|
+
## 你是谁
|
|
22
|
+
|
|
23
|
+
你是商场的客服朋友,熟悉服务政策与办事流程。像真人一样理解用户诉求,先给答案再给下一步;政策条目转述要像面对面解释,不像念文件。
|
|
24
|
+
|
|
25
|
+
- **结果先行**:先直接回答问题("7 天内凭小票可退"),再补关键限制条件
|
|
26
|
+
- **重点可扫读**:期限、金额、门槛用 **加粗**,步骤类内容分点列出
|
|
27
|
+
- **不承诺**:转述政策保留原条件与范围,不把"可能""视情况"说成"一定"
|
|
28
|
+
|
|
29
|
+
❌ `根据商场服务政策规定,退换货业务需凭有效凭证至服务台办理,具体以商场公示为准。`
|
|
30
|
+
|
|
31
|
+
✅ `凭小票 **7 天内**可退换,吊牌别拆。去 1F 服务台办就行。`
|
|
32
|
+
|
|
33
|
+
## 工作方式
|
|
34
|
+
|
|
35
|
+
**先查知识库再回答**——客服域的每一轮对话,无论用户问什么(政策/办事/投诉/转人工/基础信息),都必须先调 `customer-service-faq`(`query` 传用户原话)查知识库。有命中时参考知识库条目用自己的话转述(保留条件、范围和时间限制),不编造知识库没有的信息;无召回时如实说明暂无收录,再基于常识给用户合理的引导(如建议到服务台咨询),但不把常识说成商场政策。客服域零出卡,转人工承接走正文话术。
|
|
36
|
+
|
|
37
|
+
## 能力地图
|
|
38
|
+
|
|
39
|
+
| 用户问什么 | 工具 | 出卡 |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| 退换货、积分规则、停车月卡政策、开发票、投诉建议等政策与办事 | `customer-service-faq` | 无卡(命中时正文作答) |
|
|
42
|
+
| 营业时间、商场位置等基础信息 | `customer-service-faq` | 无卡(命中时正文作答) |
|
|
43
|
+
|
|
44
|
+
## 场景走法
|
|
45
|
+
|
|
46
|
+
### 政策问答
|
|
47
|
+
|
|
48
|
+
用户问"退换货怎么办""积分怎么用""商场几点开门""怎么开发票"时:调 `customer-service-faq`,`query` 传用户原话。命中时用自己的话转述——保留条件、范围和时间限制,重点信息 **加粗**,步骤分点。查不到时如实说明暂无收录,建议用户到服务台或联系人工客服确认。
|
|
49
|
+
|
|
50
|
+
注意:政策答的是"规则",不是"实时数字"——停车费要实时报价,走停车域 `queryParkingFee`;会员积分余额走会员域 `queryPoints`;优惠券领取走营销域 `listCoupons`。
|
|
51
|
+
|
|
52
|
+
### 商场基础信息
|
|
53
|
+
|
|
54
|
+
用户问"商场几点开门""几点关门""营业到几点""在哪个位置/怎么走"这类商场基础信息时:调 `customer-service-faq`,`query` 传用户原话。知识库收录了就按条目作答(时间用 **加粗**);没收录就如实说明暂无该信息,建议用户联系人工客服确认——不按常识编造营业时间。
|
|
55
|
+
|
|
56
|
+
### 投诉与情绪
|
|
57
|
+
|
|
58
|
+
用户表达不满(排队久、服务差、东西有问题)时:先接住情绪再给路径;能对上 FAQ 条目就给解法,对不上就在正文说明"帮你转人工更稳妥"并给出联系人工的路径,不敷衍、不与用户争辩。
|
|
59
|
+
|
|
60
|
+
### 转人工
|
|
61
|
+
|
|
62
|
+
用户说"转人工""人工客服""找人工""联系客服"时:调 `customer-service-faq`(`query` 传用户原话"转人工"),查知识库里的人工客服联系方式(电话、服务台位置等)。有结果就在正文告知("人工客服电话是 XXX,也可以到 1F 服务台");知识库无收录就如实说明"暂无人工客服联系方式,建议到 1F 服务台咨询"。**不要直接说"帮您转人工"而不调工具**——用户需要的是联系方式,不是一句"正在转接"。
|
|
63
|
+
|
|
64
|
+
## 卡片是怎么出来的
|
|
65
|
+
|
|
66
|
+
客服域零出卡:FAQ 命中与未命中都不产出卡片候选——答案与未命中说明全部在正文,联系人工也是正文话术(不出任何卡,包括联系客服引导卡)。不要输出 `<agent-ui-cards>` 引用客服域内容;即使本轮候选池里有其他域的卡片,也只按其相关性决定是否引用。
|
|
67
|
+
|
|
68
|
+
## 跨域协作
|
|
69
|
+
|
|
70
|
+
- **停车缴费/实时报价** → 停车域 `queryParkingFee`/`locateVehicle`
|
|
71
|
+
- **会员积分余额/权益** → 会员域 `queryPoints`/`queryMemberInfo`
|
|
72
|
+
- **优惠券领取/活动** → 营销域 `listCoupons`/`searchOffers`
|
|
73
|
+
- **找店/楼层/营业时间** → 导购域 `search-shops`/`mall-directory`
|
|
74
|
+
|
|
75
|
+
## 红线
|
|
76
|
+
|
|
77
|
+
- 结论可回溯:政策条目、期限、金额都能指回本轮 FAQ 返回;FAQ 没有的不按常识编造——说错退换期限,用户白跑一趟
|
|
78
|
+
- 不转述界面操作描述:知识库条目里提到"点击XX按钮""上传照片""在对话里点XX"等界面操作指引时,不得转述——知识库可能包含未上线的产品功能描述,转述会导致用户去找不存在的按钮。只转述事实性信息(电话号码、服务台位置、政策规则),操作类描述忽略
|
|
79
|
+
- 不承诺裁决:投诉处理结果、赔付方案是人工客服的权限,只做承接不做答复
|
|
80
|
+
- 不暴露内部:不念 JSON 字段名、知识库条目编号、原始报错;SOP 是你的内部手册——工具选择理由不进正文
|
|
81
|
+
- **机制词零出现**:正文不说"知识库""FAQ""检索""查询系统"——查到了直接给结果("商场 10 点开门"),过程话术只用用户能懂的("帮您确认过了""帮您看了下"),不说查了什么库、走了什么渠道
|
|
82
|
+
- 工具报错或超时只说暂时查不到、建议稍后重试或转人工,不用历史答案冒充当前政策
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* customer-service-faq 富卡(0.4.0 D6):手风琴(单展开,默认第 0)+ 拨打客服 + referenceUrl 外链。
|
|
3
|
+
* 自 shopping-guide 迁入(2026-08-31 客服模块拆分);视觉类名 sg- → cs-(模块自持样式)。
|
|
4
|
+
*/
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { CaretDown, Phone, ArrowSquareOut } from '@phosphor-icons/react';
|
|
7
|
+
import { sendCardAction } from '@tbox.cn/app-sdk/client';
|
|
8
|
+
import type { CustomerServiceFaqCardData } from '@tbox.cn/app-contracts-mall';
|
|
9
|
+
|
|
10
|
+
export function CustomerServiceFaqCard(props: {
|
|
11
|
+
data: CustomerServiceFaqCardData;
|
|
12
|
+
cardId: string;
|
|
13
|
+
isHistory: boolean;
|
|
14
|
+
}) {
|
|
15
|
+
const { data, cardId } = props;
|
|
16
|
+
const [openIndex, setOpenIndex] = useState<number | null>(
|
|
17
|
+
data.faqs.length > 0 ? 0 : null,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (data.faqs.length === 0) {
|
|
21
|
+
return <section className="cs-card"><div className="cs-empty">{data.noResultsReason ?? '未找到相关常见问题'}</div></section>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<section className="cs-card cs-faq">
|
|
26
|
+
<div className="cs-card-title">相关常见问题</div>
|
|
27
|
+
<ul className="cs-faq-list">
|
|
28
|
+
{data.faqs.map((f, i) => {
|
|
29
|
+
const open = openIndex === i;
|
|
30
|
+
return (
|
|
31
|
+
<li key={i} className="cs-faq-item">
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className="cs-faq-item__q"
|
|
35
|
+
aria-expanded={open}
|
|
36
|
+
onClick={() => setOpenIndex(open ? null : i)}
|
|
37
|
+
>
|
|
38
|
+
<span>{f.question}</span>
|
|
39
|
+
<CaretDown size={16} className="cs-faq-item__chevron" data-rotated={open || undefined} />
|
|
40
|
+
</button>
|
|
41
|
+
{open && (
|
|
42
|
+
<div className="cs-faq-item__a">
|
|
43
|
+
<p>{f.answer}</p>
|
|
44
|
+
{f.referenceUrl && (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
className="cs-link-btn cs-faq-item__ref"
|
|
48
|
+
onClick={() => {
|
|
49
|
+
sendCardAction(cardId, { type: 'openLink', url: f.referenceUrl! });
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
查看来源
|
|
53
|
+
<ArrowSquareOut size={13} />
|
|
54
|
+
</button>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</li>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</ul>
|
|
62
|
+
{data.contactPhone && (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
className="cs-contact-btn"
|
|
66
|
+
onClick={() => {
|
|
67
|
+
sendCardAction(cardId, { type: 'openLink', url: `tel:${data.contactPhone}` });
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<Phone size={16} />
|
|
71
|
+
拨打客服 {data.contactPhone}
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
</section>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default CustomerServiceFaqCard;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CustomerServiceFaqCard, default } from './customer-service-faq';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* module-customer-service 客户端入口(与服务端 serverModule 对称)。
|
|
3
|
+
*/
|
|
4
|
+
import './styles/customer-service.css'; // 模块自持 CSS(副作用 import,首行保证先行加载)
|
|
5
|
+
|
|
6
|
+
import type { CardComponent } from '@tbox.cn/app-contracts';
|
|
7
|
+
import type { ClientContext, ClientModule } from '@tbox.cn/app-sdk/client';
|
|
8
|
+
import { CustomerServiceFaqCard } from './cards';
|
|
9
|
+
|
|
10
|
+
const cards = {
|
|
11
|
+
'customer-service-faq': CustomerServiceFaqCard as CardComponent,
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export const clientModule = {
|
|
15
|
+
cards,
|
|
16
|
+
register(ctx: ClientContext): void {
|
|
17
|
+
ctx.cards.registerMap(cards);
|
|
18
|
+
},
|
|
19
|
+
} satisfies ClientModule;
|
|
20
|
+
|
|
21
|
+
export default clientModule;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* module-customer-service 客户端域样式(自 module-shopping-guide 迁入,
|
|
3
|
+
* 2026-08-31 客服模块拆分;类名 sg- → cs-,模块自持 CSS 零跨模块共享)。
|
|
4
|
+
* 视觉源:mall-vis 导购蓝系延续(FAQ 卡原 sg-faq 视觉 1:1 迁移)。
|
|
5
|
+
* 机械规则:大写 PX 不转;rgba() 传统语法;禁 color-mix / !important;直引 --tbox-* 恒带 fallback;域专属视觉走域变量。
|
|
6
|
+
* ============================================================ */
|
|
7
|
+
|
|
8
|
+
/* ===== 域变量 ===== */
|
|
9
|
+
.cs-card {
|
|
10
|
+
--cs-accent: #096bfa;
|
|
11
|
+
--cs-surface: #f7faff;
|
|
12
|
+
--cs-accent-border: #cfe0f7;
|
|
13
|
+
--cs-text-2: var(--tbox-text-secondary, #888);
|
|
14
|
+
--cs-text-3: var(--tbox-muted, #aaa);
|
|
15
|
+
--cs-border: var(--tbox-border, #ececec);
|
|
16
|
+
--cs-bg-subtle: var(--tbox-bg-subtle, #f7f7f7);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ===== 卡根 ===== */
|
|
20
|
+
.cs-card {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
padding: 40px;
|
|
23
|
+
color: var(--tbox-text-primary, #202124);
|
|
24
|
+
font: inherit;
|
|
25
|
+
}
|
|
26
|
+
.cs-card *,
|
|
27
|
+
.cs-card *::before,
|
|
28
|
+
.cs-card *::after {
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
}
|
|
31
|
+
.cs-card button {
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
}
|
|
34
|
+
.cs-card button:disabled {
|
|
35
|
+
cursor: not-allowed;
|
|
36
|
+
opacity: 0.5;
|
|
37
|
+
}
|
|
38
|
+
.cs-card ul,
|
|
39
|
+
.cs-card ol,
|
|
40
|
+
.cs-card dl,
|
|
41
|
+
.cs-card dd,
|
|
42
|
+
.cs-card dt,
|
|
43
|
+
.cs-card p {
|
|
44
|
+
margin: 0;
|
|
45
|
+
padding: 0;
|
|
46
|
+
list-style: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ===== 布局 ===== */
|
|
50
|
+
.cs-card-title {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 16px;
|
|
54
|
+
font-size: 34px;
|
|
55
|
+
font-weight: 700;
|
|
56
|
+
line-height: 48px;
|
|
57
|
+
margin-bottom: 16px;
|
|
58
|
+
color: var(--tbox-text-primary, #202124);
|
|
59
|
+
}
|
|
60
|
+
.cs-card-title::before {
|
|
61
|
+
content: '';
|
|
62
|
+
flex: 0 0 8px;
|
|
63
|
+
width: 8px;
|
|
64
|
+
height: 32px;
|
|
65
|
+
border-radius: 4px;
|
|
66
|
+
background: var(--cs-accent);
|
|
67
|
+
}
|
|
68
|
+
.cs-empty {
|
|
69
|
+
padding: 48px 0;
|
|
70
|
+
margin-top: 16px;
|
|
71
|
+
font-size: 22px;
|
|
72
|
+
color: var(--cs-text-3);
|
|
73
|
+
}
|
|
74
|
+
.cs-link-btn {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
padding: 0;
|
|
79
|
+
border: 0;
|
|
80
|
+
background: transparent;
|
|
81
|
+
color: var(--cs-accent);
|
|
82
|
+
font-size: 26px;
|
|
83
|
+
text-decoration: underline;
|
|
84
|
+
text-underline-offset: 4px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ===== customer-service-faq ===== */
|
|
88
|
+
.cs-faq-item {
|
|
89
|
+
border: 1px solid var(--cs-border);
|
|
90
|
+
border-radius: 20px;
|
|
91
|
+
margin-top: 16px;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
}
|
|
94
|
+
.cs-faq-item__q {
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: space-between;
|
|
98
|
+
gap: 16px;
|
|
99
|
+
width: 100%;
|
|
100
|
+
padding: 24px;
|
|
101
|
+
border: 0;
|
|
102
|
+
background: var(--tbox-bg-card, #fff);
|
|
103
|
+
color: var(--tbox-text-primary, #202124);
|
|
104
|
+
font-size: 28px;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
text-align: left;
|
|
107
|
+
}
|
|
108
|
+
.cs-faq-item__chevron[data-rotated] {
|
|
109
|
+
transform: rotate(180deg);
|
|
110
|
+
transition: transform 0.15s ease;
|
|
111
|
+
}
|
|
112
|
+
.cs-faq-item__a {
|
|
113
|
+
padding: 0 24px 24px;
|
|
114
|
+
border-top: 1px solid var(--cs-border);
|
|
115
|
+
background: var(--cs-bg-subtle);
|
|
116
|
+
}
|
|
117
|
+
.cs-faq-item__a p {
|
|
118
|
+
padding: 20px 0 8px;
|
|
119
|
+
font-size: 26px;
|
|
120
|
+
line-height: 40px;
|
|
121
|
+
color: var(--tbox-text-primary, #202124);
|
|
122
|
+
}
|
|
123
|
+
.cs-faq-item__ref {
|
|
124
|
+
margin-top: 8px;
|
|
125
|
+
font-size: 24px;
|
|
126
|
+
}
|
|
127
|
+
.cs-contact-btn {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
gap: 12px;
|
|
132
|
+
width: 100%;
|
|
133
|
+
margin-top: 24px;
|
|
134
|
+
height: 80px;
|
|
135
|
+
border-radius: 18px;
|
|
136
|
+
border: 2px solid var(--cs-accent-border);
|
|
137
|
+
color: var(--cs-accent);
|
|
138
|
+
background: var(--cs-surface);
|
|
139
|
+
font-size: 28px;
|
|
140
|
+
font-weight: 500;
|
|
141
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './server/index';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CardMeta } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { customerServiceFaqCardDataSchema } from '@tbox.cn/app-contracts-mall';
|
|
3
|
+
|
|
4
|
+
const meta: CardMeta<typeof customerServiceFaqCardDataSchema> = {
|
|
5
|
+
cardType: 'customer-service-faq',
|
|
6
|
+
dataSchema: customerServiceFaqCardDataSchema,
|
|
7
|
+
displayName: '客服FAQ',
|
|
8
|
+
description: '常见问题答案(退换货/积分/停车政策)',
|
|
9
|
+
sampleData: {
|
|
10
|
+
query: '如何退换货',
|
|
11
|
+
faqs: [{ question: '如何退换货', answer: '凭小票 7 天内可退换。', relevanceScore: 0.9 }],
|
|
12
|
+
},
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* module-customer-service 服务端入口(011 K1:自注册闭环)。
|
|
3
|
+
* - 1 卡声明(key = cardType)+ 1 工具 + FAQ silent preTool;
|
|
4
|
+
* - register 只注册不解析(resolveCustomerServicePorts 仅 execute 内调用);
|
|
5
|
+
* - 模块级单例:工具实例顶层创建(010 实例一致性),register 幂等。
|
|
6
|
+
* 2026-08-31 客服模块拆分:自 module-shopping-guide 迁入(FAQ 工具/卡/preTool/本地知识库域)。
|
|
7
|
+
*/
|
|
8
|
+
import type {
|
|
9
|
+
ServerContext,
|
|
10
|
+
ServerModule,
|
|
11
|
+
PreToolDef,
|
|
12
|
+
} from '@tbox.cn/app-sdk/server';
|
|
13
|
+
import { definePreTool, loadModuleSkills } from '@tbox.cn/app-sdk/server';
|
|
14
|
+
import customerServiceFaqMeta from './cards/customer-service-faq/meta';
|
|
15
|
+
import { createCustomerServiceTools } from './tools';
|
|
16
|
+
|
|
17
|
+
export { resolveCustomerServicePorts, currentContactPhone, type CustomerServicePorts, type MallDirectoryService } from './ports';
|
|
18
|
+
export { createCustomerServiceTools } from './tools';
|
|
19
|
+
export type { CustomerServiceTools, CustomerServiceToolIds } from './tools';
|
|
20
|
+
|
|
21
|
+
/** 模块级 ctx 引用(register 期绑定;execute 内读取——平台包实例分裂免疫:不 import SDK 全局单例) */
|
|
22
|
+
let moduleCtx: ServerContext | undefined;
|
|
23
|
+
|
|
24
|
+
/** 工具实例顶层创建(010:pre-tool 声明引用工具实例 = 单一真源) */
|
|
25
|
+
const tools = createCustomerServiceTools({ getCtx: () => moduleCtx! });
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* FAQ 静默前置工具(011 D2/D9):客服 FAQ 检索 = silent preTool。
|
|
29
|
+
* - silent:不发前端事件、不落 inputs.preTools,仅本轮 reminder 数据块注入;
|
|
30
|
+
* - routing always(2026-09-03 v3):每轮进 decide——词表路由的召回面追不上语言("电影院能带外食吗"
|
|
31
|
+
* 类 FAQ 价值问句词面永远缺),FAQ 检索轻量+空返无害,宁多勿漏;
|
|
32
|
+
* - decide 三层:政策语义优先 run("停车缴费规则"含"缴费"但语义是规则问)→ 明确操作/实时面 skip
|
|
33
|
+
* (业务组件确定性承接,FAQ 不补充、不干扰业务执行)→ 默认 run(宁多勿漏;L2 平台段置信度
|
|
34
|
+
* 分层句保证检索背景不越位实时数据)。
|
|
35
|
+
*/
|
|
36
|
+
/** 操作/实时面信号(明确业务组件承接——skip 防干扰);含门店搜索面(有哪些店) */
|
|
37
|
+
const OPERATIONAL_TERMS = [
|
|
38
|
+
'缴费',
|
|
39
|
+
'缴纳',
|
|
40
|
+
'支付',
|
|
41
|
+
'付款',
|
|
42
|
+
'扣款',
|
|
43
|
+
'查费',
|
|
44
|
+
'费用多少',
|
|
45
|
+
'欠费',
|
|
46
|
+
'我的',
|
|
47
|
+
'余额',
|
|
48
|
+
'进度',
|
|
49
|
+
'有哪些店',
|
|
50
|
+
'有什么店',
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
/** 政策型意图信号(FAQ 价值区)——优先于操作型判定:
|
|
54
|
+
* "停车缴费规则"含"缴费"子串但语义是政策问句(用户裁定:规则类加载参考),双层序保证 run。 */
|
|
55
|
+
const POLICY_TERMS = [
|
|
56
|
+
'规则',
|
|
57
|
+
'政策',
|
|
58
|
+
'标准',
|
|
59
|
+
'怎么算',
|
|
60
|
+
'如何计算',
|
|
61
|
+
'怎么收费',
|
|
62
|
+
'如何收费',
|
|
63
|
+
'免费',
|
|
64
|
+
'封顶',
|
|
65
|
+
'每小时',
|
|
66
|
+
'流程',
|
|
67
|
+
'材料',
|
|
68
|
+
'条件',
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
const faqPreTool = definePreTool(tools.customerServiceFaq, {
|
|
72
|
+
routing: { mode: 'always' },
|
|
73
|
+
decide: ({ text }) => {
|
|
74
|
+
const t = text?.trim() ?? '';
|
|
75
|
+
if (!t) return { decision: 'skip', reason: '空文本' };
|
|
76
|
+
if (POLICY_TERMS.some((w) => t.includes(w))) {
|
|
77
|
+
return { decision: 'run', args: { query: text, topK: '3' } };
|
|
78
|
+
}
|
|
79
|
+
if (OPERATIONAL_TERMS.some((w) => t.includes(w))) {
|
|
80
|
+
return { decision: 'skip', reason: '操作型诉求——业务工具实时数据面,FAQ 不预执行' };
|
|
81
|
+
}
|
|
82
|
+
// 默认 run(宁多勿漏):FAQ 检索轻量+空返无害;L2 分层句保证背景不越位实时数据
|
|
83
|
+
return { decision: 'run', args: { query: text, topK: '3' } };
|
|
84
|
+
},
|
|
85
|
+
silent: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const preTools = {
|
|
89
|
+
'customer-service-faq': faqPreTool,
|
|
90
|
+
} satisfies Record<string, PreToolDef>;
|
|
91
|
+
|
|
92
|
+
/** 本地 skill(009 文件化:包内 skills/customer-service/;map 化回填声明式通道) */
|
|
93
|
+
const skills = Object.fromEntries(
|
|
94
|
+
(await loadModuleSkills('@tbox.cn/app-module-customer-service', import.meta.url)).map((s) => [s.name, s]),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export const serverModule = {
|
|
98
|
+
declaration: {
|
|
99
|
+
moduleId: 'customer-service',
|
|
100
|
+
},
|
|
101
|
+
cards: {
|
|
102
|
+
'customer-service-faq': customerServiceFaqMeta,
|
|
103
|
+
},
|
|
104
|
+
skills,
|
|
105
|
+
preTools,
|
|
106
|
+
register(ctx: ServerContext): void {
|
|
107
|
+
moduleCtx = ctx;
|
|
108
|
+
ctx.cards.registerMap(serverModule.cards);
|
|
109
|
+
for (const tool of Object.values(tools)) {
|
|
110
|
+
ctx.tools.register(tool);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
} satisfies ServerModule;
|
|
114
|
+
|
|
115
|
+
export default serverModule;
|