kece-skill-mcp 0.0.0-stage → 1.2.0

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/dist/auth.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { Config } from "./config.js";
2
+ export declare function md5(content: string): string;
3
+ export declare function aesEncrypt(plainText: string, appSecret: string): string;
4
+ export type AuthHeaders = Record<string, string>;
5
+ export declare function buildAuthHeaders(body: string, config: Config): AuthHeaders;
package/dist/auth.js ADDED
@@ -0,0 +1,27 @@
1
+ import { createHash, createCipheriv } from "crypto";
2
+ export function md5(content) {
3
+ return createHash("md5").update(content, "utf-8").digest("hex");
4
+ }
5
+ function deriveAesKey(appSecret) {
6
+ const sha1Pass1 = createHash("sha1").update(appSecret, "utf-8").digest();
7
+ const sha1Pass2 = createHash("sha1").update(sha1Pass1).digest();
8
+ return sha1Pass2.subarray(0, 16);
9
+ }
10
+ export function aesEncrypt(plainText, appSecret) {
11
+ const keyBuffer = deriveAesKey(appSecret);
12
+ const cipher = createCipheriv("aes-128-ecb", keyBuffer, null);
13
+ cipher.setAutoPadding(true);
14
+ return cipher.update(plainText, "utf-8", "hex") + cipher.final("hex");
15
+ }
16
+ export function buildAuthHeaders(body, config) {
17
+ const sign = md5(body);
18
+ const curtime = Math.floor(Date.now() / 1000).toString();
19
+ const checksum = aesEncrypt(config.appKey + sign + curtime, config.appSecret);
20
+ return {
21
+ "ur-appkey": config.appKey,
22
+ "ur-sign": sign,
23
+ "ur-curtime": curtime,
24
+ "ur-checksum": checksum,
25
+ "Content-Type": "application/json;charset=utf-8",
26
+ };
27
+ }
@@ -0,0 +1,6 @@
1
+ import type { Config } from "./config.js";
2
+ export declare class KeceClient {
3
+ private config;
4
+ constructor(config: Config);
5
+ request<T = any>(path: string, body: Record<string, any>): Promise<T>;
6
+ }
package/dist/client.js ADDED
@@ -0,0 +1,25 @@
1
+ import { buildAuthHeaders } from "./auth.js";
2
+ export class KeceClient {
3
+ config;
4
+ constructor(config) {
5
+ this.config = config;
6
+ }
7
+ async request(path, body) {
8
+ const bodyStr = JSON.stringify(body);
9
+ const headers = buildAuthHeaders(bodyStr, this.config);
10
+ const url = `${this.config.baseUrl}${path}`;
11
+ const response = await fetch(url, {
12
+ method: "POST",
13
+ headers,
14
+ body: bodyStr,
15
+ });
16
+ if (!response.ok) {
17
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
18
+ }
19
+ const result = await response.json();
20
+ if (result.code && String(result.code) !== "200") {
21
+ throw new Error(`API错误 [${result.code}]: ${result.msg || "未知错误"}`);
22
+ }
23
+ return result;
24
+ }
25
+ }
@@ -0,0 +1,6 @@
1
+ export interface Config {
2
+ appKey: string;
3
+ appSecret: string;
4
+ baseUrl: string;
5
+ }
6
+ export declare function getConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from "fs";
2
+ import { resolve } from "path";
3
+ function loadEnv() {
4
+ const envVars = {};
5
+ try {
6
+ const envPath = resolve(import.meta.dirname, "../.env");
7
+ const content = readFileSync(envPath, "utf-8");
8
+ for (const line of content.split("\n")) {
9
+ const trimmed = line.trim();
10
+ if (!trimmed || trimmed.startsWith("#"))
11
+ continue;
12
+ const eqIdx = trimmed.indexOf("=");
13
+ if (eqIdx === -1)
14
+ continue;
15
+ envVars[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
16
+ }
17
+ }
18
+ catch { }
19
+ return envVars;
20
+ }
21
+ export function getConfig() {
22
+ const env = { ...loadEnv(), ...process.env };
23
+ const appKey = env.KECE_APP_KEY;
24
+ const appSecret = env.KECE_APP_SECRET;
25
+ const baseUrl = env.KECE_BASE_URL || "https://huke.163.com";
26
+ if (!appKey || !appSecret) {
27
+ throw new Error("缺少 KECE_APP_KEY 或 KECE_APP_SECRET 配置");
28
+ }
29
+ return { appKey, appSecret, baseUrl };
30
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { getConfig } from "./config.js";
5
+ import { KeceClient } from "./client.js";
6
+ import { queryCustomerSchema, queryCustomer } from "./tools/customer-query.js";
7
+ import { searchCustomersSchema, searchCustomers } from "./tools/customer-search.js";
8
+ import { updateCustomerTagSchema, updateCustomerTag, batchUpdateTagSchema, batchUpdateCustomerTag } from "./tools/customer-tag.js";
9
+ import { createMassSendSchema, createMassSend } from "./tools/mass-send.js";
10
+ import { getCustomerFieldsSchema, getCustomerFields, listStaffSchema, listStaff, listSegmentsSchema, listSegments, createSegmentSchema, createSegment, importSegmentSchema, importSegment, listMassSendTasksSchema, listMassSendTasks, } from "./tools/helpers.js";
11
+ import { resolveIdsSchema, resolveIds } from "./tools/resolve-ids.js";
12
+ import { listCustomersSchema, listCustomers } from "./tools/customer-list.js";
13
+ import { accountHealthCheckSchema, accountHealthCheck } from "./tools/health-check.js";
14
+ let client;
15
+ try {
16
+ client = new KeceClient(getConfig());
17
+ }
18
+ catch (err) {
19
+ console.error(`[kece-skill-mcp] 启动失败:${err?.message || err}`);
20
+ console.error("[kece-skill-mcp] 请在 WorkBuddy 连接器设置中填写 AppKey 与 AppSecret。");
21
+ process.exit(1);
22
+ }
23
+ const server = new McpServer({
24
+ name: "kece-scrm-skill",
25
+ version: "1.2.0",
26
+ });
27
+ function safeTool(fn) {
28
+ return async (params) => {
29
+ try {
30
+ const result = await fn(params);
31
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
32
+ }
33
+ catch (err) {
34
+ const msg = err?.message || String(err);
35
+ let hint = "";
36
+ if (msg.includes("1014") || msg.includes("认证失败")) {
37
+ hint = "\n\n请检查 AppKey 和 AppSecret 是否正确。获取路径:科策后台 → 设置 → 系统对接 → API对接";
38
+ }
39
+ else if (msg.includes("1003") || msg.includes("无权限")) {
40
+ hint = "\n\n当前 API 账号无此操作权限,请联系管理员。";
41
+ }
42
+ else if (msg.includes("1009") || msg.includes("缺少参数")) {
43
+ hint = "\n\n请补充必要参数后重试。";
44
+ }
45
+ else if (msg.includes("fetch") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT")) {
46
+ hint = "\n\n无法连接科策服务器,请检查网络或稍后重试。";
47
+ }
48
+ return { content: [{ type: "text", text: `调用失败:${msg}${hint}` }] };
49
+ }
50
+ };
51
+ }
52
+ // ===== Skill 1: 查客户 =====
53
+ server.tool("query_customer", "查询客户详情。支持通过手机号、姓名、微信号、邮箱、CID、公司名称查询客户信息。", queryCustomerSchema.shape, safeTool((params) => queryCustomer(client, params)));
54
+ // ===== Skill 3: 圈人 =====
55
+ server.tool("search_customers", "条件筛选客户(圈人)。支持按标签、手机号、客层等条件筛选客户,返回符合条件的客户列表和总数。用于人群圈选。", searchCustomersSchema.shape, safeTool((params) => searchCustomers(client, params)));
56
+ // ===== Skill 2: 打标签 =====
57
+ server.tool("update_customer_tag", "为单个客户添加或移除标签。需要先通过 get_customer_fields 获取标签组ID和标签ID。", updateCustomerTagSchema.shape, safeTool((params) => updateCustomerTag(client, params)));
58
+ server.tool("batch_update_tag", "批量为多个客户添加或移除标签。传入客户ID列表,并发执行。超过10人时建议先确认。", batchUpdateTagSchema.shape, safeTool((params) => batchUpdateCustomerTag(client, params)));
59
+ // ===== Skill 4: 群发 =====
60
+ server.tool("create_mass_send", "创建企微客户群发任务。基于筛选条件圈选目标客户并发送消息。所有群发操作必须经用户确认后执行。", createMassSendSchema.shape, safeTool((params) => createMassSend(client, params)));
61
+ // ===== 辅助工具 =====
62
+ server.tool("get_customer_fields", "获取所有客户字段定义,包括标签组及选项。用于将标签名称转换为标签ID,是打标签和圈人的前置查询。", getCustomerFieldsSchema.shape, safeTool(() => getCustomerFields(client)));
63
+ server.tool("list_staff", "查询企业员工列表,返回 accid、姓名、角色、部门。用于群发时获取发送人 accid 列表。", listStaffSchema.shape, safeTool((params) => listStaff(client, params)));
64
+ server.tool("list_segments", "查询人群包列表。支持按关键词搜索。用于查看已有人群包或获取人群包ID。", listSegmentsSchema.shape, safeTool((params) => listSegments(client, params)));
65
+ server.tool("create_segment", "创建一个新的导入型人群包。创建后可通过 import_segment 导入用户。", createSegmentSchema.shape, safeTool((params) => createSegment(client, params)));
66
+ server.tool("import_segment", "将客户ID列表导入到指定人群包。配合 search_customers 获取的 cid 和 create_segment 创建的人群包使用。", importSegmentSchema.shape, safeTool((params) => importSegment(client, params)));
67
+ server.tool("list_mass_send_tasks", "查询群发任务列表,了解最近群发的执行状态。", listMassSendTasksSchema.shape, safeTool((params) => listMassSendTasks(client, params)));
68
+ // ===== 全量客户检索 =====
69
+ server.tool("list_customers", "查询全量客户。不带任何筛选条件时返回客户总数(回答「我们一共多少客户」);可按时间范围、跟进人筛选,或分页拉取客户明细。与 search_customers 的区别:本工具面向全量客户池,不依赖人群包或标签圈选。", listCustomersSchema.shape, safeTool((params) => listCustomers(client, params)));
70
+ // ===== 账号体检 =====
71
+ server.tool("account_health_check", "生成账号体检报告:客户资产、员工维度、社群维度、获客资产、运营动作、转化六个维度的统计报表。一次调用返回 Markdown 报表,适合回答「账号体检」「私域盘点」「最近整体运营情况如何」。", accountHealthCheckSchema.shape, safeTool((params) => accountHealthCheck(client, params)));
72
+ // ===== ID 映射(科策 × 数数打通) =====
73
+ server.tool("resolve_ids", "双向转换游戏账号ID(数数#account_id)与科策客户ID(cid)。用于科策与数数ThinkingData之间的用户身份打通。需要先通过 get_customer_fields 确认存储游戏账号ID的字段。", resolveIdsSchema.shape, safeTool((params) => resolveIds(client, params)));
74
+ async function main() {
75
+ const transport = new StdioServerTransport();
76
+ await server.connect(transport);
77
+ }
78
+ main().catch(console.error);
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import type { KeceClient } from "../client.js";
3
+ export declare const listCustomersSchema: z.ZodObject<{
4
+ mode: z.ZodDefault<z.ZodOptional<z.ZodEnum<["count", "list"]>>>;
5
+ days: z.ZodOptional<z.ZodNumber>;
6
+ startTime: z.ZodOptional<z.ZodNumber>;
7
+ endTime: z.ZodOptional<z.ZodNumber>;
8
+ follower: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
+ page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
10
+ pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ page: number;
13
+ pageSize: number;
14
+ mode: "count" | "list";
15
+ days?: number | undefined;
16
+ startTime?: number | undefined;
17
+ endTime?: number | undefined;
18
+ follower?: string[] | undefined;
19
+ }, {
20
+ page?: number | undefined;
21
+ pageSize?: number | undefined;
22
+ mode?: "count" | "list" | undefined;
23
+ days?: number | undefined;
24
+ startTime?: number | undefined;
25
+ endTime?: number | undefined;
26
+ follower?: string[] | undefined;
27
+ }>;
28
+ export declare function listCustomers(client: KeceClient, params: z.infer<typeof listCustomersSchema>): Promise<{
29
+ mode: string;
30
+ period: string;
31
+ total: any;
32
+ } | {
33
+ page: number;
34
+ pageSize: number;
35
+ customers: any;
36
+ mode: "count" | "list";
37
+ period: string;
38
+ total: any;
39
+ }>;
@@ -0,0 +1,138 @@
1
+ import { z } from "zod";
2
+ export const listCustomersSchema = z.object({
3
+ mode: z
4
+ .enum(["count", "list"])
5
+ .optional()
6
+ .default("count")
7
+ .describe("count=只返回客户总数;list=返回分页客户明细"),
8
+ days: z
9
+ .number()
10
+ .int()
11
+ .min(1)
12
+ .max(3650)
13
+ .optional()
14
+ .describe("统计最近 N 天内进入客户池的客户,与 startTime/endTime 二选一"),
15
+ startTime: z.number().optional().describe("起始时间,毫秒时间戳"),
16
+ endTime: z.number().optional().describe("结束时间,毫秒时间戳,默认当前时间"),
17
+ follower: z.array(z.string()).optional().describe("按跟进人 accid 过滤"),
18
+ page: z.number().int().min(1).optional().default(1).describe("页码(mode=list 时生效)"),
19
+ pageSize: z.number().int().min(1).max(100).optional().default(20).describe("每页数量,最大 100"),
20
+ });
21
+ // customer/list 在 startTime=0 时会返回 1010 error param,必须给一个非零起点
22
+ const EARLIEST = 946684800000; // 2000-01-01
23
+ export async function listCustomers(client, params) {
24
+ const now = Date.now();
25
+ const hasFilter = params.days !== undefined ||
26
+ params.startTime !== undefined ||
27
+ params.endTime !== undefined ||
28
+ (params.follower && params.follower.length > 0);
29
+ // 无任何筛选条件的全量总数走 customer/search,最快(实测与 customer/list 结果一致)
30
+ if (params.mode === "count" && !hasFilter) {
31
+ const result = await client.request("/openapi/customer/search", {
32
+ customerType: 1,
33
+ page: 1,
34
+ pageSize: 1,
35
+ });
36
+ const data = unwrap(result);
37
+ return {
38
+ mode: "count",
39
+ period: "全部",
40
+ total: data.total || 0,
41
+ };
42
+ }
43
+ const { startTime, endTime, period } = resolveRange(params, now);
44
+ const body = {
45
+ startTime,
46
+ endTime,
47
+ page: params.mode === "list" ? params.page : 1,
48
+ pageSize: params.mode === "list" ? params.pageSize : 1,
49
+ };
50
+ if (params.follower && params.follower.length > 0)
51
+ body.follower = params.follower;
52
+ const result = await client.request("/openapi/customer/list", body);
53
+ const data = unwrap(result);
54
+ const base = {
55
+ mode: params.mode,
56
+ period,
57
+ total: data.total || 0,
58
+ };
59
+ if (params.mode === "count")
60
+ return base;
61
+ const list = data.customerList || [];
62
+ return {
63
+ ...base,
64
+ page: params.page,
65
+ pageSize: params.pageSize,
66
+ customers: list.map((c) => {
67
+ const name = findField(c.customFields, -2);
68
+ const mobile = findField(c.customFields, -3);
69
+ return {
70
+ cid: c.cid,
71
+ name: name || "未填写",
72
+ mobile: maskPhone(mobile),
73
+ follower: c.follower || "",
74
+ status: formatStatus(c.status),
75
+ entryTime: fmtTime(c.entryTime),
76
+ };
77
+ }),
78
+ };
79
+ }
80
+ function unwrap(result) {
81
+ if (result?.data && typeof result.data === "object" && result.data.total !== undefined) {
82
+ return result.data;
83
+ }
84
+ return result || {};
85
+ }
86
+ function resolveRange(params, now) {
87
+ const endTime = params.endTime ?? now;
88
+ let startTime;
89
+ if (params.startTime !== undefined) {
90
+ startTime = params.startTime;
91
+ }
92
+ else if (params.days !== undefined) {
93
+ startTime = endTime - params.days * 86400000;
94
+ }
95
+ else {
96
+ startTime = EARLIEST;
97
+ }
98
+ if (startTime < EARLIEST)
99
+ startTime = EARLIEST;
100
+ const label = params.days !== undefined
101
+ ? `近 ${params.days} 天`
102
+ : `${fmtTime(startTime)} ~ ${fmtTime(endTime)}`;
103
+ return { startTime, endTime, period: label };
104
+ }
105
+ function findField(fields, id) {
106
+ if (!Array.isArray(fields))
107
+ return "";
108
+ const hit = fields.find((f) => Number(f.id) === id);
109
+ return hit?.fieldValue ?? "";
110
+ }
111
+ function maskPhone(phone) {
112
+ if (!phone)
113
+ return "";
114
+ if (phone.length < 7)
115
+ return phone;
116
+ return phone.slice(0, 3) + "****" + phone.slice(-4);
117
+ }
118
+ function fmtTime(ms) {
119
+ if (!ms)
120
+ return "";
121
+ const d = new Date(ms);
122
+ const pad = (n) => String(n).padStart(2, "0");
123
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
124
+ }
125
+ function formatStatus(status) {
126
+ const map = {
127
+ 0: "未入池",
128
+ 4: "待分配潜客",
129
+ 8: "跟进中潜客",
130
+ 9: "公海潜客",
131
+ 5: "待分配",
132
+ 6: "跟进中",
133
+ 7: "公海",
134
+ 2: "已删除",
135
+ 3: "已合并",
136
+ };
137
+ return map[status] || `状态${status}`;
138
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import type { KeceClient } from "../client.js";
3
+ export declare const queryCustomerSchema: z.ZodObject<{
4
+ type: z.ZodEnum<["phone", "wechat", "email", "cid", "name", "company"]>;
5
+ value: z.ZodString;
6
+ }, "strip", z.ZodTypeAny, {
7
+ type: "phone" | "wechat" | "email" | "cid" | "name" | "company";
8
+ value: string;
9
+ }, {
10
+ type: "phone" | "wechat" | "email" | "cid" | "name" | "company";
11
+ value: string;
12
+ }>;
13
+ export declare function queryCustomer(client: KeceClient, params: z.infer<typeof queryCustomerSchema>): Promise<{
14
+ found: boolean;
15
+ message: string;
16
+ total?: undefined;
17
+ customers?: undefined;
18
+ } | {
19
+ found: boolean;
20
+ total: any;
21
+ customers: any;
22
+ message?: undefined;
23
+ }>;
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ export const queryCustomerSchema = z.object({
3
+ type: z
4
+ .enum(["phone", "wechat", "email", "cid", "name", "company"])
5
+ .describe("查询类型:phone=手机号, wechat=微信号, email=邮箱, cid=客户ID, name=姓名, company=公司名称"),
6
+ value: z.string().describe("查询值,如手机号、姓名等"),
7
+ });
8
+ const TYPE_MAP = {
9
+ phone: 0,
10
+ wechat: 1,
11
+ email: 2,
12
+ cid: 3,
13
+ name: 5,
14
+ company: 6,
15
+ };
16
+ export async function queryCustomer(client, params) {
17
+ const result = await client.request("/openapi/customer/query", {
18
+ type: TYPE_MAP[params.type],
19
+ targets: [params.value],
20
+ });
21
+ const customers = result.data || [];
22
+ if (customers.length === 0) {
23
+ return { found: false, message: "未找到匹配的客户,请确认信息是否正确" };
24
+ }
25
+ const formatted = customers.map((c) => ({
26
+ cid: c.cid,
27
+ name: getFieldValue(c.customFields, -2),
28
+ phone: maskPhone(getFieldValue(c.customFields, -3)),
29
+ wechatNick: getFieldValue(c.customFields, -6),
30
+ email: getFieldValue(c.customFields, -4),
31
+ follower: c.followerNick || c.follower,
32
+ status: formatStatus(c.status),
33
+ tags: c.customFields
34
+ ?.filter((f) => f.type === 4 || f.type === 5 || f.type === 11)
35
+ .map((f) => ({ name: f.name, value: f.fieldValue })) || [],
36
+ createTime: formatTime(c.createTime),
37
+ entryTime: formatTime(c.entryTime),
38
+ lastActivity: formatTime(c.updateTime),
39
+ }));
40
+ return {
41
+ found: true,
42
+ total: customers.length,
43
+ customers: formatted,
44
+ };
45
+ }
46
+ function getFieldValue(fields, id) {
47
+ return fields?.find((f) => f.id === id)?.fieldValue || "";
48
+ }
49
+ function maskPhone(phone) {
50
+ if (!phone || phone.length < 7)
51
+ return phone || "无";
52
+ return phone.slice(0, 3) + "****" + phone.slice(-4);
53
+ }
54
+ function formatStatus(status) {
55
+ const map = {
56
+ 4: "待分配潜客",
57
+ 5: "待分配",
58
+ 6: "跟进中",
59
+ 7: "公海",
60
+ 8: "跟进中潜客",
61
+ 9: "公海潜客",
62
+ };
63
+ return map[status] || `状态${status}`;
64
+ }
65
+ function formatTime(ts) {
66
+ if (!ts)
67
+ return "无";
68
+ return new Date(ts).toISOString().slice(0, 10);
69
+ }
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import type { KeceClient } from "../client.js";
3
+ export declare const searchCustomersSchema: z.ZodObject<{
4
+ filterType: z.ZodEnum<["quick", "advanced", "segment"]>;
5
+ filterFields: z.ZodOptional<z.ZodArray<z.ZodObject<{
6
+ fieldId: z.ZodString;
7
+ filterType: z.ZodNumber;
8
+ filterValue: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ fieldId: string;
11
+ filterType: number;
12
+ filterValue?: string | undefined;
13
+ }, {
14
+ fieldId: string;
15
+ filterType: number;
16
+ filterValue?: string | undefined;
17
+ }>, "many">>;
18
+ segmentIds: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
19
+ keyword: z.ZodOptional<z.ZodString>;
20
+ page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
21
+ pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
22
+ }, "strip", z.ZodTypeAny, {
23
+ filterType: "quick" | "advanced" | "segment";
24
+ page: number;
25
+ pageSize: number;
26
+ filterFields?: {
27
+ fieldId: string;
28
+ filterType: number;
29
+ filterValue?: string | undefined;
30
+ }[] | undefined;
31
+ segmentIds?: number[] | undefined;
32
+ keyword?: string | undefined;
33
+ }, {
34
+ filterType: "quick" | "advanced" | "segment";
35
+ filterFields?: {
36
+ fieldId: string;
37
+ filterType: number;
38
+ filterValue?: string | undefined;
39
+ }[] | undefined;
40
+ segmentIds?: number[] | undefined;
41
+ keyword?: string | undefined;
42
+ page?: number | undefined;
43
+ pageSize?: number | undefined;
44
+ }>;
45
+ export declare function searchCustomers(client: KeceClient, params: z.infer<typeof searchCustomersSchema>): Promise<{
46
+ total: any;
47
+ page: number;
48
+ pageSize: number;
49
+ customers: any;
50
+ }>;
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ const filterFieldSchema = z.object({
3
+ fieldId: z.string().describe("筛选字段ID,如 label(标签)、mobile(手机号)"),
4
+ filterType: z.number().describe("筛选条件:1=满足/包含, 2=不为空, 3=包含所有, 4=不包含, 5=等于, 6=不等于, 7=介于, 8=大于, 9=小于"),
5
+ filterValue: z.string().optional().describe("筛选值"),
6
+ });
7
+ export const searchCustomersSchema = z.object({
8
+ filterType: z
9
+ .enum(["quick", "advanced", "segment"])
10
+ .describe("筛选模式:quick=快捷筛选(标签), advanced=高级筛选(多条件), segment=客层筛选"),
11
+ filterFields: z
12
+ .array(filterFieldSchema)
13
+ .optional()
14
+ .describe("筛选字段列表(quick/advanced 模式使用)"),
15
+ segmentIds: z
16
+ .array(z.number())
17
+ .optional()
18
+ .describe("客层ID列表(segment 模式使用)"),
19
+ keyword: z.string().optional().describe("搜索关键词(至少2个字符)"),
20
+ page: z.number().optional().default(1).describe("页码"),
21
+ pageSize: z.number().optional().default(50).describe("每页数量,1~200"),
22
+ });
23
+ const FILTER_TYPE_MAP = {
24
+ quick: 1,
25
+ advanced: 3,
26
+ segment: 2,
27
+ };
28
+ export async function searchCustomers(client, params) {
29
+ const body = {
30
+ customerType: 1,
31
+ page: params.page,
32
+ pageSize: params.pageSize,
33
+ };
34
+ if (params.keyword) {
35
+ body.keyword = params.keyword;
36
+ body.filterId = "-1";
37
+ body.queryFromType = 2;
38
+ }
39
+ else {
40
+ const queryFilter = { type: FILTER_TYPE_MAP[params.filterType] };
41
+ if (params.filterType === "segment") {
42
+ queryFilter.segmentIds = params.segmentIds || [];
43
+ }
44
+ else {
45
+ queryFilter.filterContent = {
46
+ filterFields: (params.filterFields || []).map((f) => ({
47
+ fieldId: f.fieldId,
48
+ filterType: f.filterType,
49
+ filterValue: f.filterValue || "",
50
+ })),
51
+ };
52
+ }
53
+ body.queryFilter = queryFilter;
54
+ }
55
+ const result = await client.request("/openapi/customer/search", body);
56
+ return {
57
+ total: result.total || 0,
58
+ page: params.page,
59
+ pageSize: params.pageSize,
60
+ customers: (result.customerList || []).map((c) => ({
61
+ cid: c.cid,
62
+ name: c.name,
63
+ phone: maskPhone(c.mobile),
64
+ follower: c.follower,
65
+ labels: c.labels || [],
66
+ createTime: c.createTime ? new Date(c.createTime).toISOString().slice(0, 10) : "",
67
+ entryTime: c.entryTime ? new Date(c.entryTime).toISOString().slice(0, 10) : "",
68
+ })),
69
+ };
70
+ }
71
+ function maskPhone(phone) {
72
+ if (!phone || phone.length < 7)
73
+ return phone || "无";
74
+ return phone.slice(0, 3) + "****" + phone.slice(-4);
75
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import type { KeceClient } from "../client.js";
3
+ export declare const updateCustomerTagSchema: z.ZodObject<{
4
+ cid: z.ZodOptional<z.ZodNumber>;
5
+ externalUserid: z.ZodOptional<z.ZodString>;
6
+ tagGroupId: z.ZodNumber;
7
+ addTag: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
8
+ removeTag: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
9
+ syncQiWei: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ tagGroupId: number;
12
+ syncQiWei: boolean;
13
+ cid?: number | undefined;
14
+ externalUserid?: string | undefined;
15
+ addTag?: number[] | undefined;
16
+ removeTag?: number[] | undefined;
17
+ }, {
18
+ tagGroupId: number;
19
+ cid?: number | undefined;
20
+ externalUserid?: string | undefined;
21
+ addTag?: number[] | undefined;
22
+ removeTag?: number[] | undefined;
23
+ syncQiWei?: boolean | undefined;
24
+ }>;
25
+ export declare function updateCustomerTag(client: KeceClient, params: z.infer<typeof updateCustomerTagSchema>): Promise<{
26
+ success: boolean;
27
+ message: string;
28
+ syncQiWei?: undefined;
29
+ } | {
30
+ success: boolean;
31
+ message: string;
32
+ syncQiWei: boolean;
33
+ }>;
34
+ export declare const batchUpdateTagSchema: z.ZodObject<{
35
+ cidList: z.ZodArray<z.ZodNumber, "many">;
36
+ tagGroupId: z.ZodNumber;
37
+ addTag: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
38
+ removeTag: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
39
+ syncQiWei: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
40
+ }, "strip", z.ZodTypeAny, {
41
+ tagGroupId: number;
42
+ syncQiWei: boolean;
43
+ cidList: number[];
44
+ addTag?: number[] | undefined;
45
+ removeTag?: number[] | undefined;
46
+ }, {
47
+ tagGroupId: number;
48
+ cidList: number[];
49
+ addTag?: number[] | undefined;
50
+ removeTag?: number[] | undefined;
51
+ syncQiWei?: boolean | undefined;
52
+ }>;
53
+ export declare function batchUpdateCustomerTag(client: KeceClient, params: z.infer<typeof batchUpdateTagSchema>): Promise<{
54
+ message: string;
55
+ successCount: number;
56
+ failCount: number;
57
+ failList: number[];
58
+ }>;