@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
package/AGENTS.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# module-customer-service —— 集成指引(AI 助手用)
|
|
2
|
+
|
|
3
|
+
本模块为商圈业务模块,声明消费以下服务槽(Port 接口定义见 `src/server/ports/`):
|
|
4
|
+
|
|
5
|
+
- 服务槽清单:见 `tbox.module.json` 的 `contributes.services`(demand 侧,含 title/description meta)。
|
|
6
|
+
|
|
7
|
+
## 自定义 API 对接(AI 集成)
|
|
8
|
+
|
|
9
|
+
用户为本模块服务槽配置「自定义 API」后,adapter 实施落点 = `packages/provider-custom/`
|
|
10
|
+
(客服:客服会话能力)。实施步骤与完成判据见 `packages/provider-custom/AGENTS.md`;
|
|
11
|
+
本模块 Port 接口为 adapter 的实现契约(输入 `ServiceAdapterInput`:config/instanceId/credentialRef)。
|
|
12
|
+
|
|
13
|
+
## 真实厂商接入
|
|
14
|
+
|
|
15
|
+
官方 provider 包形态(manifest 槽声明 + 独立包发布)参照 `modules/provider-mock` 结构样例与
|
|
16
|
+
docs/reference/providers.md;厂商中立措辞纪律见该参考(本文件受 vendor-neutrality 门禁扫描)。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tbox.cn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# module-customer-service · 客服承接业务组件
|
|
2
|
+
|
|
3
|
+
商圈客服域承接组件(`@tbox.cn/app-module-customer-service`,componentId
|
|
4
|
+
`customer-service`)。2026-08-31 自 module-shopping-guide 拆分迁入(FAQ 工具/卡/
|
|
5
|
+
preTool/本地知识库域)。极简形态:1 工具 + 1 卡 + 1 silent preTool + 常本地知识库
|
|
6
|
+
装配;**客服域零出卡**(2026-09-02 联系客服引导卡下线,转人工由正文话术承接)。
|
|
7
|
+
|
|
8
|
+
## 业务职责
|
|
9
|
+
|
|
10
|
+
| 能力域 | 承接 |
|
|
11
|
+
|---|---|
|
|
12
|
+
| FAQ 检索 | 商场常见问题知识(服务政策/会员积分/停车/设施/办事指引)|
|
|
13
|
+
| 知识检索 | SDK KnowledgeClient 检索,恒本地装配(与 provider 远程检索面正交)|
|
|
14
|
+
| 联系通路 | contactPhone 数据面(mall-directory 服务注入)|
|
|
15
|
+
|
|
16
|
+
## 架构分层
|
|
17
|
+
|
|
18
|
+
src/
|
|
19
|
+
server/
|
|
20
|
+
index.ts 模块注册(1 卡 + 1 工具 + faqPreTool;无 handler)
|
|
21
|
+
tools/index.ts customerServiceFaq
|
|
22
|
+
ports/ resolveCustomerServicePorts + local-adapters + currentContactPhone
|
|
23
|
+
projections/ faqSummary / projectCustomerServiceFaq
|
|
24
|
+
cards/customer-service-faq/meta.ts
|
|
25
|
+
client/ 卡组件(存量;工具零出卡后仅作直连渲染面保留)
|
|
26
|
+
|
|
27
|
+
## 领域模型(契约真源)
|
|
28
|
+
|
|
29
|
+
- contracts-mall customer-service 分节:CustomerServiceQueryPort(searchFaq /
|
|
30
|
+
searchKnowledge)、FaqEntry、KnowledgeEntry、customerServiceFaqCardDataSchema。
|
|
31
|
+
- FAQ 卡数据形状(query/faqs[]/contactPhone?/noResultsReason?)仍由 projection 组装,
|
|
32
|
+
但仅作 summary 载体,不再出卡。
|
|
33
|
+
|
|
34
|
+
## 工具清单(tools/index.ts)
|
|
35
|
+
|
|
36
|
+
| 工具 | oneLiner | carding | 门禁 |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| customerServiceFaq(tools/index.ts:56)| FAQ 检索:命中返回知识条目作答素材;未命中返回 summary 如实说明 | **零出卡**(不挂 withCardEmissionHint,description 不预告出卡——tools/index.ts:82-85 注释铁律)| 无(FAQ 域无主体语义,不查 mall,用 mallScopeOrEmpty 宽松上下文)|
|
|
39
|
+
|
|
40
|
+
入参面:query(string)+ topK(string 1-5,缺省 3;D11 字符串化 schema——仅
|
|
41
|
+
string/map<string,string>);入参合法才 clamp,缺省由 adapter cfg.topK ?? 3 兜底。
|
|
42
|
+
|
|
43
|
+
**零出卡的 Shapes**(tools/index.ts:75-78 注释):FAQ 召回结果 JSON 单行进 summary
|
|
44
|
+
供模型流式回复;noResultsReason 仍进 summary 供模型转述;contactPhone 保留为数据面。
|
|
45
|
+
|
|
46
|
+
## silent preTool(index.ts:71-90)
|
|
47
|
+
|
|
48
|
+
faqPreTool = definePreTool(customerServiceFaq, mode always, silent):
|
|
49
|
+
- 2026-09-03 v3 裁决:routing always(词表召回面追不上语言——「电影院能带外食吗」类
|
|
50
|
+
FAQ 价值问句词面永远缺;FAQ 检索轻量 + 空返无害,宁多勿漏)。
|
|
51
|
+
- decide 三层:POLICY_TERMS(规则/政策/怎么收费/封顶/每小时/流程/材料…)优先 run;
|
|
52
|
+
OPERATIONAL_TERMS(缴费/支付/余额/有哪些店…)skip——操作/实时面由业务组件确定性
|
|
53
|
+
承接,FAQ 不干扰业务执行;默认 run(宁多勿漏)。
|
|
54
|
+
- 政策词优先的裁决样本:「停车缴费规则」含「缴费」子串但语义是规则问(用户裁定)。
|
|
55
|
+
|
|
56
|
+
## Port 与配置(ports/)
|
|
57
|
+
|
|
58
|
+
- 恒本地知识库装配:KnowledgeSearchAdapter implements CustomerServiceQueryPort
|
|
59
|
+
(ports/local-adapters.ts:22-68)——SDK knowledge.search(datasetId, query, topK,
|
|
60
|
+
scoreThreshold?);FAQ 文档 {question,answer} JSON 解析,非 JSON 走 320 字符摘要;
|
|
61
|
+
knowledge 资源同构。构造失败/未配置 TBOX_API_KEY → EmptyCustomerServiceAdapter
|
|
62
|
+
空降级(ports/index.ts:74-80)。
|
|
63
|
+
- 配置链(ports/index.ts:47-65):app-config 服务按资源键取 per-mall 配置;
|
|
64
|
+
**资源级合并**——客服侧 components/customer-service.json 覆盖、导购侧遗留
|
|
65
|
+
components/shopping-guide.json 同资源键做底(拆分过渡期兼容:真实 ref/datasetId
|
|
66
|
+
在导购侧遗留配置中不被客服占位遮蔽);无服务/缺键 → 占位等值回退(datasetId
|
|
67
|
+
'faq' / 'mall-knowledge',召回必然为空 + warn 日志)。
|
|
68
|
+
- 资源键 = `customer-service-faq-kb` / `knowledge-assistance-kb`(manifest contributes.resources
|
|
69
|
+
声明;`ref` 经同目录 `knowledge.json` 台账展开,datasetId 直填免台账);无服务/缺键时占位
|
|
70
|
+
datasetId 等值回退(召回必然为空,离线零行为变化)。
|
|
71
|
+
- contactPhone:currentContactPhone(ports/index.ts:32-36)= mall scope mallId →
|
|
72
|
+
ctx.services 'mall-directory' 服务目录(模板注册,deployment.malls 单一来源)。
|
|
73
|
+
|
|
74
|
+
## 卡片清单(1 张)
|
|
75
|
+
|
|
76
|
+
customer-service-faq(meta.ts;schemaVersion 1;工具零出卡,meta 注册保留作
|
|
77
|
+
schema 真源与样例数据锚)。
|
|
78
|
+
|
|
79
|
+
## 跨模块协作
|
|
80
|
+
|
|
81
|
+
- ← shopping-guide:FAQ 域迁入来源;其遗留配置做资源级合并的底。
|
|
82
|
+
- ← member:无门禁(FAQ 公开面);member 知识条目由知识库内容承载。
|
|
83
|
+
|
|
84
|
+
## 铁律摘要
|
|
85
|
+
|
|
86
|
+
- register 只注册不解析(resolveCustomerServicePorts 仅 execute 内调用)。
|
|
87
|
+
- fail-visible 统一包装:execute 异常返回 { found:false, message:'暂时无法查询,请稍后再试' }
|
|
88
|
+
(tools/index.ts:44-53 统一)。
|
|
89
|
+
- 工具契约与预告知一致:description 不预告出卡,工具不产 cards 字段。
|
|
90
|
+
|
|
91
|
+
## 已知限制与文档漂移
|
|
92
|
+
|
|
93
|
+
- contracts-mall 的 customerServiceFaqCardDataSchema 仍在 shopping-guide 分节(:330),
|
|
94
|
+
随拆分整理时迁驻或标注 deprecated。
|
|
95
|
+
- knowledge client 每次调用构造(无热路径缓存诉求——FAQ 检索频次低)。
|
|
96
|
+
|
|
97
|
+
## 用法
|
|
98
|
+
|
|
99
|
+
在生成应用中:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
tbox-app module add @tbox.cn/app-module-customer-service
|
|
103
|
+
```
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// src/server/index.ts
|
|
2
|
+
import { definePreTool, loadModuleSkills } from "@tbox.cn/app-sdk/server";
|
|
3
|
+
|
|
4
|
+
// src/server/cards/customer-service-faq/meta.ts
|
|
5
|
+
import { customerServiceFaqCardDataSchema } from "@tbox.cn/app-contracts-mall";
|
|
6
|
+
var meta = {
|
|
7
|
+
cardType: "customer-service-faq",
|
|
8
|
+
dataSchema: customerServiceFaqCardDataSchema,
|
|
9
|
+
displayName: "\u5BA2\u670DFAQ",
|
|
10
|
+
description: "\u5E38\u89C1\u95EE\u9898\u7B54\u6848\uFF08\u9000\u6362\u8D27/\u79EF\u5206/\u505C\u8F66\u653F\u7B56\uFF09",
|
|
11
|
+
sampleData: {
|
|
12
|
+
query: "\u5982\u4F55\u9000\u6362\u8D27",
|
|
13
|
+
faqs: [{ question: "\u5982\u4F55\u9000\u6362\u8D27", answer: "\u51ED\u5C0F\u7968 7 \u5929\u5185\u53EF\u9000\u6362\u3002", relevanceScore: 0.9 }]
|
|
14
|
+
},
|
|
15
|
+
schemaVersion: 1
|
|
16
|
+
};
|
|
17
|
+
var meta_default = meta;
|
|
18
|
+
|
|
19
|
+
// src/server/tools/index.ts
|
|
20
|
+
import { createTool, requireToolRequestContext } from "@tbox.cn/app-sdk/server";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { mallScopeOrEmpty } from "@tbox.cn/app-contracts-mall";
|
|
23
|
+
|
|
24
|
+
// src/server/ports/index.ts
|
|
25
|
+
import { parseMallScope } from "@tbox.cn/app-contracts-mall";
|
|
26
|
+
import { createKnowledgeClient, KNOWLEDGE_PLACEHOLDER_DATASET_IDS } from "@tbox.cn/app-sdk/server";
|
|
27
|
+
|
|
28
|
+
// src/server/ports/local-adapters.ts
|
|
29
|
+
var KnowledgeSearchAdapter = class {
|
|
30
|
+
constructor(knowledge, cfg) {
|
|
31
|
+
this.knowledge = knowledge;
|
|
32
|
+
this.cfg = cfg;
|
|
33
|
+
}
|
|
34
|
+
knowledge;
|
|
35
|
+
cfg;
|
|
36
|
+
async searchFaq(_ctx, query, topK) {
|
|
37
|
+
const results = await this.knowledge.search({
|
|
38
|
+
// cfg 来源(resolver 展开直填 / 模块占位常量)恒有 datasetId(ResolvedResource 必填字段)
|
|
39
|
+
datasetId: this.cfg.faq.datasetId,
|
|
40
|
+
query,
|
|
41
|
+
topK: topK ?? this.cfg.faq.topK ?? 3,
|
|
42
|
+
...this.cfg.faq.scoreThreshold !== void 0 ? { scoreThreshold: this.cfg.faq.scoreThreshold } : {}
|
|
43
|
+
});
|
|
44
|
+
return results.map((hit) => {
|
|
45
|
+
const parsed = tryParseJsonFaqDoc(hit.content);
|
|
46
|
+
return {
|
|
47
|
+
question: parsed?.question ?? query,
|
|
48
|
+
answer: parsed?.answer ?? summarize(hit.content),
|
|
49
|
+
...hit.originFileName ? { category: hit.originFileName } : {},
|
|
50
|
+
relevanceScore: hit.score
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async searchKnowledge(_ctx, query, topK) {
|
|
55
|
+
const results = await this.knowledge.search({
|
|
56
|
+
datasetId: this.cfg.knowledge.datasetId,
|
|
57
|
+
query,
|
|
58
|
+
topK: topK ?? this.cfg.knowledge.topK ?? 3,
|
|
59
|
+
...this.cfg.knowledge.scoreThreshold !== void 0 ? { scoreThreshold: this.cfg.knowledge.scoreThreshold } : {}
|
|
60
|
+
});
|
|
61
|
+
return results.map((hit) => ({
|
|
62
|
+
content: hit.content,
|
|
63
|
+
score: hit.score,
|
|
64
|
+
...hit.originFileName ? { title: hit.originFileName } : {}
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var EmptyCustomerServiceAdapter = class {
|
|
69
|
+
async searchFaq() {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
async searchKnowledge() {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
function tryParseJsonFaqDoc(content) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(content);
|
|
79
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.answer === "string") {
|
|
80
|
+
const doc = parsed;
|
|
81
|
+
return {
|
|
82
|
+
question: typeof doc.question === "string" ? doc.question : "",
|
|
83
|
+
answer: doc.answer
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
function summarize(content) {
|
|
91
|
+
return content.replace(/^#+\s+/gm, "").replace(/\s+/g, " ").trim().slice(0, 320);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/server/ports/index.ts
|
|
95
|
+
function resolveMallDirectory(ctx) {
|
|
96
|
+
return ctx.services.resolve?.("mall-directory");
|
|
97
|
+
}
|
|
98
|
+
function currentContactPhone(reqCtx, ctx) {
|
|
99
|
+
const mallId = parseMallScope(reqCtx?.attributes)?.mallId;
|
|
100
|
+
if (!mallId) return void 0;
|
|
101
|
+
return resolveMallDirectory(ctx)?.contactPhone(mallId);
|
|
102
|
+
}
|
|
103
|
+
var MODULE_ID = "module-customer-service";
|
|
104
|
+
var DEFAULT_FAQ_RESOURCE = { type: "knowledge", datasetId: KNOWLEDGE_PLACEHOLDER_DATASET_IDS[0] };
|
|
105
|
+
var DEFAULT_KB_RESOURCE = { type: "knowledge", datasetId: KNOWLEDGE_PLACEHOLDER_DATASET_IDS[1] };
|
|
106
|
+
function resolveAssistanceConfig(ctx, mallId) {
|
|
107
|
+
const resolver = ctx.services.resolve?.("app-config");
|
|
108
|
+
const resources = resolver ? resolver(MODULE_ID) : {};
|
|
109
|
+
if (!resolver) {
|
|
110
|
+
console.warn(`[customer-service] app-config \u670D\u52A1\u672A\u6CE8\u5165 mallId=${mallId ?? "none"}\uFF0C\u77E5\u8BC6\u5E93\u4F7F\u7528\u5360\u4F4D datasetId\uFF08\u53EC\u56DE\u5FC5\u7136\u4E3A\u7A7A\uFF09`);
|
|
111
|
+
} else if (!resources["customer-service-faq-kb"]) {
|
|
112
|
+
console.warn(`[customer-service] app-config \u65E0 customer-service-faq-kb \u8D44\u6E90 mallId=${mallId ?? "none"}\uFF0C\u77E5\u8BC6\u5E93\u4F7F\u7528\u5360\u4F4D datasetId`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
faq: resources["customer-service-faq-kb"] ?? DEFAULT_FAQ_RESOURCE,
|
|
116
|
+
knowledge: resources["knowledge-assistance-kb"] ?? DEFAULT_KB_RESOURCE
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function resolveCustomerServicePorts(reqCtx, ctx) {
|
|
120
|
+
const mallId = parseMallScope(reqCtx?.attributes)?.mallId;
|
|
121
|
+
const cfg = resolveAssistanceConfig(ctx, mallId);
|
|
122
|
+
try {
|
|
123
|
+
return { customerService: new KnowledgeSearchAdapter(createKnowledgeClient(), cfg) };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
ctx.logger?.debug(`knowledge \u672A\u914D\u7F6E\u6216\u6784\u9020\u5931\u8D25\uFF0CFAQ \u7A7A\u964D\u7EA7\uFF1Aerr=${e.message}`);
|
|
126
|
+
return { customerService: new EmptyCustomerServiceAdapter() };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/server/projections/index.ts
|
|
131
|
+
function faqSummary(data) {
|
|
132
|
+
return JSON.stringify(data);
|
|
133
|
+
}
|
|
134
|
+
function projectCustomerServiceFaq(query, faqs, noResultsReason, contactPhone) {
|
|
135
|
+
return {
|
|
136
|
+
query,
|
|
137
|
+
faqs: faqs.map((f) => ({
|
|
138
|
+
question: f.question,
|
|
139
|
+
answer: f.answer,
|
|
140
|
+
...f.category ? { category: f.category } : {},
|
|
141
|
+
relevanceScore: f.relevanceScore
|
|
142
|
+
})),
|
|
143
|
+
...contactPhone ? { contactPhone } : {},
|
|
144
|
+
...noResultsReason ? { noResultsReason } : {}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/server/tools/index.ts
|
|
149
|
+
var str = (desc) => z.string().describe(desc);
|
|
150
|
+
var optionalStr = (desc) => z.string().optional().describe(desc);
|
|
151
|
+
function reqCtxOf(execCtx) {
|
|
152
|
+
return requireToolRequestContext(execCtx);
|
|
153
|
+
}
|
|
154
|
+
function toLooseContext(reqCtx) {
|
|
155
|
+
return {
|
|
156
|
+
scope: mallScopeOrEmpty(reqCtx),
|
|
157
|
+
requestContext: reqCtx
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function createCustomerServiceTools(_opts) {
|
|
161
|
+
const exec = (fn) => {
|
|
162
|
+
return async (args, execCtx) => {
|
|
163
|
+
const ctx = _opts.getCtx();
|
|
164
|
+
try {
|
|
165
|
+
return await fn(resolveCustomerServicePorts(reqCtxOf(execCtx), ctx), ctx, args, execCtx);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
168
|
+
console.warn(`[customer-service] tool exec error: ${message}`);
|
|
169
|
+
return { found: false, message: "\u6682\u65F6\u65E0\u6CD5\u67E5\u8BE2\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5" };
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
const customerServiceFaq = createTool({
|
|
174
|
+
id: "customer-service-faq",
|
|
175
|
+
description: "\u5BA2\u670DFAQ\uFF1A\u641C\u7D22\u5546\u573A\u5E38\u89C1\u95EE\u9898\u77E5\u8BC6\uFF08\u670D\u52A1\u653F\u7B56\u3001\u4F1A\u5458\u79EF\u5206\u3001\u505C\u8F66\u3001\u8BBE\u65BD\u3001\u529E\u4E8B\u6307\u5F15\u7B49\uFF09\u3002\u547D\u4E2D\u8FD4\u56DE\u77E5\u8BC6\u6761\u76EE\uFF08\u4F5C\u7B54\u7D20\u6750\u2014\u2014\u636E\u6B64\u76F4\u63A5\u4F5C\u7B54\uFF0C\u91CD\u70B9\u4FE1\u606F\u7528 **\u52A0\u7C97**\uFF0C\u6B65\u9AA4\u7C7B\u5185\u5BB9\u5206\u70B9\u5217\u51FA\uFF09\uFF1B\u65E0\u7ED3\u679C\u8FD4\u56DE summary \u672A\u547D\u4E2D\u8BF4\u660E\uFF08\u6B63\u6587\u5982\u5B9E\u544A\u77E5\uFF0C\u4E0D\u51FA\u5361\uFF09\u3002",
|
|
176
|
+
inputSchema: z.object({
|
|
177
|
+
query: str("\u7528\u6237\u95EE\u9898"),
|
|
178
|
+
topK: optionalStr("\u8FD4\u56DE\u6761\u6570\uFF08\u5B57\u7B26\u4E32\uFF0C1-5\uFF0C\u7F3A\u7701 3\uFF09")
|
|
179
|
+
}),
|
|
180
|
+
execute: exec(async (ports, _ctx, args, execCtx) => {
|
|
181
|
+
const n = Number(args.topK);
|
|
182
|
+
const topK = Number.isFinite(n) && n >= 1 ? Math.min(5, n) : void 0;
|
|
183
|
+
const faqs = await ports.customerService.searchFaq(toLooseContext(reqCtxOf(execCtx)), String(args.query), topK);
|
|
184
|
+
const data = projectCustomerServiceFaq(
|
|
185
|
+
String(args.query),
|
|
186
|
+
faqs,
|
|
187
|
+
faqs.length === 0 ? "\u672A\u627E\u5230\u76F8\u5173\u5E38\u89C1\u95EE\u9898\uFF0C\u8BF7\u5C1D\u8BD5\u4EBA\u5DE5\u5BA2\u670D\u3002" : void 0,
|
|
188
|
+
currentContactPhone(reqCtxOf(execCtx), _opts.getCtx())
|
|
189
|
+
);
|
|
190
|
+
return { summary: faqSummary(data) };
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
customerServiceFaq
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/server/index.ts
|
|
199
|
+
var moduleCtx;
|
|
200
|
+
var tools = createCustomerServiceTools({ getCtx: () => moduleCtx });
|
|
201
|
+
var OPERATIONAL_TERMS = [
|
|
202
|
+
"\u7F34\u8D39",
|
|
203
|
+
"\u7F34\u7EB3",
|
|
204
|
+
"\u652F\u4ED8",
|
|
205
|
+
"\u4ED8\u6B3E",
|
|
206
|
+
"\u6263\u6B3E",
|
|
207
|
+
"\u67E5\u8D39",
|
|
208
|
+
"\u8D39\u7528\u591A\u5C11",
|
|
209
|
+
"\u6B20\u8D39",
|
|
210
|
+
"\u6211\u7684",
|
|
211
|
+
"\u4F59\u989D",
|
|
212
|
+
"\u8FDB\u5EA6",
|
|
213
|
+
"\u6709\u54EA\u4E9B\u5E97",
|
|
214
|
+
"\u6709\u4EC0\u4E48\u5E97"
|
|
215
|
+
];
|
|
216
|
+
var POLICY_TERMS = [
|
|
217
|
+
"\u89C4\u5219",
|
|
218
|
+
"\u653F\u7B56",
|
|
219
|
+
"\u6807\u51C6",
|
|
220
|
+
"\u600E\u4E48\u7B97",
|
|
221
|
+
"\u5982\u4F55\u8BA1\u7B97",
|
|
222
|
+
"\u600E\u4E48\u6536\u8D39",
|
|
223
|
+
"\u5982\u4F55\u6536\u8D39",
|
|
224
|
+
"\u514D\u8D39",
|
|
225
|
+
"\u5C01\u9876",
|
|
226
|
+
"\u6BCF\u5C0F\u65F6",
|
|
227
|
+
"\u6D41\u7A0B",
|
|
228
|
+
"\u6750\u6599",
|
|
229
|
+
"\u6761\u4EF6"
|
|
230
|
+
];
|
|
231
|
+
var faqPreTool = definePreTool(tools.customerServiceFaq, {
|
|
232
|
+
routing: { mode: "always" },
|
|
233
|
+
decide: ({ text }) => {
|
|
234
|
+
const t = text?.trim() ?? "";
|
|
235
|
+
if (!t) return { decision: "skip", reason: "\u7A7A\u6587\u672C" };
|
|
236
|
+
if (POLICY_TERMS.some((w) => t.includes(w))) {
|
|
237
|
+
return { decision: "run", args: { query: text, topK: "3" } };
|
|
238
|
+
}
|
|
239
|
+
if (OPERATIONAL_TERMS.some((w) => t.includes(w))) {
|
|
240
|
+
return { decision: "skip", reason: "\u64CD\u4F5C\u578B\u8BC9\u6C42\u2014\u2014\u4E1A\u52A1\u5DE5\u5177\u5B9E\u65F6\u6570\u636E\u9762\uFF0CFAQ \u4E0D\u9884\u6267\u884C" };
|
|
241
|
+
}
|
|
242
|
+
return { decision: "run", args: { query: text, topK: "3" } };
|
|
243
|
+
},
|
|
244
|
+
silent: true
|
|
245
|
+
});
|
|
246
|
+
var preTools = {
|
|
247
|
+
"customer-service-faq": faqPreTool
|
|
248
|
+
};
|
|
249
|
+
var skills = Object.fromEntries(
|
|
250
|
+
(await loadModuleSkills("@tbox.cn/app-module-customer-service", import.meta.url)).map((s) => [s.name, s])
|
|
251
|
+
);
|
|
252
|
+
var serverModule = {
|
|
253
|
+
declaration: {
|
|
254
|
+
moduleId: "customer-service"
|
|
255
|
+
},
|
|
256
|
+
cards: {
|
|
257
|
+
"customer-service-faq": meta_default
|
|
258
|
+
},
|
|
259
|
+
skills,
|
|
260
|
+
preTools,
|
|
261
|
+
register(ctx) {
|
|
262
|
+
moduleCtx = ctx;
|
|
263
|
+
ctx.cards.registerMap(serverModule.cards);
|
|
264
|
+
for (const tool of Object.values(tools)) {
|
|
265
|
+
ctx.tools.register(tool);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var server_default = serverModule;
|
|
270
|
+
|
|
271
|
+
export {
|
|
272
|
+
currentContactPhone,
|
|
273
|
+
resolveCustomerServicePorts,
|
|
274
|
+
createCustomerServiceTools,
|
|
275
|
+
serverModule,
|
|
276
|
+
server_default
|
|
277
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CardComponent } from '@tbox.cn/app-contracts';
|
|
2
|
+
import { ClientContext } from '@tbox.cn/app-sdk/client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* module-customer-service 客户端入口(与服务端 serverModule 对称)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare const clientModule: {
|
|
9
|
+
cards: {
|
|
10
|
+
readonly 'customer-service-faq': CardComponent;
|
|
11
|
+
};
|
|
12
|
+
register(ctx: ClientContext): void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export { clientModule, clientModule as default };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/client/index.ts
|
|
2
|
+
import "../customer-service-UTQ5AVTL.css";
|
|
3
|
+
|
|
4
|
+
// src/client/cards/customer-service-faq/index.tsx
|
|
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 { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
function CustomerServiceFaqCard(props) {
|
|
10
|
+
const { data, cardId } = props;
|
|
11
|
+
const [openIndex, setOpenIndex] = useState(
|
|
12
|
+
data.faqs.length > 0 ? 0 : null
|
|
13
|
+
);
|
|
14
|
+
if (data.faqs.length === 0) {
|
|
15
|
+
return /* @__PURE__ */ jsx("section", { className: "cs-card", children: /* @__PURE__ */ jsx("div", { className: "cs-empty", children: data.noResultsReason ?? "\u672A\u627E\u5230\u76F8\u5173\u5E38\u89C1\u95EE\u9898" }) });
|
|
16
|
+
}
|
|
17
|
+
return /* @__PURE__ */ jsxs("section", { className: "cs-card cs-faq", children: [
|
|
18
|
+
/* @__PURE__ */ jsx("div", { className: "cs-card-title", children: "\u76F8\u5173\u5E38\u89C1\u95EE\u9898" }),
|
|
19
|
+
/* @__PURE__ */ jsx("ul", { className: "cs-faq-list", children: data.faqs.map((f, i) => {
|
|
20
|
+
const open = openIndex === i;
|
|
21
|
+
return /* @__PURE__ */ jsxs("li", { className: "cs-faq-item", children: [
|
|
22
|
+
/* @__PURE__ */ jsxs(
|
|
23
|
+
"button",
|
|
24
|
+
{
|
|
25
|
+
type: "button",
|
|
26
|
+
className: "cs-faq-item__q",
|
|
27
|
+
"aria-expanded": open,
|
|
28
|
+
onClick: () => setOpenIndex(open ? null : i),
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsx("span", { children: f.question }),
|
|
31
|
+
/* @__PURE__ */ jsx(CaretDown, { size: 16, className: "cs-faq-item__chevron", "data-rotated": open || void 0 })
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
),
|
|
35
|
+
open && /* @__PURE__ */ jsxs("div", { className: "cs-faq-item__a", children: [
|
|
36
|
+
/* @__PURE__ */ jsx("p", { children: f.answer }),
|
|
37
|
+
f.referenceUrl && /* @__PURE__ */ jsxs(
|
|
38
|
+
"button",
|
|
39
|
+
{
|
|
40
|
+
type: "button",
|
|
41
|
+
className: "cs-link-btn cs-faq-item__ref",
|
|
42
|
+
onClick: () => {
|
|
43
|
+
sendCardAction(cardId, { type: "openLink", url: f.referenceUrl });
|
|
44
|
+
},
|
|
45
|
+
children: [
|
|
46
|
+
"\u67E5\u770B\u6765\u6E90",
|
|
47
|
+
/* @__PURE__ */ jsx(ArrowSquareOut, { size: 13 })
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
] })
|
|
52
|
+
] }, i);
|
|
53
|
+
}) }),
|
|
54
|
+
data.contactPhone && /* @__PURE__ */ jsxs(
|
|
55
|
+
"button",
|
|
56
|
+
{
|
|
57
|
+
type: "button",
|
|
58
|
+
className: "cs-contact-btn",
|
|
59
|
+
onClick: () => {
|
|
60
|
+
sendCardAction(cardId, { type: "openLink", url: `tel:${data.contactPhone}` });
|
|
61
|
+
},
|
|
62
|
+
children: [
|
|
63
|
+
/* @__PURE__ */ jsx(Phone, { size: 16 }),
|
|
64
|
+
"\u62E8\u6253\u5BA2\u670D ",
|
|
65
|
+
data.contactPhone
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
] });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/client/index.ts
|
|
73
|
+
var cards = {
|
|
74
|
+
"customer-service-faq": CustomerServiceFaqCard
|
|
75
|
+
};
|
|
76
|
+
var clientModule = {
|
|
77
|
+
cards,
|
|
78
|
+
register(ctx) {
|
|
79
|
+
ctx.cards.registerMap(cards);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var client_default = clientModule;
|
|
83
|
+
export {
|
|
84
|
+
clientModule,
|
|
85
|
+
client_default as default
|
|
86
|
+
};
|
|
@@ -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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { CustomerServicePorts, CustomerServiceToolIds, CustomerServiceTools, MallDirectoryService, createCustomerServiceTools, currentContactPhone, resolveCustomerServicePorts, default as serverModule } from './server/index.js';
|
|
2
|
+
import '@mastra/core/tools';
|
|
3
|
+
import '@tbox.cn/app-sdk/server';
|
|
4
|
+
import '@tbox.cn/app-contracts';
|
|
5
|
+
import 'zod';
|
|
6
|
+
import '@tbox.cn/app-contracts-mall';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCustomerServiceTools,
|
|
3
|
+
currentContactPhone,
|
|
4
|
+
resolveCustomerServicePorts,
|
|
5
|
+
serverModule
|
|
6
|
+
} from "./chunk-PN4SPFWE.js";
|
|
7
|
+
export {
|
|
8
|
+
createCustomerServiceTools,
|
|
9
|
+
currentContactPhone,
|
|
10
|
+
resolveCustomerServicePorts,
|
|
11
|
+
serverModule
|
|
12
|
+
};
|