kece-skill-mcp 0.0.0-stage → 1.1.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 +5 -0
- package/dist/auth.js +27 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +25 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +72 -0
- package/dist/tools/customer-query.d.ts +23 -0
- package/dist/tools/customer-query.js +69 -0
- package/dist/tools/customer-search.d.ts +50 -0
- package/dist/tools/customer-search.js +75 -0
- package/dist/tools/customer-tag.d.ts +58 -0
- package/dist/tools/customer-tag.js +77 -0
- package/dist/tools/helpers.d.ts +77 -0
- package/dist/tools/helpers.js +135 -0
- package/dist/tools/mass-send.d.ts +100 -0
- package/dist/tools/mass-send.js +94 -0
- package/dist/tools/resolve-ids.d.ts +16 -0
- package/dist/tools/resolve-ids.js +79 -0
- package/package.json +29 -4
- package/README.md +0 -3
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
|
+
}
|
package/dist/client.d.ts
ADDED
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
|
+
}
|
package/dist/config.d.ts
ADDED
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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
let client;
|
|
13
|
+
try {
|
|
14
|
+
client = new KeceClient(getConfig());
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
console.error(`[kece-skill-mcp] 启动失败:${err?.message || err}`);
|
|
18
|
+
console.error("[kece-skill-mcp] 请在 WorkBuddy 连接器设置中填写 AppKey 与 AppSecret。");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const server = new McpServer({
|
|
22
|
+
name: "kece-scrm-skill",
|
|
23
|
+
version: "1.1.0",
|
|
24
|
+
});
|
|
25
|
+
function safeTool(fn) {
|
|
26
|
+
return async (params) => {
|
|
27
|
+
try {
|
|
28
|
+
const result = await fn(params);
|
|
29
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const msg = err?.message || String(err);
|
|
33
|
+
let hint = "";
|
|
34
|
+
if (msg.includes("1014") || msg.includes("认证失败")) {
|
|
35
|
+
hint = "\n\n请检查 AppKey 和 AppSecret 是否正确。获取路径:科策后台 → 设置 → 系统对接 → API对接";
|
|
36
|
+
}
|
|
37
|
+
else if (msg.includes("1003") || msg.includes("无权限")) {
|
|
38
|
+
hint = "\n\n当前 API 账号无此操作权限,请联系管理员。";
|
|
39
|
+
}
|
|
40
|
+
else if (msg.includes("1009") || msg.includes("缺少参数")) {
|
|
41
|
+
hint = "\n\n请补充必要参数后重试。";
|
|
42
|
+
}
|
|
43
|
+
else if (msg.includes("fetch") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT")) {
|
|
44
|
+
hint = "\n\n无法连接科策服务器,请检查网络或稍后重试。";
|
|
45
|
+
}
|
|
46
|
+
return { content: [{ type: "text", text: `调用失败:${msg}${hint}` }] };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// ===== Skill 1: 查客户 =====
|
|
51
|
+
server.tool("query_customer", "查询客户详情。支持通过手机号、姓名、微信号、邮箱、CID、公司名称查询客户信息。", queryCustomerSchema.shape, safeTool((params) => queryCustomer(client, params)));
|
|
52
|
+
// ===== Skill 3: 圈人 =====
|
|
53
|
+
server.tool("search_customers", "条件筛选客户(圈人)。支持按标签、手机号、客层等条件筛选客户,返回符合条件的客户列表和总数。用于人群圈选。", searchCustomersSchema.shape, safeTool((params) => searchCustomers(client, params)));
|
|
54
|
+
// ===== Skill 2: 打标签 =====
|
|
55
|
+
server.tool("update_customer_tag", "为单个客户添加或移除标签。需要先通过 get_customer_fields 获取标签组ID和标签ID。", updateCustomerTagSchema.shape, safeTool((params) => updateCustomerTag(client, params)));
|
|
56
|
+
server.tool("batch_update_tag", "批量为多个客户添加或移除标签。传入客户ID列表,并发执行。超过10人时建议先确认。", batchUpdateTagSchema.shape, safeTool((params) => batchUpdateCustomerTag(client, params)));
|
|
57
|
+
// ===== Skill 4: 群发 =====
|
|
58
|
+
server.tool("create_mass_send", "创建企微客户群发任务。基于筛选条件圈选目标客户并发送消息。所有群发操作必须经用户确认后执行。", createMassSendSchema.shape, safeTool((params) => createMassSend(client, params)));
|
|
59
|
+
// ===== 辅助工具 =====
|
|
60
|
+
server.tool("get_customer_fields", "获取所有客户字段定义,包括标签组及选项。用于将标签名称转换为标签ID,是打标签和圈人的前置查询。", getCustomerFieldsSchema.shape, safeTool(() => getCustomerFields(client)));
|
|
61
|
+
server.tool("list_staff", "查询企业员工列表,返回 accid、姓名、角色、部门。用于群发时获取发送人 accid 列表。", listStaffSchema.shape, safeTool((params) => listStaff(client, params)));
|
|
62
|
+
server.tool("list_segments", "查询人群包列表。支持按关键词搜索。用于查看已有人群包或获取人群包ID。", listSegmentsSchema.shape, safeTool((params) => listSegments(client, params)));
|
|
63
|
+
server.tool("create_segment", "创建一个新的导入型人群包。创建后可通过 import_segment 导入用户。", createSegmentSchema.shape, safeTool((params) => createSegment(client, params)));
|
|
64
|
+
server.tool("import_segment", "将客户ID列表导入到指定人群包。配合 search_customers 获取的 cid 和 create_segment 创建的人群包使用。", importSegmentSchema.shape, safeTool((params) => importSegment(client, params)));
|
|
65
|
+
server.tool("list_mass_send_tasks", "查询群发任务列表,了解最近群发的执行状态。", listMassSendTasksSchema.shape, safeTool((params) => listMassSendTasks(client, params)));
|
|
66
|
+
// ===== ID 映射(科策 × 数数打通) =====
|
|
67
|
+
server.tool("resolve_ids", "双向转换游戏账号ID(数数#account_id)与科策客户ID(cid)。用于科策与数数ThinkingData之间的用户身份打通。需要先通过 get_customer_fields 确认存储游戏账号ID的字段。", resolveIdsSchema.shape, safeTool((params) => resolveIds(client, params)));
|
|
68
|
+
async function main() {
|
|
69
|
+
const transport = new StdioServerTransport();
|
|
70
|
+
await server.connect(transport);
|
|
71
|
+
}
|
|
72
|
+
main().catch(console.error);
|
|
@@ -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
|
+
}>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const updateCustomerTagSchema = z.object({
|
|
3
|
+
cid: z.number().optional().describe("客户ID(cid 和 externalUserid 二选一)"),
|
|
4
|
+
externalUserid: z.string().optional().describe("企微 external_userid"),
|
|
5
|
+
tagGroupId: z.number().describe("标签组ID(通过 get_customer_fields 查询获得)"),
|
|
6
|
+
addTag: z.array(z.number()).optional().describe("要添加的标签ID列表"),
|
|
7
|
+
removeTag: z.array(z.number()).optional().describe("要移除的标签ID列表"),
|
|
8
|
+
syncQiWei: z.boolean().optional().default(false).describe("是否同步到企微标签"),
|
|
9
|
+
});
|
|
10
|
+
export async function updateCustomerTag(client, params) {
|
|
11
|
+
if (!params.cid && !params.externalUserid) {
|
|
12
|
+
return { success: false, message: "请提供 cid 或 externalUserid" };
|
|
13
|
+
}
|
|
14
|
+
if (!params.addTag?.length && !params.removeTag?.length) {
|
|
15
|
+
return { success: false, message: "请至少提供 addTag 或 removeTag" };
|
|
16
|
+
}
|
|
17
|
+
const body = {
|
|
18
|
+
tagGroupId: params.tagGroupId,
|
|
19
|
+
syncQiWei: params.syncQiWei,
|
|
20
|
+
};
|
|
21
|
+
if (params.cid)
|
|
22
|
+
body.cid = params.cid;
|
|
23
|
+
if (params.externalUserid)
|
|
24
|
+
body.externalUserid = params.externalUserid;
|
|
25
|
+
if (params.addTag?.length)
|
|
26
|
+
body.addTag = params.addTag;
|
|
27
|
+
if (params.removeTag?.length)
|
|
28
|
+
body.removeTag = params.removeTag;
|
|
29
|
+
await client.request("/openapi/customer/updateCustomerTag", body);
|
|
30
|
+
const actions = [];
|
|
31
|
+
if (params.addTag?.length)
|
|
32
|
+
actions.push(`添加 ${params.addTag.length} 个标签`);
|
|
33
|
+
if (params.removeTag?.length)
|
|
34
|
+
actions.push(`移除 ${params.removeTag.length} 个标签`);
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
message: `已为客户 ${params.cid || params.externalUserid} ${actions.join("、")}`,
|
|
38
|
+
syncQiWei: params.syncQiWei,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export const batchUpdateTagSchema = z.object({
|
|
42
|
+
cidList: z.array(z.number()).describe("客户ID列表"),
|
|
43
|
+
tagGroupId: z.number().describe("标签组ID"),
|
|
44
|
+
addTag: z.array(z.number()).optional().describe("要添加的标签ID列表"),
|
|
45
|
+
removeTag: z.array(z.number()).optional().describe("要移除的标签ID列表"),
|
|
46
|
+
syncQiWei: z.boolean().optional().default(false).describe("是否同步到企微"),
|
|
47
|
+
});
|
|
48
|
+
export async function batchUpdateCustomerTag(client, params) {
|
|
49
|
+
const results = { success: 0, fail: 0, failList: [] };
|
|
50
|
+
const concurrency = 10;
|
|
51
|
+
for (let i = 0; i < params.cidList.length; i += concurrency) {
|
|
52
|
+
const batch = params.cidList.slice(i, i + concurrency);
|
|
53
|
+
const promises = batch.map(async (cid) => {
|
|
54
|
+
try {
|
|
55
|
+
await updateCustomerTag(client, {
|
|
56
|
+
cid,
|
|
57
|
+
tagGroupId: params.tagGroupId,
|
|
58
|
+
addTag: params.addTag,
|
|
59
|
+
removeTag: params.removeTag,
|
|
60
|
+
syncQiWei: params.syncQiWei,
|
|
61
|
+
});
|
|
62
|
+
results.success++;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
results.fail++;
|
|
66
|
+
results.failList.push(cid);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
await Promise.all(promises);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
message: `批量打标签完成:成功 ${results.success},失败 ${results.fail}`,
|
|
73
|
+
successCount: results.success,
|
|
74
|
+
failCount: results.fail,
|
|
75
|
+
failList: results.failList,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { KeceClient } from "../client.js";
|
|
3
|
+
export declare const getCustomerFieldsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
4
|
+
export declare function getCustomerFields(client: KeceClient): Promise<{
|
|
5
|
+
fields: any;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const listStaffSchema: z.ZodObject<{
|
|
8
|
+
page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
9
|
+
pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
10
|
+
usingStatus: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
page: number;
|
|
13
|
+
pageSize: number;
|
|
14
|
+
usingStatus: number;
|
|
15
|
+
}, {
|
|
16
|
+
page?: number | undefined;
|
|
17
|
+
pageSize?: number | undefined;
|
|
18
|
+
usingStatus?: number | undefined;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function listStaff(client: KeceClient, params: z.infer<typeof listStaffSchema>): Promise<{
|
|
21
|
+
total: any;
|
|
22
|
+
staffs: any;
|
|
23
|
+
}>;
|
|
24
|
+
export declare const listSegmentsSchema: z.ZodObject<{
|
|
25
|
+
keyword: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
keyword?: string | undefined;
|
|
28
|
+
}, {
|
|
29
|
+
keyword?: string | undefined;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function listSegments(client: KeceClient, params: z.infer<typeof listSegmentsSchema>): Promise<{
|
|
32
|
+
total: any;
|
|
33
|
+
segments: any;
|
|
34
|
+
}>;
|
|
35
|
+
export declare const createSegmentSchema: z.ZodObject<{
|
|
36
|
+
name: z.ZodString;
|
|
37
|
+
description: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, "strip", z.ZodTypeAny, {
|
|
39
|
+
name: string;
|
|
40
|
+
description?: string | undefined;
|
|
41
|
+
}, {
|
|
42
|
+
name: string;
|
|
43
|
+
description?: string | undefined;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function createSegment(client: KeceClient, params: z.infer<typeof createSegmentSchema>): Promise<{
|
|
46
|
+
success: boolean;
|
|
47
|
+
segmentId: any;
|
|
48
|
+
message: string;
|
|
49
|
+
}>;
|
|
50
|
+
export declare const importSegmentSchema: z.ZodObject<{
|
|
51
|
+
segmentId: z.ZodNumber;
|
|
52
|
+
cidList: z.ZodArray<z.ZodNumber, "many">;
|
|
53
|
+
accountType: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
54
|
+
}, "strip", z.ZodTypeAny, {
|
|
55
|
+
cidList: number[];
|
|
56
|
+
segmentId: number;
|
|
57
|
+
accountType: number;
|
|
58
|
+
}, {
|
|
59
|
+
cidList: number[];
|
|
60
|
+
segmentId: number;
|
|
61
|
+
accountType?: number | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
export declare function importSegment(client: KeceClient, params: z.infer<typeof importSegmentSchema>): Promise<{
|
|
64
|
+
success: boolean;
|
|
65
|
+
message: string;
|
|
66
|
+
}>;
|
|
67
|
+
export declare const listMassSendTasksSchema: z.ZodObject<{
|
|
68
|
+
pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
69
|
+
}, "strip", z.ZodTypeAny, {
|
|
70
|
+
pageSize: number;
|
|
71
|
+
}, {
|
|
72
|
+
pageSize?: number | undefined;
|
|
73
|
+
}>;
|
|
74
|
+
export declare function listMassSendTasks(client: KeceClient, params: z.infer<typeof listMassSendTasksSchema>): Promise<{
|
|
75
|
+
total: any;
|
|
76
|
+
tasks: any;
|
|
77
|
+
}>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ===== 查询客户字段 =====
|
|
3
|
+
export const getCustomerFieldsSchema = z.object({});
|
|
4
|
+
export async function getCustomerFields(client) {
|
|
5
|
+
const result = await client.request("/openapi/customer/fields", {});
|
|
6
|
+
const fields = result.data || [];
|
|
7
|
+
return {
|
|
8
|
+
fields: fields.map((f) => ({
|
|
9
|
+
id: f.id,
|
|
10
|
+
name: f.name,
|
|
11
|
+
type: f.type,
|
|
12
|
+
typeLabel: formatFieldType(f.type),
|
|
13
|
+
status: f.status,
|
|
14
|
+
options: f.options?.map((o) => ({ id: o.id, value: o.optionValue })) || [],
|
|
15
|
+
})),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function formatFieldType(type) {
|
|
19
|
+
const map = {
|
|
20
|
+
1: "单行文本", 2: "多行文本", 3: "数字",
|
|
21
|
+
4: "单选标签", 5: "多选标签", 8: "日期",
|
|
22
|
+
9: "时间", 10: "日期+时间", 11: "标签", 12: "头像",
|
|
23
|
+
};
|
|
24
|
+
return map[type] || `类型${type}`;
|
|
25
|
+
}
|
|
26
|
+
// ===== 查询员工列表 =====
|
|
27
|
+
export const listStaffSchema = z.object({
|
|
28
|
+
page: z.number().optional().default(1).describe("页码"),
|
|
29
|
+
pageSize: z.number().optional().default(100).describe("每页数量,最大100"),
|
|
30
|
+
usingStatus: z.number().optional().default(1).describe("员工状态:-1=所有, 0=停用, 1=启用"),
|
|
31
|
+
});
|
|
32
|
+
export async function listStaff(client, params) {
|
|
33
|
+
const result = await client.request("/openapi/ent/listStaff", {
|
|
34
|
+
page: params.page,
|
|
35
|
+
pageSize: params.pageSize,
|
|
36
|
+
usingStatus: params.usingStatus,
|
|
37
|
+
recursive: true,
|
|
38
|
+
});
|
|
39
|
+
const data = result.data || result;
|
|
40
|
+
const staffs = data.staffs || [];
|
|
41
|
+
return {
|
|
42
|
+
total: data.total || 0,
|
|
43
|
+
staffs: staffs.map((s) => ({
|
|
44
|
+
accid: s.accid,
|
|
45
|
+
name: s.nick,
|
|
46
|
+
role: formatRole(s.role),
|
|
47
|
+
mobile: s.mobile,
|
|
48
|
+
department: s.depts?.map((d) => d.name).join("/") || "",
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function formatRole(role) {
|
|
53
|
+
const map = {
|
|
54
|
+
1: "企业所有者", 2: "管理员", 3: "销售", 4: "运营", 5: "销售主管",
|
|
55
|
+
};
|
|
56
|
+
return map[role] || `角色${role}`;
|
|
57
|
+
}
|
|
58
|
+
// ===== 查询人群包列表 =====
|
|
59
|
+
export const listSegmentsSchema = z.object({
|
|
60
|
+
keyword: z.string().optional().describe("搜索关键词(ID或名称)"),
|
|
61
|
+
});
|
|
62
|
+
export async function listSegments(client, params) {
|
|
63
|
+
const body = {};
|
|
64
|
+
if (params.keyword)
|
|
65
|
+
body.keyword = params.keyword;
|
|
66
|
+
const result = await client.request("/openapi/cdp/segment/list", body);
|
|
67
|
+
const data = result.data || {};
|
|
68
|
+
const segments = data.contents || data.list || (Array.isArray(data) ? data : []);
|
|
69
|
+
return {
|
|
70
|
+
total: data.total || segments.length,
|
|
71
|
+
segments: segments.map((s) => ({
|
|
72
|
+
id: s.id || s.segmentId,
|
|
73
|
+
name: s.name,
|
|
74
|
+
type: s.type,
|
|
75
|
+
count: s.count || s.customerCount,
|
|
76
|
+
updateTime: s.updateTime ? new Date(s.updateTime).toISOString().slice(0, 10) : "",
|
|
77
|
+
})),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// ===== 创建人群包 =====
|
|
81
|
+
export const createSegmentSchema = z.object({
|
|
82
|
+
name: z.string().describe("人群包名称"),
|
|
83
|
+
description: z.string().optional().describe("描述"),
|
|
84
|
+
});
|
|
85
|
+
export async function createSegment(client, params) {
|
|
86
|
+
const result = await client.request("/openapi/cdp/segment/create", {
|
|
87
|
+
name: params.name,
|
|
88
|
+
description: params.description || "",
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
segmentId: result.data,
|
|
93
|
+
message: `人群包「${params.name}」已创建(ID: ${result.data})`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// ===== 导入用户到人群包 =====
|
|
97
|
+
export const importSegmentSchema = z.object({
|
|
98
|
+
segmentId: z.number().describe("人群包ID"),
|
|
99
|
+
cidList: z.array(z.number()).describe("客户ID列表"),
|
|
100
|
+
accountType: z.number().optional().default(3).describe("账号类型,3=CID"),
|
|
101
|
+
});
|
|
102
|
+
export async function importSegment(client, params) {
|
|
103
|
+
const result = await client.request("/openapi/cdp/segment/import", {
|
|
104
|
+
segmentId: params.segmentId,
|
|
105
|
+
cidList: params.cidList,
|
|
106
|
+
accountType: params.accountType,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
message: `已导入 ${params.cidList.length} 位用户到人群包(ID: ${params.segmentId})`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ===== 查询群发任务列表 =====
|
|
114
|
+
export const listMassSendTasksSchema = z.object({
|
|
115
|
+
pageSize: z.number().optional().default(20).describe("每页数量,最大100"),
|
|
116
|
+
});
|
|
117
|
+
export async function listMassSendTasks(client, params) {
|
|
118
|
+
const result = await client.request("/openapi/wxWork/send/msg/list/v2", {
|
|
119
|
+
pageSize: params.pageSize,
|
|
120
|
+
});
|
|
121
|
+
const tasks = result.data || [];
|
|
122
|
+
return {
|
|
123
|
+
total: tasks.length,
|
|
124
|
+
tasks: tasks.map((t) => ({
|
|
125
|
+
taskId: t.sendTaskId,
|
|
126
|
+
status: formatTaskStatus(t.flag),
|
|
127
|
+
content: t.content?.slice(0, 50) || "",
|
|
128
|
+
createTime: t.createTime ? new Date(t.createTime).toISOString().slice(0, 16).replace("T", " ") : "",
|
|
129
|
+
})),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function formatTaskStatus(flag) {
|
|
133
|
+
const map = { 1: "已开始", 2: "未开始", 3: "已取消" };
|
|
134
|
+
return map[flag] || `状态${flag}`;
|
|
135
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { KeceClient } from "../client.js";
|
|
3
|
+
export declare const createMassSendSchema: z.ZodObject<{
|
|
4
|
+
type: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
5
|
+
wxfollowers: z.ZodArray<z.ZodString, "many">;
|
|
6
|
+
textContent: z.ZodOptional<z.ZodString>;
|
|
7
|
+
medias: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
8
|
+
sendType: z.ZodNumber;
|
|
9
|
+
picUrl: z.ZodOptional<z.ZodString>;
|
|
10
|
+
linkUrl: z.ZodOptional<z.ZodString>;
|
|
11
|
+
linkTitle: z.ZodOptional<z.ZodString>;
|
|
12
|
+
linkContent: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
sendType: number;
|
|
15
|
+
picUrl?: string | undefined;
|
|
16
|
+
linkUrl?: string | undefined;
|
|
17
|
+
linkTitle?: string | undefined;
|
|
18
|
+
linkContent?: string | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
sendType: number;
|
|
21
|
+
picUrl?: string | undefined;
|
|
22
|
+
linkUrl?: string | undefined;
|
|
23
|
+
linkTitle?: string | undefined;
|
|
24
|
+
linkContent?: string | undefined;
|
|
25
|
+
}>, "many">>;
|
|
26
|
+
filterType: z.ZodEnum<["quick", "advanced", "segment"]>;
|
|
27
|
+
filterFields: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
28
|
+
fieldId: z.ZodString;
|
|
29
|
+
filterType: z.ZodNumber;
|
|
30
|
+
filterValue: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, "strip", z.ZodTypeAny, {
|
|
32
|
+
fieldId: string;
|
|
33
|
+
filterType: number;
|
|
34
|
+
filterValue?: string | undefined;
|
|
35
|
+
}, {
|
|
36
|
+
fieldId: string;
|
|
37
|
+
filterType: number;
|
|
38
|
+
filterValue?: string | undefined;
|
|
39
|
+
}>, "many">>;
|
|
40
|
+
segmentIds: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
|
|
41
|
+
sendInterval: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
sendNumOnetime: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
allowAutoSend: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
44
|
+
categoryName: z.ZodOptional<z.ZodString>;
|
|
45
|
+
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
type: number;
|
|
47
|
+
filterType: "quick" | "advanced" | "segment";
|
|
48
|
+
wxfollowers: string[];
|
|
49
|
+
allowAutoSend: boolean;
|
|
50
|
+
filterFields?: {
|
|
51
|
+
fieldId: string;
|
|
52
|
+
filterType: number;
|
|
53
|
+
filterValue?: string | undefined;
|
|
54
|
+
}[] | undefined;
|
|
55
|
+
segmentIds?: number[] | undefined;
|
|
56
|
+
textContent?: string | undefined;
|
|
57
|
+
medias?: {
|
|
58
|
+
sendType: number;
|
|
59
|
+
picUrl?: string | undefined;
|
|
60
|
+
linkUrl?: string | undefined;
|
|
61
|
+
linkTitle?: string | undefined;
|
|
62
|
+
linkContent?: string | undefined;
|
|
63
|
+
}[] | undefined;
|
|
64
|
+
sendInterval?: number | undefined;
|
|
65
|
+
sendNumOnetime?: number | undefined;
|
|
66
|
+
categoryName?: string | undefined;
|
|
67
|
+
}, {
|
|
68
|
+
filterType: "quick" | "advanced" | "segment";
|
|
69
|
+
wxfollowers: string[];
|
|
70
|
+
type?: number | undefined;
|
|
71
|
+
filterFields?: {
|
|
72
|
+
fieldId: string;
|
|
73
|
+
filterType: number;
|
|
74
|
+
filterValue?: string | undefined;
|
|
75
|
+
}[] | undefined;
|
|
76
|
+
segmentIds?: number[] | undefined;
|
|
77
|
+
textContent?: string | undefined;
|
|
78
|
+
medias?: {
|
|
79
|
+
sendType: number;
|
|
80
|
+
picUrl?: string | undefined;
|
|
81
|
+
linkUrl?: string | undefined;
|
|
82
|
+
linkTitle?: string | undefined;
|
|
83
|
+
linkContent?: string | undefined;
|
|
84
|
+
}[] | undefined;
|
|
85
|
+
sendInterval?: number | undefined;
|
|
86
|
+
sendNumOnetime?: number | undefined;
|
|
87
|
+
allowAutoSend?: boolean | undefined;
|
|
88
|
+
categoryName?: string | undefined;
|
|
89
|
+
}>;
|
|
90
|
+
export declare function createMassSend(client: KeceClient, params: z.infer<typeof createMassSendSchema>): Promise<{
|
|
91
|
+
success: boolean;
|
|
92
|
+
message: string;
|
|
93
|
+
taskId?: undefined;
|
|
94
|
+
type?: undefined;
|
|
95
|
+
} | {
|
|
96
|
+
success: boolean;
|
|
97
|
+
taskId: any;
|
|
98
|
+
message: string;
|
|
99
|
+
type: string;
|
|
100
|
+
}>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const mediaSchema = z.object({
|
|
3
|
+
sendType: z.number().describe("发送类型:1=链接, 2=图片, 3=小程序"),
|
|
4
|
+
picUrl: z.string().optional().describe("图片地址"),
|
|
5
|
+
linkUrl: z.string().optional().describe("链接地址"),
|
|
6
|
+
linkTitle: z.string().optional().describe("链接标题"),
|
|
7
|
+
linkContent: z.string().optional().describe("链接描述"),
|
|
8
|
+
});
|
|
9
|
+
const filterFieldSchema = z.object({
|
|
10
|
+
fieldId: z.string().describe("筛选字段ID"),
|
|
11
|
+
filterType: z.number().describe("筛选条件类型"),
|
|
12
|
+
filterValue: z.string().optional().describe("筛选值"),
|
|
13
|
+
});
|
|
14
|
+
export const createMassSendSchema = z.object({
|
|
15
|
+
type: z.number().optional().default(0).describe("任务类型:0=客户群发, 4=RPA群发"),
|
|
16
|
+
wxfollowers: z.array(z.string()).describe("发送人 accid 列表(从 list_staff 获取)"),
|
|
17
|
+
textContent: z.string().optional().describe("文本消息内容(最长4000字符),与 medias 至少有一个"),
|
|
18
|
+
medias: z.array(mediaSchema).optional().describe("附件列表"),
|
|
19
|
+
filterType: z
|
|
20
|
+
.enum(["quick", "advanced", "segment"])
|
|
21
|
+
.describe("人群筛选模式:quick=快捷筛选, advanced=高级筛选, segment=客层筛选"),
|
|
22
|
+
filterFields: z.array(filterFieldSchema).optional().describe("筛选字段(quick/advanced 模式)"),
|
|
23
|
+
segmentIds: z.array(z.number()).optional().describe("客层ID列表(segment 模式)"),
|
|
24
|
+
sendInterval: z.number().optional().describe("发送间隔(分钟)"),
|
|
25
|
+
sendNumOnetime: z.number().optional().describe("每批次发送客户数(不小于1000)"),
|
|
26
|
+
allowAutoSend: z.boolean().optional().default(false).describe("是否自动发送"),
|
|
27
|
+
categoryName: z.string().optional().describe("分类名称"),
|
|
28
|
+
});
|
|
29
|
+
const FILTER_TYPE_MAP = {
|
|
30
|
+
quick: 1,
|
|
31
|
+
advanced: 3,
|
|
32
|
+
segment: 2,
|
|
33
|
+
};
|
|
34
|
+
export async function createMassSend(client, params) {
|
|
35
|
+
if (!params.textContent && !params.medias?.length) {
|
|
36
|
+
return { success: false, message: "文本内容和附件至少提供一个" };
|
|
37
|
+
}
|
|
38
|
+
const queryFilter = { type: FILTER_TYPE_MAP[params.filterType] };
|
|
39
|
+
if (params.filterType === "segment") {
|
|
40
|
+
queryFilter.segmentIds = params.segmentIds || [];
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
queryFilter.filterContent = {
|
|
44
|
+
filterFields: (params.filterFields || []).map((f) => ({
|
|
45
|
+
fieldId: f.fieldId,
|
|
46
|
+
filterType: f.filterType,
|
|
47
|
+
filterValue: f.filterValue || "",
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const body = {
|
|
52
|
+
type: params.type,
|
|
53
|
+
batchDo: true,
|
|
54
|
+
taskType: "task",
|
|
55
|
+
allowAutoSend: params.allowAutoSend,
|
|
56
|
+
allowSelect: false,
|
|
57
|
+
botUseScope: { switchFlag: 0 },
|
|
58
|
+
categoryId: 0,
|
|
59
|
+
cdp: params.filterType === "segment",
|
|
60
|
+
needRemoveDup: false,
|
|
61
|
+
personalizedConfSwitch: 0,
|
|
62
|
+
segmentSplit: false,
|
|
63
|
+
timeType: 1,
|
|
64
|
+
customerType: 1,
|
|
65
|
+
queryFilter,
|
|
66
|
+
welcomeMessage: {
|
|
67
|
+
welcomeMsg: params.textContent || "",
|
|
68
|
+
medias: (params.medias || []).map((m) => ({
|
|
69
|
+
sendType: m.sendType,
|
|
70
|
+
mediaType: 0,
|
|
71
|
+
picUrl: m.picUrl || "",
|
|
72
|
+
linkUrl: m.linkUrl || "",
|
|
73
|
+
linkTitle: m.linkTitle || "",
|
|
74
|
+
linkContent: m.linkContent || "",
|
|
75
|
+
})),
|
|
76
|
+
mediaType: 0,
|
|
77
|
+
sendType: 0,
|
|
78
|
+
},
|
|
79
|
+
wxfollowers: params.wxfollowers,
|
|
80
|
+
};
|
|
81
|
+
if (params.sendInterval)
|
|
82
|
+
body.sendInterval = params.sendInterval;
|
|
83
|
+
if (params.sendNumOnetime)
|
|
84
|
+
body.sendNumOnetime = params.sendNumOnetime;
|
|
85
|
+
if (params.categoryName)
|
|
86
|
+
body.categoryName = params.categoryName;
|
|
87
|
+
const result = await client.request("/openapi/wxWork/send/msg/createByAudience", body);
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
taskId: result.data,
|
|
91
|
+
message: `群发任务已创建(ID: ${result.data})`,
|
|
92
|
+
type: params.type === 4 ? "RPA群发(自动直发)" : "普通群发(需员工确认)",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { KeceClient } from "../client.js";
|
|
3
|
+
export declare const resolveIdsSchema: z.ZodObject<{
|
|
4
|
+
direction: z.ZodEnum<["account_to_cid", "cid_to_account"]>;
|
|
5
|
+
ids: z.ZodArray<z.ZodString, "many">;
|
|
6
|
+
fieldId: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
direction: "account_to_cid" | "cid_to_account";
|
|
9
|
+
ids: string[];
|
|
10
|
+
fieldId?: string | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
direction: "account_to_cid" | "cid_to_account";
|
|
13
|
+
ids: string[];
|
|
14
|
+
fieldId?: string | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function resolveIds(client: KeceClient, params: z.infer<typeof resolveIdsSchema>): Promise<any>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const resolveIdsSchema = z.object({
|
|
3
|
+
direction: z
|
|
4
|
+
.enum(["account_to_cid", "cid_to_account"])
|
|
5
|
+
.describe("转换方向:account_to_cid=游戏账号ID转科策CID, cid_to_account=科策CID转游戏账号ID"),
|
|
6
|
+
ids: z.array(z.string()).describe("要转换的ID列表"),
|
|
7
|
+
fieldId: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("存储游戏账号ID的科策自定义字段ID。不传则使用环境变量 KECE_GAME_ACCOUNT_FIELD_ID"),
|
|
11
|
+
});
|
|
12
|
+
export async function resolveIds(client, params) {
|
|
13
|
+
const fieldId = params.fieldId || process.env.KECE_GAME_ACCOUNT_FIELD_ID;
|
|
14
|
+
if (!fieldId) {
|
|
15
|
+
return {
|
|
16
|
+
success: false,
|
|
17
|
+
message: "未配置游戏账号字段ID。请传入 fieldId 参数,或设置环境变量 KECE_GAME_ACCOUNT_FIELD_ID。可通过 get_customer_fields 查询所有字段找到对应ID。",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (params.direction === "account_to_cid") {
|
|
21
|
+
return resolveAccountToCid(client, params.ids, fieldId);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
return resolveCidToAccount(client, params.ids, fieldId);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function resolveAccountToCid(client, accountIds, fieldId) {
|
|
28
|
+
const results = [];
|
|
29
|
+
for (const accountId of accountIds) {
|
|
30
|
+
const resp = await client.request("/openapi/customer/search", {
|
|
31
|
+
customerType: 1,
|
|
32
|
+
page: 1,
|
|
33
|
+
pageSize: 1,
|
|
34
|
+
queryFilter: {
|
|
35
|
+
type: 1,
|
|
36
|
+
filterContent: {
|
|
37
|
+
filterFields: [{ fieldId, filterType: 5, filterValue: accountId }],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const list = resp.data?.customerList || [];
|
|
42
|
+
results.push({
|
|
43
|
+
accountId,
|
|
44
|
+
cid: list.length > 0 ? list[0].cid : null,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const matched = results.filter((r) => r.cid !== null);
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
total: accountIds.length,
|
|
51
|
+
matched: matched.length,
|
|
52
|
+
unmatched: accountIds.length - matched.length,
|
|
53
|
+
results,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function resolveCidToAccount(client, cids, fieldId) {
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const cid of cids) {
|
|
59
|
+
const resp = await client.request("/openapi/customer/query", {
|
|
60
|
+
type: 3,
|
|
61
|
+
targets: [cid],
|
|
62
|
+
});
|
|
63
|
+
const customers = resp.data || [];
|
|
64
|
+
let accountId = null;
|
|
65
|
+
if (customers.length > 0) {
|
|
66
|
+
const field = (customers[0].customFields || []).find((f) => String(f.id) === fieldId);
|
|
67
|
+
accountId = field?.fieldValue || null;
|
|
68
|
+
}
|
|
69
|
+
results.push({ cid, accountId });
|
|
70
|
+
}
|
|
71
|
+
const matched = results.filter((r) => r.accountId !== null);
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
total: cids.length,
|
|
75
|
+
matched: matched.length,
|
|
76
|
+
unmatched: cids.length - matched.length,
|
|
77
|
+
results,
|
|
78
|
+
};
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kece-skill-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "科策SCRM智能运营 MCP Server - 查客户/打标签/圈人/群发/数数ID打通",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kece-skill-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"dev": "tsx src/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
24
|
+
"zod": "^3.22.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.0.0",
|
|
28
|
+
"tsx": "^4.7.0",
|
|
29
|
+
"typescript": "^5.4.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/README.md
DELETED