@puyinkai/xiaobao-cli 0.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/README.md +81 -0
- package/dist/api-Ccj5dLMo.mjs +100 -0
- package/dist/api-client-DwySN6x-.mjs +75 -0
- package/dist/audio-VmDQMq59.mjs +14 -0
- package/dist/auth-ChHvqjCS.mjs +15 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +30 -0
- package/dist/consultant-ABFTL_jx.mjs +11 -0
- package/dist/customer-DtSnQqy_.mjs +11 -0
- package/dist/device-flow-BgsZipYA.mjs +166 -0
- package/dist/focus-BDYuP2vZ.mjs +11 -0
- package/dist/format-BZvv8lYc.mjs +610 -0
- package/dist/headers-D79npewp.mjs +11 -0
- package/dist/list--o5Q_PI8.mjs +80 -0
- package/dist/list-Bkyi95YO.mjs +76 -0
- package/dist/list-CGACp0y-.mjs +76 -0
- package/dist/list-CVKSGNu4.mjs +91 -0
- package/dist/list-CzB9_Jho.mjs +91 -0
- package/dist/list-DW6KAAaC.mjs +92 -0
- package/dist/list-iwd9cH1I.mjs +41 -0
- package/dist/login-BwkrByMm.mjs +91 -0
- package/dist/logout-BJm9pjje.mjs +49 -0
- package/dist/project-Ba05YzJ5.mjs +14 -0
- package/dist/project-store-Bz5kf-EI.mjs +62 -0
- package/dist/qa-D-B9FV9W.mjs +60 -0
- package/dist/resistance-Cf7zsdB3.mjs +11 -0
- package/dist/text-BLH4R3xv.mjs +49 -0
- package/dist/token-store-CHZ_rJQk.mjs +85 -0
- package/dist/use-DV9Ii3Tn.mjs +61 -0
- package/dist/util-DgwkUfV9.mjs +46 -0
- package/dist/visit-Ct3Vea8I.mjs +11 -0
- package/dist/whoami-DwTHvI4B.mjs +51 -0
- package/package.json +36 -0
- package/skills/wangxiaobao-audio-query/SKILL.md +252 -0
- package/skills/wangxiaobao-audio-wiki/SKILL.md +332 -0
- package/skills/wangxiaobao-customer-focus-query/SKILL.md +213 -0
- package/skills/wangxiaobao-customer-query/SKILL.md +214 -0
- package/skills/wangxiaobao-customer-resistance-query/SKILL.md +211 -0
- package/skills/wangxiaobao-quick-qa/SKILL.md +238 -0
- package/skills/wangxiaobao-switch-project/SKILL.md +179 -0
- package/skills/wangxiaobao-visit-query/SKILL.md +249 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { a as resolveConfig } from "./device-flow-BgsZipYA.mjs";
|
|
2
|
+
import { t as xbApiFetch } from "./api-client-DwySN6x-.mjs";
|
|
3
|
+
import { n as writeResult, t as writeError } from "./format-BZvv8lYc.mjs";
|
|
4
|
+
import { t as requireActiveProject } from "./project-store-Bz5kf-EI.mjs";
|
|
5
|
+
import { t as tenantProjectHeaders } from "./headers-D79npewp.mjs";
|
|
6
|
+
import { defineCommand } from "citty";
|
|
7
|
+
//#region src/commands/qa.ts
|
|
8
|
+
/**
|
|
9
|
+
* `xiaobao-cli qa <prompt>` — 旺小宝业务数据问数.
|
|
10
|
+
*
|
|
11
|
+
* Natural-language Q&A over the active project's data — customers / visits /
|
|
12
|
+
* 挖需率 / 销冠 / 录音覆盖率 / KPIs. Wraps POST /ai-open/langchain/quick-qa/query.
|
|
13
|
+
*
|
|
14
|
+
* Multi-turn: pass back the `thread_id` from previous response via --thread-id
|
|
15
|
+
* for follow-ups. Timeout 30 min (backend can run long multi-step data lookups).
|
|
16
|
+
*/
|
|
17
|
+
var qa_default = defineCommand({
|
|
18
|
+
meta: {
|
|
19
|
+
name: "qa",
|
|
20
|
+
description: "业务数据问数(自然语言)— 客户 / 来访 / 挖需率 / 销冠 / 录音覆盖率 等"
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
prompt: {
|
|
24
|
+
type: "positional",
|
|
25
|
+
required: true,
|
|
26
|
+
description: "用户问题文本"
|
|
27
|
+
},
|
|
28
|
+
"thread-id": {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "多轮 thread id;不传走 stateless run"
|
|
31
|
+
},
|
|
32
|
+
"api-base": { type: "string" },
|
|
33
|
+
"auth-base": { type: "string" },
|
|
34
|
+
format: {
|
|
35
|
+
type: "string",
|
|
36
|
+
default: "json"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async run({ args }) {
|
|
40
|
+
try {
|
|
41
|
+
const config = resolveConfig({
|
|
42
|
+
apiBase: args["api-base"],
|
|
43
|
+
authBase: args["auth-base"]
|
|
44
|
+
});
|
|
45
|
+
const active = await requireActiveProject();
|
|
46
|
+
const body = { prompt: String(args.prompt) };
|
|
47
|
+
if (args["thread-id"]) body.thread_id = args["thread-id"];
|
|
48
|
+
writeResult(await xbApiFetch(config, "POST", "/ai-open/langchain/quick-qa/query", {
|
|
49
|
+
headers: tenantProjectHeaders(active),
|
|
50
|
+
body,
|
|
51
|
+
timeoutMs: 18e5
|
|
52
|
+
}), args.format);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
writeError(err, args.format);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
//#endregion
|
|
60
|
+
export { qa_default as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
//#region src/commands/resistance/index.ts
|
|
3
|
+
var resistance_default = defineCommand({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "resistance",
|
|
6
|
+
description: "客户抗性点标签分页查询"
|
|
7
|
+
},
|
|
8
|
+
subCommands: { list: () => import("./list-CzB9_Jho.mjs").then((m) => m.default) }
|
|
9
|
+
});
|
|
10
|
+
//#endregion
|
|
11
|
+
export { resistance_default as default };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { a as resolveConfig } from "./device-flow-BgsZipYA.mjs";
|
|
2
|
+
import { t as xbApiFetch } from "./api-client-DwySN6x-.mjs";
|
|
3
|
+
import { n as writeResult, t as writeError } from "./format-BZvv8lYc.mjs";
|
|
4
|
+
import { t as requireActiveProject } from "./project-store-Bz5kf-EI.mjs";
|
|
5
|
+
import { t as tenantProjectHeaders } from "./headers-D79npewp.mjs";
|
|
6
|
+
import { defineCommand } from "citty";
|
|
7
|
+
//#region src/commands/audio/text.ts
|
|
8
|
+
/**
|
|
9
|
+
* `xiaobao-cli audio text <audio-id>` — fetch a single recording's transcript
|
|
10
|
+
* via GET /ai-open/audio/text/{audioId}.
|
|
11
|
+
*
|
|
12
|
+
* Returns talkRatios (per-speaker speaking ratios) + texts (segmented ASR
|
|
13
|
+
* with role / beginTime / endTime / text / commentaryNum per item).
|
|
14
|
+
*/
|
|
15
|
+
var text_default = defineCommand({
|
|
16
|
+
meta: {
|
|
17
|
+
name: "text",
|
|
18
|
+
description: "查询单条录音转录文本(含说话人占比 + 分段文本)"
|
|
19
|
+
},
|
|
20
|
+
args: {
|
|
21
|
+
audioId: {
|
|
22
|
+
type: "positional",
|
|
23
|
+
required: true,
|
|
24
|
+
description: "录音 ID(来自 audio list 的 content[].audioId)"
|
|
25
|
+
},
|
|
26
|
+
"api-base": { type: "string" },
|
|
27
|
+
"auth-base": { type: "string" },
|
|
28
|
+
format: {
|
|
29
|
+
type: "string",
|
|
30
|
+
default: "json"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async run({ args }) {
|
|
34
|
+
try {
|
|
35
|
+
const config = resolveConfig({
|
|
36
|
+
apiBase: args["api-base"],
|
|
37
|
+
authBase: args["auth-base"]
|
|
38
|
+
});
|
|
39
|
+
const active = await requireActiveProject();
|
|
40
|
+
const audioId = String(args.audioId);
|
|
41
|
+
writeResult(await xbApiFetch(config, "GET", `/ai-open/audio/text/${encodeURIComponent(audioId)}`, { headers: tenantProjectHeaders(active) }), args.format);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
writeError(err, args.format);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
export { text_default as default };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
//#region src/core/token-store.ts
|
|
6
|
+
/**
|
|
7
|
+
* Persistent token store, file-backed.
|
|
8
|
+
*
|
|
9
|
+
* CLI-specific layout vs the plugin (which stored under workspaceDir):
|
|
10
|
+
*
|
|
11
|
+
* PRIMARY ~/.xiaobao/token.json (writes always here, mode 0600)
|
|
12
|
+
* FALLBACK ~/.openclaw/state/wangxiaobao/token.json (read-only legacy path)
|
|
13
|
+
*
|
|
14
|
+
* The fallback lets openclaw-xiaobao plugin users adopt the CLI without
|
|
15
|
+
* re-running OAuth: first CLI invocation reads the existing plugin token,
|
|
16
|
+
* any subsequent write (login / refresh / logout) lands in ~/.xiaobao/ and
|
|
17
|
+
* the legacy path is left untouched.
|
|
18
|
+
*/
|
|
19
|
+
const PRIMARY_DIR = join(homedir(), ".xiaobao");
|
|
20
|
+
const PRIMARY_TOKEN_FILE = join(PRIMARY_DIR, "token.json");
|
|
21
|
+
const LEGACY_TOKEN_FILE = join(homedir(), ".openclaw", "state", "wangxiaobao", "token.json");
|
|
22
|
+
/**
|
|
23
|
+
* Read order: primary → legacy fallback. Returns null if both missing.
|
|
24
|
+
*/
|
|
25
|
+
async function readToken() {
|
|
26
|
+
for (const f of [PRIMARY_TOKEN_FILE, LEGACY_TOKEN_FILE]) {
|
|
27
|
+
if (!existsSync(f)) continue;
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(f, "utf-8");
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (e.code === "ENOENT") continue;
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
async function writeToken(resp) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const expiresIn = resp.expires_in ?? 7200;
|
|
41
|
+
const tokenInfo = {
|
|
42
|
+
access_token: resp.access_token,
|
|
43
|
+
refresh_token: resp.refresh_token,
|
|
44
|
+
id_token: resp.id_token,
|
|
45
|
+
token_type: resp.token_type || "Bearer",
|
|
46
|
+
scope: resp.scope,
|
|
47
|
+
expires_at: now + expiresIn * 1e3,
|
|
48
|
+
obtained_at: now
|
|
49
|
+
};
|
|
50
|
+
await mkdir(PRIMARY_DIR, {
|
|
51
|
+
recursive: true,
|
|
52
|
+
mode: 448
|
|
53
|
+
});
|
|
54
|
+
await writeFile(PRIMARY_TOKEN_FILE, JSON.stringify(tokenInfo, null, 2), { mode: 384 });
|
|
55
|
+
try {
|
|
56
|
+
await chmod(PRIMARY_TOKEN_FILE, 384);
|
|
57
|
+
} catch {}
|
|
58
|
+
return tokenInfo;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Only deletes the primary file. Legacy openclaw path is left intact so a
|
|
62
|
+
* `xiaobao-cli auth logout` doesn't silently nuke the plugin's token.
|
|
63
|
+
*/
|
|
64
|
+
async function clearToken() {
|
|
65
|
+
try {
|
|
66
|
+
await unlink(PRIMARY_TOKEN_FILE);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e.code !== "ENOENT") throw e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function isAccessTokenValid(token) {
|
|
72
|
+
return Boolean(token?.access_token && token.expires_at > Date.now() + 6e4);
|
|
73
|
+
}
|
|
74
|
+
function decodeJwtPayload(jwt) {
|
|
75
|
+
if (!jwt) return null;
|
|
76
|
+
const parts = jwt.split(".");
|
|
77
|
+
if (parts.length !== 3) return null;
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { writeToken as a, readToken as i, decodeJwtPayload as n, isAccessTokenValid as r, clearToken as t };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { n as writeResult, t as writeError } from "./format-BZvv8lYc.mjs";
|
|
2
|
+
import { n as saveActiveProject } from "./project-store-Bz5kf-EI.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
//#region src/commands/project/use.ts
|
|
5
|
+
/**
|
|
6
|
+
* `xiaobao-cli project use` — persist active tenant/project to
|
|
7
|
+
* ~/.xiaobao/active-project.json (mode 0600). Mirrors openclaw-xiaobao's
|
|
8
|
+
* `xiaobao_switch_project` tool.
|
|
9
|
+
*/
|
|
10
|
+
var use_default = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: "use",
|
|
13
|
+
description: "激活指定租户/项目(先用 `project list` 拿候选)"
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
"tenant-id": {
|
|
17
|
+
type: "string",
|
|
18
|
+
required: true,
|
|
19
|
+
description: "租户 ID"
|
|
20
|
+
},
|
|
21
|
+
"tenant-name": {
|
|
22
|
+
type: "string",
|
|
23
|
+
required: true,
|
|
24
|
+
description: "租户显示名"
|
|
25
|
+
},
|
|
26
|
+
"project-id": {
|
|
27
|
+
type: "string",
|
|
28
|
+
required: true,
|
|
29
|
+
description: "项目 ID(estateId)"
|
|
30
|
+
},
|
|
31
|
+
"project-name": {
|
|
32
|
+
type: "string",
|
|
33
|
+
required: true,
|
|
34
|
+
description: "项目显示名"
|
|
35
|
+
},
|
|
36
|
+
format: {
|
|
37
|
+
type: "string",
|
|
38
|
+
default: "json"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async run({ args }) {
|
|
42
|
+
try {
|
|
43
|
+
const saved = await saveActiveProject({
|
|
44
|
+
tenantId: args["tenant-id"],
|
|
45
|
+
tenantName: args["tenant-name"],
|
|
46
|
+
projectId: args["project-id"],
|
|
47
|
+
projectName: args["project-name"]
|
|
48
|
+
});
|
|
49
|
+
writeResult({
|
|
50
|
+
success: true,
|
|
51
|
+
activeProject: saved,
|
|
52
|
+
message: `已切换到「${saved.tenantName} / ${saved.projectName}」`
|
|
53
|
+
}, args.format);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
writeError(err, args.format);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
//#endregion
|
|
61
|
+
export { use_default as default };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/core/util.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared utilities for CLI commands.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the helpers scattered across openclaw-xiaobao tool files
|
|
6
|
+
* (toLocalDateTime / mergeUserIds), consolidated here so commands stay slim.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Normalize an LLM/human-provided datetime string to the format pierre's
|
|
10
|
+
* Jackson deserializer expects (`yyyy-MM-dd HH:mm:ss`):
|
|
11
|
+
*
|
|
12
|
+
* `2026-05-09T12:34:56` → `2026-05-09 12:34:56`
|
|
13
|
+
* `2026-05-09T12:34:56.789` → `2026-05-09 12:34:56`
|
|
14
|
+
* `2026-05-09T12:34:56+08:00` → `2026-05-09 12:34:56`
|
|
15
|
+
* `2026-05-09 12:34:56` → unchanged
|
|
16
|
+
*/
|
|
17
|
+
function toLocalDateTime(input) {
|
|
18
|
+
if (!input) return input;
|
|
19
|
+
return input.replace("T", " ").replace(/\.\d+/, "").replace(/[+-]\d{2}:?\d{2}$/, "").replace(/Z$/, "").trim();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Merge a single id + a list of ids, de-dup. Returns undefined when empty so
|
|
23
|
+
* the upstream API treats it as "no filter" (not "filter to nothing").
|
|
24
|
+
*/
|
|
25
|
+
function mergeUserIds(single, list) {
|
|
26
|
+
const merged = [];
|
|
27
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28
|
+
const push = (v) => {
|
|
29
|
+
if (!v) return;
|
|
30
|
+
if (!seen.has(v)) {
|
|
31
|
+
seen.add(v);
|
|
32
|
+
merged.push(v);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (single) push(single);
|
|
36
|
+
if (list) for (const v of list) push(v);
|
|
37
|
+
return merged.length > 0 ? merged : void 0;
|
|
38
|
+
}
|
|
39
|
+
/** Split `"a,b,c"` → `["a", "b", "c"]`, trimmed, empties dropped. */
|
|
40
|
+
function csvToArray(input) {
|
|
41
|
+
if (!input) return void 0;
|
|
42
|
+
const arr = input.split(",").map((x) => x.trim()).filter(Boolean);
|
|
43
|
+
return arr.length > 0 ? arr : void 0;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { mergeUserIds as n, toLocalDateTime as r, csvToArray as t };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
//#region src/commands/visit/index.ts
|
|
3
|
+
var visit_default = defineCommand({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "visit",
|
|
6
|
+
description: "客户来访分页查询"
|
|
7
|
+
},
|
|
8
|
+
subCommands: { list: () => import("./list-Bkyi95YO.mjs").then((m) => m.default) }
|
|
9
|
+
});
|
|
10
|
+
//#endregion
|
|
11
|
+
export { visit_default as default };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { i as readToken, n as decodeJwtPayload } from "./token-store-CHZ_rJQk.mjs";
|
|
2
|
+
import { n as writeResult, t as writeError } from "./format-BZvv8lYc.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
//#region src/commands/auth/whoami.ts
|
|
5
|
+
/**
|
|
6
|
+
* `xiaobao-cli auth whoami` — print currently logged-in 旺小宝 user.
|
|
7
|
+
*
|
|
8
|
+
* Pure local op: reads cached id_token (primary then legacy fallback) and
|
|
9
|
+
* decodes the JWT payload. No HTTP call.
|
|
10
|
+
*/
|
|
11
|
+
var whoami_default = defineCommand({
|
|
12
|
+
meta: {
|
|
13
|
+
name: "whoami",
|
|
14
|
+
description: "查看当前登录的旺小宝用户(解析缓存 id_token)"
|
|
15
|
+
},
|
|
16
|
+
args: { format: {
|
|
17
|
+
type: "string",
|
|
18
|
+
default: "json"
|
|
19
|
+
} },
|
|
20
|
+
async run({ args }) {
|
|
21
|
+
try {
|
|
22
|
+
const token = await readToken();
|
|
23
|
+
if (!token) {
|
|
24
|
+
writeResult({
|
|
25
|
+
logged_in: false,
|
|
26
|
+
message: "No token cached. Run `xiaobao-cli auth login` to log in."
|
|
27
|
+
}, args.format);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const claims = decodeJwtPayload(token.id_token);
|
|
31
|
+
writeResult({
|
|
32
|
+
logged_in: true,
|
|
33
|
+
user: claims ? {
|
|
34
|
+
sub: claims.sub,
|
|
35
|
+
name: claims.name,
|
|
36
|
+
phone_number: claims.phone_number,
|
|
37
|
+
email: claims.email,
|
|
38
|
+
preferred_username: claims.preferred_username
|
|
39
|
+
} : null,
|
|
40
|
+
token_expires_at: token.expires_at,
|
|
41
|
+
token_expires_in_seconds: Math.max(0, Math.floor((token.expires_at - Date.now()) / 1e3)),
|
|
42
|
+
scope: token.scope
|
|
43
|
+
}, args.format);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
writeError(err, args.format);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
//#endregion
|
|
51
|
+
export { whoami_default as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@puyinkai/xiaobao-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "旺小宝 CLI — host-agnostic 14 tools (openclaw-xiaobao plugin 的 CLI 等价物)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xiaobao-cli": "./dist/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*.mjs",
|
|
11
|
+
"dist/**/*.d.mts",
|
|
12
|
+
"skills/**/SKILL.md",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"packageManager": "pnpm@10.32.1",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsdown",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"dev": "node --import tsx src/cli.ts"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"citty": "^0.1.6"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@toon-format/toon": "^2.2.0",
|
|
29
|
+
"@types/node": "^25.2.3",
|
|
30
|
+
"tsdown": "^0.21.4",
|
|
31
|
+
"typescript": "^5.9.3"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wangxiaobao-audio-query
|
|
3
|
+
description: |
|
|
4
|
+
旺小宝录音只读查询技能。按时间 / 销售 / 翻页查询录音元数据列表,可选地抽样取单条转录文本预览。**不写文件、不动游标**——是 `wangxiaobao-audio-wiki` 的"探查 / 抽样"版。
|
|
5
|
+
|
|
6
|
+
**当以下情况时使用此 Skill**:
|
|
7
|
+
(1) 用户提到"看看有哪些录音"、"查录音"、"列录音"、"哪些销售有几条录音"、"录音多不多"
|
|
8
|
+
(2) 用户想抽样几条录音文本预览,但不想批量入库
|
|
9
|
+
(3) 用户在 sync 之前想估个量、看一下数据形状
|
|
10
|
+
(4) 用户问"张三/某销售本周打了几个电话"、"上周末有几条客户对话"
|
|
11
|
+
(5) 任何"看一眼录音"而**不**触发"同步 / 入库 / 归档"意图的场景
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
> **Host-agnostic CLI skill** — 本 skill 假设 `xiaobao-cli` 已装到 PATH
|
|
15
|
+
> (`npm i -g @puyinkai/xiaobao-cli` 或 `npx -y @puyinkai/xiaobao-cli`)。
|
|
16
|
+
> Agent 通过 shell 工具(Bash / Run / Shell)执行命令、读 **stdout JSON** 消费;
|
|
17
|
+
> stderr 是进度/错误提示。退出码 0 = 成功,非 0 = 业务/网络错(错误对象同时打到 stdout 可解析)。
|
|
18
|
+
>
|
|
19
|
+
> CLI 14 个子命令跟 openclaw-xiaobao plugin 14 个 tool **1:1 等价**,返回 JSON
|
|
20
|
+
> 结构完全一致(`{status, ok, data: {...}}`)。skill 里看到的 `resp.data.data.xxx`
|
|
21
|
+
> 取数路径直接对 stdout JSON 用 `jq` / `JSON.parse` 即可。
|
|
22
|
+
>
|
|
23
|
+
> **plugin tool → CLI 命令翻译表(数组参数走逗号分隔)**:
|
|
24
|
+
>
|
|
25
|
+
> | plugin tool | CLI 命令 |
|
|
26
|
+
> | --- | --- |
|
|
27
|
+
> | `xiaobao_authorize { force? }` | `xiaobao-cli auth login [--force]` |
|
|
28
|
+
> | `xiaobao_whoami` | `xiaobao-cli auth whoami` |
|
|
29
|
+
> | `xiaobao_logout` | `xiaobao-cli auth logout` |
|
|
30
|
+
> | `xiaobao_list_projects { keyword? }` | `xiaobao-cli project list [--keyword <kw>]` |
|
|
31
|
+
> | `xiaobao_switch_project { tenantId, tenantName, projectId, projectName }` | `xiaobao-cli project use --tenant-id ... --tenant-name ... --project-id ... --project-name ...` |
|
|
32
|
+
> | `xiaobao_list_consultants` | `xiaobao-cli consultant list` |
|
|
33
|
+
> | `xiaobao_list_audio { fromDate, toDate, userId?, userIdList?, page, size }` | `xiaobao-cli audio list --from "..." --to "..." [--user-id ...] [--user-id-list a,b,c] [--page N] [--size N]` |
|
|
34
|
+
> | `xiaobao_get_audio_text { audioId }` | `xiaobao-cli audio text <audioId>` |
|
|
35
|
+
> | `xiaobao_list_customers { ... }` | `xiaobao-cli customer list [--user-id] [--user-name] [--customer-name] [--customer-phone] [--portrait] [--from] [--to] [--page] [--size]` |
|
|
36
|
+
> | `xiaobao_list_visits { ... }` | `xiaobao-cli visit list [--customer-id] [--customer-name] [--from] [--to] [--page] [--size]` |
|
|
37
|
+
> | `xiaobao_list_customer_focus { visitIds, customerIds, audioIds, category, classification, fromDate, toDate, ... }` | `xiaobao-cli focus list [--visit-ids a,b] [--customer-ids a,b] [--audio-ids a,b] [--category ...] [--classification ...] [--from ...] [--to ...]` |
|
|
38
|
+
> | `xiaobao_list_customer_resistance { ... }` | `xiaobao-cli resistance list [同 focus]` |
|
|
39
|
+
> | `xiaobao_quick_qa { prompt, threadId? }` | `xiaobao-cli qa "<prompt>" [--thread-id ...]` |
|
|
40
|
+
> | `xiaobao_api { method, path, query, body, headers }` | `xiaobao-cli api <METHOD> <PATH> [--query k=v] [--body '<json>'] [--headers k=v]` |
|
|
41
|
+
>
|
|
42
|
+
> 用 `--format toon` 切到 TOON(uniform 数组省 30-50% token,LLM 上下文优化);
|
|
43
|
+
> 用 `--format json`(默认)保持 JSON。state 路径:`~/.xiaobao/`(fallback 读 `~/.openclaw/state/wangxiaobao/`)。
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 旺小宝录音查询
|
|
47
|
+
|
|
48
|
+
只读探查录音。**没有副作用**——不写 wiki / 不推游标,
|
|
49
|
+
所以用错也没成本,鼓励放心调用。
|
|
50
|
+
|
|
51
|
+
> 想要"完整入库流程"(拉全部文本 → 写 wiki/raw/audio/ → 推进游标),切到
|
|
52
|
+
> `wangxiaobao-audio-wiki` skill。本 skill 跟那个的边界就是**有没有副作用**。
|
|
53
|
+
|
|
54
|
+
## 执行前必读
|
|
55
|
+
|
|
56
|
+
- 必须有有效 token:先调 `xiaobao_whoami`,未登录 / 过期就调 `xiaobao_authorize`
|
|
57
|
+
- **必须有激活项目**:tenant/project 由 tool 内部从
|
|
58
|
+
`~/.openclaw/state/wangxiaobao/active-project.json` 读,调用方不需要传任何
|
|
59
|
+
tenant/project 参数。如果 tool 返回 `error: 'NO_ACTIVE_PROJECT'`,
|
|
60
|
+
让用户跑 `wangxiaobao-switch-project` skill 后再重试
|
|
61
|
+
- **绝不要**写文件、推游标。即使用户随口说"顺便存一下"也要拦:
|
|
62
|
+
那是 `wangxiaobao-audio-wiki` 的事,本 skill 一旦写盘就破坏了"只读"承诺
|
|
63
|
+
- LocalDateTime 字段格式:`yyyy-MM-dd HH:mm:ss`(**空格**分隔,不带时区)。
|
|
64
|
+
pierre 框架的 Jackson 反序列化只认这个格式;带 `T` 或 `+08:00` 会被 plugin
|
|
65
|
+
容错处理,但传空格形式最稳
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 快速索引:意图 → 工具
|
|
70
|
+
|
|
71
|
+
| 用户意图 | plugin tool | 关键参数 |
|
|
72
|
+
| ---------------------- | -------------------------- | ----------------------------------------- |
|
|
73
|
+
| 列某时段所有录音元数据 | `xiaobao_list_audio` | `fromDate` / `toDate` |
|
|
74
|
+
| 列某销售的录音 | `xiaobao_list_audio` | `+ userId: <uid>`(多个销售用 `userIdList`) |
|
|
75
|
+
| 反查销售 user-id | `xiaobao_list_consultants` | 无参(拿当前用户授权范围内的所有顾问) |
|
|
76
|
+
| 看总数 / 估个量 | `xiaobao_list_audio` | `page: 1, size: 1`,只读 `total` |
|
|
77
|
+
| 翻页继续看 | `xiaobao_list_audio` | `page += 1` |
|
|
78
|
+
| 看某条录音转录预览 | `xiaobao_get_audio_text` | `audioId`(从 list_audio 结果拿) |
|
|
79
|
+
|
|
80
|
+
**tool 参数里没有** `tenantId` / `projectId` —— plugin 内部从激活项目状态读。
|
|
81
|
+
如果还没激活过项目,跑 `wangxiaobao-switch-project` skill 先设一次。
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 核心约束
|
|
86
|
+
|
|
87
|
+
### 1. 时间范围:用户没明确说就推断合理窗口
|
|
88
|
+
|
|
89
|
+
当用户没给具体时间,按用户的话推断一个**能覆盖意图**的窗口,而不是默认拉
|
|
90
|
+
最近 N 天造成超时。常见映射:
|
|
91
|
+
|
|
92
|
+
| 用户说法 | 解析 |
|
|
93
|
+
| -------------- | ----------------------------------------------------- |
|
|
94
|
+
| "今天的录音" | from = 今天 00:00:00, to = now |
|
|
95
|
+
| "上周二的录音" | from = 上周二 00:00:00, to = 上周三 00:00:00 |
|
|
96
|
+
| "5 月份的" | from = 2026-05-01 00:00:00, to = 2026-06-01 00:00:00 |
|
|
97
|
+
| "最近几条" | from = now - 1 day, to = now, size = 10 |
|
|
98
|
+
|
|
99
|
+
用户明确给时间时直接用,不要二次猜测。
|
|
100
|
+
|
|
101
|
+
### 2. 分页:page 从 **1** 开始
|
|
102
|
+
|
|
103
|
+
PageParam 默认 `page: 1, size: 10`,传 `page: 0` 上游会报错。读 `total` +
|
|
104
|
+
当前 `page * size` 判断是否还有下一页,需要时翻页;浏览概览时第 1 页通常
|
|
105
|
+
够用。`size` 上限 500(schema 已限制),预览展示建议 20。
|
|
106
|
+
|
|
107
|
+
### 3. 按销售筛选:先反查 user-id 再过滤
|
|
108
|
+
|
|
109
|
+
不带 `userId` / `userIdList` 就拉**所有人**的录音。要筛某个销售:
|
|
110
|
+
|
|
111
|
+
- 用户能直接给 user-id(数字)→ 用 `userId: <id>`(单个)或
|
|
112
|
+
`userIdList: [<id1>, <id2>, ...]`(多个)
|
|
113
|
+
- 只知道名字 → 调 **`xiaobao_list_consultants`**(无参),拿到当前用户
|
|
114
|
+
授权范围内的所有顾问(含 `userName` / `userId`),按名字 match 后取 `userId`
|
|
115
|
+
传给 `xiaobao_list_audio`
|
|
116
|
+
|
|
117
|
+
> 已弃用做法:"先 list_audio 不带 filter,再扫 content[].saleName 反查"——
|
|
118
|
+
> 这种 hack 在录音数据稀疏 / 时间窗短时找不到目标销售。**改用
|
|
119
|
+
> `xiaobao_list_consultants` 反查**,精确且快。
|
|
120
|
+
|
|
121
|
+
### 4. 单条文本预览:按需调,不要全量
|
|
122
|
+
|
|
123
|
+
`xiaobao_get_audio_text` 一次拿一条录音的转录。**只在用户挑了具体某条之
|
|
124
|
+
后再调**,不要 list 完顺手把每条都 get_audio_text 一遍——那是 sync skill
|
|
125
|
+
的事。一次会话 1-3 条样本预览足够。
|
|
126
|
+
|
|
127
|
+
### 5. 响应外层结构
|
|
128
|
+
|
|
129
|
+
`xiaobao_list_audio` 和 `xiaobao_get_audio_text` 都返回 pierre 框架的
|
|
130
|
+
`Result<T>` 包:
|
|
131
|
+
|
|
132
|
+
```jsonc
|
|
133
|
+
{
|
|
134
|
+
"code": "0", // 业务状态码,"0" 是成功
|
|
135
|
+
"msg": "success",
|
|
136
|
+
"data": { ... } // 真正的 payload
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
list_audio 里 `data` 又是 `PageResult<AudioPageResp>`:
|
|
141
|
+
`{ page, size, total, content[] }`,所以**取元数据数组**要写
|
|
142
|
+
`resp.data.data.content`(plugin tool 不展开 envelope,原样返回)。
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 使用场景示例
|
|
147
|
+
|
|
148
|
+
### 场景 1:列今天所有录音
|
|
149
|
+
|
|
150
|
+
```jsonc
|
|
151
|
+
// xiaobao_list_audio
|
|
152
|
+
{
|
|
153
|
+
"fromDate": "2026-05-12 00:00:00",
|
|
154
|
+
"toDate": "2026-05-12 23:59:59",
|
|
155
|
+
"page": 1,
|
|
156
|
+
"size": 20
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
渲染给用户(按 startTime desc,audioId 必须显示——后面取文本要用):
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
共 145 条录音(第 1/8 页)
|
|
164
|
+
|
|
165
|
+
1. 张三 · 2026-05-12 10:23:11 ~ 10:35:42(12 分钟) · audioId=12345
|
|
166
|
+
2. 李四 · 2026-05-12 09:50:00 ~ 10:02:15(12 分钟) · audioId=12344
|
|
167
|
+
3. 王五 · 2026-05-12 09:10:00 ~ 09:25:11(15 分钟) · audioId=12343
|
|
168
|
+
...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 场景 2:估算"上周末打了几个电话"
|
|
172
|
+
|
|
173
|
+
只关心 `total`,第 1 页就够:
|
|
174
|
+
|
|
175
|
+
```jsonc
|
|
176
|
+
{
|
|
177
|
+
"fromDate": "2026-05-10 00:00:00",
|
|
178
|
+
"toDate": "2026-05-12 00:00:00",
|
|
179
|
+
"page": 1,
|
|
180
|
+
"size": 1
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
回复:「周末两天共 23 条录音,主要集中在周日下午」。
|
|
185
|
+
|
|
186
|
+
### 场景 3:取某条录音的转录预览
|
|
187
|
+
|
|
188
|
+
用户从场景 1 列表里挑了序号 1(audioId=12345):
|
|
189
|
+
|
|
190
|
+
```jsonc
|
|
191
|
+
// xiaobao_get_audio_text
|
|
192
|
+
{ "audioId": 12345 }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
渲染:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
audioId 12345 · 张三 · 2026-05-12 10:23:11(12 分钟)
|
|
199
|
+
说话人占比:销售 62% / 客户 38%
|
|
200
|
+
|
|
201
|
+
转录:
|
|
202
|
+
[10:23:15] 销售:您好张总,我是旺小宝的小张,咱们那个项目...
|
|
203
|
+
[10:23:22] 客户:嗯嗯你说
|
|
204
|
+
[10:23:25] 销售:周末方便过来看下样板间吗?
|
|
205
|
+
...
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 场景 4:按销售名字筛 —— 用 list-consultants 反查
|
|
209
|
+
|
|
210
|
+
用户说"张三本周打了几个"——只知道名字:
|
|
211
|
+
|
|
212
|
+
1. 调 `xiaobao_list_consultants`(无参)→ 拿到 `[{ userId, userName, ... }, ...]`
|
|
213
|
+
2. 在结果里找 `userName === "张三"` 的 `userId`
|
|
214
|
+
3. 调 `xiaobao_list_audio` 带 `userId: <张三的 userId>` + 本周时间窗
|
|
215
|
+
|
|
216
|
+
```jsonc
|
|
217
|
+
// step 1
|
|
218
|
+
// xiaobao_list_consultants
|
|
219
|
+
{}
|
|
220
|
+
|
|
221
|
+
// step 2 ——找到 张三 的 userId = 100
|
|
222
|
+
// step 3
|
|
223
|
+
// xiaobao_list_audio
|
|
224
|
+
{
|
|
225
|
+
"fromDate": "2026-05-06 00:00:00",
|
|
226
|
+
"toDate": "2026-05-13 00:00:00",
|
|
227
|
+
"userId": 100,
|
|
228
|
+
"page": 1, "size": 20
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
如果 list-consultants 里**找不到张三**——说明当前登录用户没权限看张三
|
|
233
|
+
(不在管理范围内 / 不在团队里)。提示用户"当前账号无权访问销售『张三』",
|
|
234
|
+
不要硬调 list-audio 假装搜(也会因 dataPermission 拿不到)。
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 常见错误与排查
|
|
239
|
+
|
|
240
|
+
- **`code: "400"` / `msg: "消息转换异常"`** — fromDate/toDate 格式不对。
|
|
241
|
+
必须 `yyyy-MM-dd HH:mm:ss` 空格分隔,不带时区。
|
|
242
|
+
- **401 / token 过期** — 调 `xiaobao_authorize { force: true }` 重登,重试一次。
|
|
243
|
+
- **`total: 0`** — 时间窗口里确实没数据,或激活的租户/项目错了。
|
|
244
|
+
让用户跑 `wangxiaobao-switch-project` skill 确认或重新激活项目。
|
|
245
|
+
- **`error: 'NO_ACTIVE_PROJECT'`** — 还没设过激活项目。让用户跑
|
|
246
|
+
`wangxiaobao-switch-project` skill 后重试本 skill。
|
|
247
|
+
- **翻页拿不到下一页** — 检查 `page` 是不是从 1 开始(不是 0),
|
|
248
|
+
`page * size > total` 时已经到末页。
|
|
249
|
+
- **`saleName` 是空的** — 上游 user info 缺失,可以只用 `userId` 标识,
|
|
250
|
+
跟用户说"该录音的销售信息上游缺失"。
|
|
251
|
+
- **想"批量转文本看看"** — 拦住:批量取文本是 `wangxiaobao-audio-wiki`
|
|
252
|
+
的事(带写文件 + 推游标),本 skill 设计上每次会话最多预览 1-3 条。
|