@puyinkai/xiaobao-cli 0.1.8 → 0.1.9

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.
Files changed (47) hide show
  1. package/dist/{api-client-zDBFgk0y.mjs → api-client-CDNjkArS.mjs} +1 -1
  2. package/dist/{api-CX99_4K7.mjs → api-fsHHWWa3.mjs} +1 -1
  3. package/dist/{audio-ShfvRomP.mjs → audio-CRSMqaKo.mjs} +2 -2
  4. package/dist/{auth-DpK81Vp5.mjs → auth-_3hBh87U.mjs} +1 -1
  5. package/dist/cli.mjs +10 -10
  6. package/dist/{consultant-CS9B42d3.mjs → consultant-E33hTGUO.mjs} +1 -1
  7. package/dist/{customer-B1cxRbJ9.mjs → customer-DcQ83oKD.mjs} +1 -1
  8. package/dist/{focus-DfNPPhm5.mjs → focus-XMRxnHPw.mjs} +1 -1
  9. package/dist/{list-DI1OD95x.mjs → list-B3nN4pd2.mjs} +1 -1
  10. package/dist/{list-CD08BRLk.mjs → list-CXquxzex.mjs} +1 -1
  11. package/dist/{list-CKlx8YuL.mjs → list-Cc4zcaNz.mjs} +1 -1
  12. package/dist/{list-DCcCS6LM.mjs → list-CnlQdzPp.mjs} +1 -1
  13. package/dist/{list-D56gEqiB.mjs → list-CrMOrnS-.mjs} +1 -1
  14. package/dist/{list-CnlFIFiY.mjs → list-DFdDcHFq.mjs} +1 -1
  15. package/dist/{list-Cu6WzPpA.mjs → list-dm9YaCG1.mjs} +1 -1
  16. package/dist/{project-BuQQewOs.mjs → project-BD0g4oAW.mjs} +1 -1
  17. package/dist/{qa-CKsKiQIm.mjs → qa-CdMcxCE_.mjs} +1 -1
  18. package/dist/{resistance-DsE-Ha6Y.mjs → resistance-6_V6At3T.mjs} +1 -1
  19. package/dist/{text-C3jhdPoe.mjs → text-C3Ap6L5n.mjs} +1 -1
  20. package/dist/{visit-CX0Mc1n6.mjs → visit-DoeECi8w.mjs} +1 -1
  21. package/dist/{whoami-CZVez-rm.mjs → whoami-z-76WaCP.mjs} +1 -1
  22. package/package.json +5 -1
  23. package/skills/wangxiaobao-audio-query/SKILL.md +2 -2
  24. package/skills/wangxiaobao-audio-wiki/SKILL.md +4 -4
  25. package/skills/wangxiaobao-customer-focus-query/SKILL.md +2 -2
  26. package/skills/wangxiaobao-customer-query/SKILL.md +2 -2
  27. package/skills/wangxiaobao-customer-resistance-query/SKILL.md +2 -2
  28. package/skills/wangxiaobao-html-to-pdf/SKILL.md +116 -0
  29. package/skills/wangxiaobao-html-to-pdf/scripts/html2pdf.py +132 -0
  30. package/skills/wangxiaobao-html-to-pdf/scripts/html2pdf_exact.py +92 -0
  31. package/skills/wangxiaobao-project-daily-report/SKILL.md +170 -0
  32. package/skills/wangxiaobao-project-daily-report/_meta.json +9 -0
  33. package/skills/wangxiaobao-project-daily-report/references/report_template.html +308 -0
  34. package/skills/wangxiaobao-project-daily-report/scripts/fetch_data.py +219 -0
  35. package/skills/wangxiaobao-project-daily-report/scripts/generate_report.py +1029 -0
  36. package/skills/wangxiaobao-project-daily-report/scripts/run.py +34 -0
  37. package/skills/wangxiaobao-project-weekly-report/SKILL.md +169 -0
  38. package/skills/wangxiaobao-project-weekly-report/scripts/generate_report.py +308 -0
  39. package/skills/wangxiaobao-project-weekly-report/scripts/html2pdf_fast.py +161 -0
  40. package/skills/wangxiaobao-project-weekly-report/scripts/logo_wangai.png +0 -0
  41. package/skills/wangxiaobao-project-weekly-report/scripts/logo_wangxiaobao.png +0 -0
  42. package/skills/wangxiaobao-project-weekly-report/scripts/prepare_data.py +682 -0
  43. package/skills/wangxiaobao-project-weekly-report/scripts/template.py +925 -0
  44. package/skills/wangxiaobao-quick-qa/SKILL.md +2 -2
  45. package/skills/wangxiaobao-shared/SKILL.md +5 -5
  46. package/skills/wangxiaobao-switch-project/SKILL.md +3 -3
  47. package/skills/wangxiaobao-visit-query/SKILL.md +2 -2
@@ -13,7 +13,7 @@ import { a as isAccessTokenValid, l as writeToken, s as readToken } from "./toke
13
13
  */
14
14
  var NotAuthenticatedError = class extends Error {
15
15
  code = "NOT_AUTHENTICATED";
16
- hint = "运行 `xiaobao-cli auth login` 完成 OAuth device flow 登录。";
16
+ hint = "运行 `xiaobao-cli auth login --no-wait` 发起登录,授权后运行 `xiaobao-cli auth login --resume` 完成。";
17
17
  constructor() {
18
18
  super("当前未登录或 token 已过期");
19
19
  this.name = "NotAuthenticatedError";
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { t as loadActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -6,8 +6,8 @@ var audio_default = defineCommand({
6
6
  description: "录音元数据分页 / 单条文本查询"
7
7
  },
8
8
  subCommands: {
9
- list: () => import("./list-DCcCS6LM.mjs").then((m) => m.default),
10
- text: () => import("./text-C3jhdPoe.mjs").then((m) => m.default)
9
+ list: () => import("./list-CnlQdzPp.mjs").then((m) => m.default),
10
+ text: () => import("./text-C3Ap6L5n.mjs").then((m) => m.default)
11
11
  }
12
12
  });
13
13
  //#endregion
@@ -7,7 +7,7 @@ var auth_default = defineCommand({
7
7
  },
8
8
  subCommands: {
9
9
  login: () => import("./login-CUUcjWPd.mjs").then((m) => m.default),
10
- whoami: () => import("./whoami-CZVez-rm.mjs").then((m) => m.default),
10
+ whoami: () => import("./whoami-z-76WaCP.mjs").then((m) => m.default),
11
11
  logout: () => import("./logout-DP7REhCd.mjs").then((m) => m.default)
12
12
  }
13
13
  });
package/dist/cli.mjs CHANGED
@@ -18,16 +18,16 @@ runMain(defineCommand({
18
18
  description: "旺小宝 CLI — host-agnostic 14 tools (openclaw-xiaobao plugin 的 CLI 等价物)"
19
19
  },
20
20
  subCommands: {
21
- auth: () => import("./auth-DpK81Vp5.mjs").then((m) => m.default),
22
- project: () => import("./project-BuQQewOs.mjs").then((m) => m.default),
23
- consultant: () => import("./consultant-CS9B42d3.mjs").then((m) => m.default),
24
- audio: () => import("./audio-ShfvRomP.mjs").then((m) => m.default),
25
- customer: () => import("./customer-B1cxRbJ9.mjs").then((m) => m.default),
26
- visit: () => import("./visit-CX0Mc1n6.mjs").then((m) => m.default),
27
- focus: () => import("./focus-DfNPPhm5.mjs").then((m) => m.default),
28
- resistance: () => import("./resistance-DsE-Ha6Y.mjs").then((m) => m.default),
29
- qa: () => import("./qa-CKsKiQIm.mjs").then((m) => m.default),
30
- api: () => import("./api-CX99_4K7.mjs").then((m) => m.default)
21
+ auth: () => import("./auth-_3hBh87U.mjs").then((m) => m.default),
22
+ project: () => import("./project-BD0g4oAW.mjs").then((m) => m.default),
23
+ consultant: () => import("./consultant-E33hTGUO.mjs").then((m) => m.default),
24
+ audio: () => import("./audio-CRSMqaKo.mjs").then((m) => m.default),
25
+ customer: () => import("./customer-DcQ83oKD.mjs").then((m) => m.default),
26
+ visit: () => import("./visit-DoeECi8w.mjs").then((m) => m.default),
27
+ focus: () => import("./focus-XMRxnHPw.mjs").then((m) => m.default),
28
+ resistance: () => import("./resistance-6_V6At3T.mjs").then((m) => m.default),
29
+ qa: () => import("./qa-CdMcxCE_.mjs").then((m) => m.default),
30
+ api: () => import("./api-fsHHWWa3.mjs").then((m) => m.default)
31
31
  }
32
32
  }));
33
33
  //#endregion
@@ -5,7 +5,7 @@ var consultant_default = defineCommand({
5
5
  name: "consultant",
6
6
  description: "查询有权限的置业顾问列表"
7
7
  },
8
- subCommands: { list: () => import("./list-Cu6WzPpA.mjs").then((m) => m.default) }
8
+ subCommands: { list: () => import("./list-dm9YaCG1.mjs").then((m) => m.default) }
9
9
  });
10
10
  //#endregion
11
11
  export { consultant_default as default };
@@ -5,7 +5,7 @@ var customer_default = defineCommand({
5
5
  name: "customer",
6
6
  description: "客户画像分页查询"
7
7
  },
8
- subCommands: { list: () => import("./list-CnlFIFiY.mjs").then((m) => m.default) }
8
+ subCommands: { list: () => import("./list-DFdDcHFq.mjs").then((m) => m.default) }
9
9
  });
10
10
  //#endregion
11
11
  export { customer_default as default };
@@ -5,7 +5,7 @@ var focus_default = defineCommand({
5
5
  name: "focus",
6
6
  description: "客户关注点标签分页查询"
7
7
  },
8
- subCommands: { list: () => import("./list-D56gEqiB.mjs").then((m) => m.default) }
8
+ subCommands: { list: () => import("./list-CrMOrnS-.mjs").then((m) => m.default) }
9
9
  });
10
10
  //#endregion
11
11
  export { focus_default as default };
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  //#region src/commands/project/list.ts
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as requireActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as requireActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as requireActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as requireActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as requireActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as requireActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -6,7 +6,7 @@ var project_default = defineCommand({
6
6
  description: "查询 / 切换激活项目"
7
7
  },
8
8
  subCommands: {
9
- list: () => import("./list-DI1OD95x.mjs").then((m) => m.default),
9
+ list: () => import("./list-B3nN4pd2.mjs").then((m) => m.default),
10
10
  use: () => import("./use-BZES4FZC.mjs").then((m) => m.default),
11
11
  current: () => import("./current-CfDExInT.mjs").then((m) => m.default)
12
12
  }
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as requireActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -5,7 +5,7 @@ var resistance_default = defineCommand({
5
5
  name: "resistance",
6
6
  description: "客户抗性点标签分页查询"
7
7
  },
8
- subCommands: { list: () => import("./list-CKlx8YuL.mjs").then((m) => m.default) }
8
+ subCommands: { list: () => import("./list-Cc4zcaNz.mjs").then((m) => m.default) }
9
9
  });
10
10
  //#endregion
11
11
  export { resistance_default as default };
@@ -1,5 +1,5 @@
1
1
  import { a as resolveConfig } from "./device-flow-D6JVcEap.mjs";
2
- import { t as xbApiFetch } from "./api-client-zDBFgk0y.mjs";
2
+ import { t as xbApiFetch } from "./api-client-CDNjkArS.mjs";
3
3
  import { n as requireActiveProject } from "./project-store-CKVJa54T.mjs";
4
4
  import { t as tenantProjectHeaders } from "./headers-DSnR4sjk.mjs";
5
5
  import { n as writeResult, t as writeError } from "./format-yjmxrbNm.mjs";
@@ -5,7 +5,7 @@ var visit_default = defineCommand({
5
5
  name: "visit",
6
6
  description: "客户来访分页查询"
7
7
  },
8
- subCommands: { list: () => import("./list-CD08BRLk.mjs").then((m) => m.default) }
8
+ subCommands: { list: () => import("./list-CXquxzex.mjs").then((m) => m.default) }
9
9
  });
10
10
  //#endregion
11
11
  export { visit_default as default };
@@ -23,7 +23,7 @@ var whoami_default = defineCommand({
23
23
  if (!token) {
24
24
  writeResult({
25
25
  logged_in: false,
26
- message: "No token cached. Run `xiaobao-cli auth login` to log in."
26
+ message: "No token cached. Run `xiaobao-cli auth login --no-wait` then `xiaobao-cli auth login --resume` to log in."
27
27
  }, args.format);
28
28
  return;
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puyinkai/xiaobao-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "旺小宝 CLI — host-agnostic 14 tools (openclaw-xiaobao plugin 的 CLI 等价物)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,10 @@
10
10
  "dist/**/*.mjs",
11
11
  "dist/**/*.d.mts",
12
12
  "skills/**/*.md",
13
+ "skills/**/*.py",
14
+ "skills/**/*.html",
15
+ "skills/**/*.png",
16
+ "skills/**/*.json",
13
17
  "README.md"
14
18
  ],
15
19
  "engines": {
@@ -17,7 +17,7 @@ metadata:
17
17
 
18
18
  ## 执行前必读
19
19
 
20
- - 必须有有效 token:先调 `xiaobao-cli auth whoami`,未登录 / 过期就调 `xiaobao-cli auth login`
20
+ - 必须有有效 token:先调 `xiaobao-cli auth whoami`,未登录 / 过期就走 `auth login --no-wait` split-flow(见 wangxiaobao-shared)
21
21
  - **必须有激活项目**:tenant/project 由 命令内部从
22
22
  `~/.xiaobao/active-project.json` 读,调用方不需要传任何
23
23
  tenant/project 参数。如果 命令 返回 `error: 'NO_ACTIVE_PROJECT'`,
@@ -184,7 +184,7 @@ xiaobao-cli audio list --from "2026-05-06 00:00:00" --to "2026-05-13 00:00:00" -
184
184
 
185
185
  - **`code: "400"` / `msg: "消息转换异常"`** — fromDate/toDate 格式不对。
186
186
  必须 `yyyy-MM-dd HH:mm:ss` 空格分隔,不带时区。
187
- - **401 / token 过期** — `xiaobao-cli auth login --force` 重登,重试一次。
187
+ - **401 / token 过期** — `auth login --no-wait --force` split-flow 重登,重试一次。
188
188
  - **`total: 0`** — 时间窗口里确实没数据,或激活的租户/项目错了。
189
189
  让用户跑 `wangxiaobao-switch-project` skill 确认或重新激活项目。
190
190
  - **`error: 'NO_ACTIVE_PROJECT'`** — 还没设过激活项目。让用户跑
@@ -25,7 +25,7 @@ Layer 2 知识页**。
25
25
 
26
26
  ## 执行前必读
27
27
 
28
- 1. **登录**:先调 `xiaobao-cli auth whoami`;未登录 / 过期就调 `xiaobao-cli auth login`。
28
+ 1. **登录**:先调 `xiaobao-cli auth whoami`;未登录 / 过期就走 `auth login --no-wait` split-flow(见 wangxiaobao-shared)。
29
29
  2. **激活项目**:命令内部从 `~/.xiaobao/active-project.json`
30
30
  读 tenant/project。如果任何 命令 返回 `error: 'NO_ACTIVE_PROJECT'`,
31
31
  先走 `wangxiaobao-switch-project` skill 让用户选。
@@ -84,7 +84,7 @@ for each window [winStart, winEnd):
84
84
  否则 page += 1
85
85
  ```
86
86
 
87
- 若 命令 返回 401 → `xiaobao-cli auth login --force` 后重试当前窗口
87
+ 若 命令 返回 401 → `auth login --no-wait --force` split-flow 重登后重试当前窗口
88
88
  (最多重试一次)。
89
89
 
90
90
  ### 第 3 步:逐条取文本
@@ -243,7 +243,7 @@ wiki/projects/{projectId}-{projectName}/
243
243
  ## 完整链路
244
244
 
245
245
  ```
246
- 1. xiaobao-cli auth login — 拿到 token
246
+ 1. xiaobao-cli auth login --no-wait → --resume — 拿到 token
247
247
  2. wangxiaobao-switch-project — 选好租户/项目,写 ~/.xiaobao/active-project.json
248
248
  3. 本 skill - sync 阶段 — 翻页拉 → 逐条取文本 → 按 顾问/日期 写 raw → 推进游标
249
249
  4. 本 skill - ingest 阶段 — 读 raw/ → 提炼 4 层 Layer 2 知识页 → 更新 index/log
@@ -271,7 +271,7 @@ wiki/projects/{projectId}-{projectName}/
271
271
 
272
272
  ## 常见错误与排查
273
273
 
274
- - **`xiaobao-cli audio list` 返回 401** — token 过期 → `xiaobao-cli auth login --force` 重登
274
+ - **`xiaobao-cli audio list` 返回 401** — token 过期 → `auth login --no-wait --force` split-flow 重登
275
275
  - **`xiaobao-cli audio text` 返回 `data: null` / `texts: []`** — 转录未就绪 / 录音异常 → 跳过该条
276
276
  - **page 翻不动 / 重复返回同一页** — 检查 page 是否从 1 开始;`total = 0` 直接结束窗口
277
277
  - **fromDate/toDate 报参数错** — 用空格分隔的 `yyyy-MM-dd HH:mm:ss`,不带时区
@@ -17,7 +17,7 @@ metadata:
17
17
 
18
18
  ## 执行前必读
19
19
 
20
- - 必须有有效 token:先调 `xiaobao-cli auth whoami`;未登录就 `xiaobao-cli auth login`
20
+ - 必须有有效 token:先调 `xiaobao-cli auth whoami`;未登录就走 `auth login --no-wait` split-flow(见 wangxiaobao-shared)
21
21
  - **必须有激活项目**:命令内部自动读,缺失返回 `NO_ACTIVE_PROJECT`
22
22
  - **数据权限隔离**:按当前用户授权可见顾问范围过滤(普通顾问看自己 /
23
23
  团队长看团队 / 项目管理员看全项目)
@@ -156,7 +156,7 @@ xiaobao-cli focus list --audio-ids 507494047338729472 --page 1 --size 50
156
156
  ## 常见错误与排查
157
157
 
158
158
  - **`error: 'NO_ACTIVE_PROJECT'`** — 跑 `wangxiaobao-switch-project` skill
159
- - **401 / token 过期** — `xiaobao-cli auth login --force` 重登
159
+ - **401 / token 过期** — `auth login --no-wait --force` split-flow 重登
160
160
  - **`total: 0`** — 过滤条件没匹配 / 该时段没数据 / 当前用户授权范围内没匹配的接待顾问
161
161
  - **传入的 ID 找不到对应数据** — 数据权限隔离把不属于授权顾问的 tag 过滤掉了;
162
162
  这是预期行为,不是 bug
@@ -17,7 +17,7 @@ metadata:
17
17
 
18
18
  ## 执行前必读
19
19
 
20
- - 必须有有效 token:先调 `xiaobao-cli auth whoami`;未登录就 `xiaobao-cli auth login`
20
+ - 必须有有效 token:先调 `xiaobao-cli auth whoami`;未登录就走 `auth login --no-wait` split-flow(见 wangxiaobao-shared)
21
21
  - **必须有激活项目**:命令内部自动读,如果返回 `NO_ACTIVE_PROJECT` 就跑
22
22
  `wangxiaobao-switch-project` skill
23
23
  - **数据权限隔离**:后端按"当前用户授权可见的顾问范围"过滤(普通顾问看自己 /
@@ -158,7 +158,7 @@ xiaobao-cli customer list --page 1 --size 1
158
158
  ## 常见错误与排查
159
159
 
160
160
  - **`error: 'NO_ACTIVE_PROJECT'`** — 跑 `wangxiaobao-switch-project` skill
161
- - **401 / token 过期** — `xiaobao-cli auth login --force` 重登
161
+ - **401 / token 过期** — `auth login --no-wait --force` split-flow 重登
162
162
  - **`total: 0`** — 时间窗口 / 过滤条件没匹配到,**或**当前用户授权范围为空
163
163
  (新人顾问刚入职 / 团队成员被移走等)
164
164
  - **顾问名找不到** — 不在当前用户授权可见范围内;不要硬调 list_customers 试
@@ -18,7 +18,7 @@ metadata:
18
18
 
19
19
  ## 执行前必读
20
20
 
21
- - 必须有有效 token:先调 `xiaobao-cli auth whoami`;未登录就 `xiaobao-cli auth login`
21
+ - 必须有有效 token:先调 `xiaobao-cli auth whoami`;未登录就走 `auth login --no-wait` split-flow(见 wangxiaobao-shared)
22
22
  - **必须有激活项目**:命令内部自动读,缺失返回 `NO_ACTIVE_PROJECT`
23
23
  - **数据权限隔离**:按当前用户授权可见顾问范围过滤(普通顾问看自己 /
24
24
  团队长看团队 / 项目管理员看全项目)
@@ -150,7 +150,7 @@ xiaobao-cli resistance list --category 户型 --from "2026-05-01 00:00:00" --to
150
150
  ## 常见错误与排查
151
151
 
152
152
  - **`error: 'NO_ACTIVE_PROJECT'`** — 跑 `wangxiaobao-switch-project` skill
153
- - **401 / token 过期** — `xiaobao-cli auth login --force` 重登
153
+ - **401 / token 过期** — `auth login --no-wait --force` split-flow 重登
154
154
  - **`total: 0`** — 过滤条件没匹配 / 该时段没数据 / 当前用户授权范围内没匹配的接待顾问
155
155
  - **抗性点 vs 关注点**:跟 LLM 强调"抗性"是负面(疑虑/反对),"关注"是中性偏正
156
156
  (主动关心);用户说"嫌弃 / 担心 / 不满"走抗性,"在意 / 关心 / 偏好"走关注
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: wangxiaobao-html-to-pdf
3
+ description: 将HTML文件转换为高质量PDF。当用户需要将HTML文件(本地文件或URL)转为PDF时触发此技能。触发词包括:HTML转PDF、生成PDF、导出PDF、convert HTML to PDF、HTML to PDF、把HTML生成PDF、PDF导出。支持完整保留CSS样式、渐变、Grid/Flex布局、背景色等。
4
+ agent_created: true
5
+ ---
6
+
7
+ # HTML to PDF
8
+
9
+ ## Overview
10
+
11
+ 将HTML文件(本地文件或HTTP URL)转换为高质量PDF,使用Playwright无头Chromium渲染引擎,完整保留CSS渐变、Grid/Flex布局、背景色、字体等样式。适用于包含复杂样式的HTML报告、海报、数据可视化页面等场景。
12
+
13
+ ## Prerequisites
14
+
15
+ 运行此技能需要:
16
+ - Python 3.8+
17
+ - Playwright + Chromium
18
+
19
+ 如未安装,先执行:
20
+ ```bash
21
+ pip install playwright
22
+ python3 -m playwright install chromium
23
+ ```
24
+
25
+ ## CLI 参数
26
+
27
+ ```bash
28
+ python3 scripts/html2pdf.py <source> [output] [options]
29
+ ```
30
+
31
+ | 参数 | 说明 |
32
+ |------|------|
33
+ | `source` | HTML文件路径或HTTP/HTTPS URL(必填) |
34
+ | `output` | 输出PDF路径(可选,默认与HTML同名.pdf) |
35
+ | `--width` | 页面宽度(如 `210mm`、`1200px`) |
36
+ | `--height` | 页面高度(如 `297mm`、`1697px`) |
37
+ | `--format` | 标准纸张:`A4`(默认)、`A3`、`Letter` 等 |
38
+ | `--landscape` | 横向模式(A4横向 = 297×210mm) |
39
+
40
+ **页面尺寸优先级**:`--width/--height` > `--landscape` > `--format`
41
+
42
+ ## Workflow
43
+
44
+ ### Step 1: 确认输入
45
+
46
+ 确认用户提供的HTML文件路径或URL:
47
+ - 本地文件:使用文件路径(如 `/path/to/report.html` 或 `report.html`)
48
+ - URL:完整的HTTP/HTTPS地址(如 `https://example.com/page.html`)
49
+
50
+ ### Step 2: 选择页面尺寸
51
+
52
+ 根据HTML设计宽度选择合适的PDF页面尺寸:
53
+
54
+ | 场景 | 参数 | 说明 |
55
+ |------|------|------|
56
+ | 标准报告/A4纵向 | (默认) | A4 210×297mm |
57
+ | 宽屏报告/A4横向 | `--landscape` | A4 横向,适合宽屏HTML |
58
+ | 与HTML设计宽完全匹配 | `--width 1200px --height 1697px` | 自定义尺寸,1:1无缩放 |
59
+ | 其他标准纸张 | `--format A3` | 支持 A3/A2/Letter 等 |
60
+
61
+ ### Step 3: 执行转换
62
+
63
+ ```bash
64
+ # 基本用法(A4纵向)
65
+ python3 scripts/html2pdf.py report.html
66
+
67
+ # 指定输出路径
68
+ python3 scripts/html2pdf.py report.html ./output.pdf
69
+
70
+ # A4 横向(适合宽屏报告)
71
+ python3 scripts/html2pdf.py report.html --landscape
72
+
73
+ # 自定义页面尺寸(与HTML设计宽完全匹配)
74
+ python3 scripts/html2pdf.py report.html --width 1200px --height 1697px
75
+
76
+ # 其他标准纸张
77
+ python3 scripts/html2pdf.py report.html --format A3
78
+ ```
79
+
80
+ ### Step 4: 验证并交付
81
+
82
+ 1. 确认PDF文件已生成(检查文件大小 > 0)
83
+ 2. 使用 `deliver_attachments` 工具交付PDF给用户
84
+ 3. 在回复中说明文件名、大小、保存位置
85
+
86
+ ## PDF参数
87
+
88
+ 默认配置:
89
+ - **纸张格式**:A4(可通过 `--format` / `--landscape` / `--width+--height` 修改)
90
+ - **边距**:上15mm / 下15mm / 左10mm / 右10mm
91
+ - **打印背景**:是(完整保留CSS背景色、渐变)
92
+ - **页眉页脚**:关闭
93
+
94
+ ## 排版一致性建议
95
+
96
+ 为确保PDF与HTML排版完全一致:
97
+ 1. **使用自定义尺寸**:`--width 1200px --height 1697px`(匹配HTML设计宽度)
98
+ 2. **或在HTML中添加打印CSS**:
99
+ ```css
100
+ @page { size: 1200px 1697px; margin: 0; }
101
+ @media print {
102
+ body { margin: 0; -webkit-print-color-adjust: exact; }
103
+ .card { break-inside: avoid; }
104
+ }
105
+ ```
106
+ 3. **A4横向**:`--landscape` 适合大多数宽屏报告
107
+
108
+ ## Troubleshooting
109
+
110
+ | 问题 | 原因 | 解决方案 |
111
+ |------|------|----------|
112
+ | 报错 `playwright` 未找到 | Python环境未安装playwright | 运行 `pip install playwright && python3 -m playwright install chromium` |
113
+ | PDF内容不全 | 页面JS未加载完成 | 增大 `wait_for_timeout` 的等待时间(默认5秒) |
114
+ | 中文字体缺失 | 系统未安装中文字体 | HTML中使用系统字体栈(PingFang SC / Microsoft YaHei) |
115
+ | 文件路径含中文报错 | 路径编码问题 | 使用绝对路径,确保路径格式正确 |
116
+ | PDF排版与HTML不一致 | 页面尺寸不匹配 | 使用 `--width/--height` 指定与HTML设计宽一致的尺寸 |
@@ -0,0 +1,132 @@
1
+ """Convert HTML file to PDF using Playwright (headless Chromium).
2
+
3
+ Supports both local HTML files and HTTP URLs.
4
+ Usage:
5
+ python3 html2pdf.py <html_path_or_url> [output_pdf_path] [options]
6
+
7
+ Options:
8
+ --width <size> Page width (e.g., "210mm", "1200px")
9
+ --height <size> Page height (e.g., "297mm", "1697px")
10
+ --format <fmt> Page format: A4, A3, Letter, etc. (default: A4)
11
+ --landscape Use landscape orientation (297mm x 210mm)
12
+
13
+ Examples:
14
+ python3 html2pdf.py report.html # A4 portrait
15
+ python3 html2pdf.py report.html --landscape # A4 landscape
16
+ python3 html2pdf.py report.html --width 1200px --height 1697px # custom size
17
+ """
18
+
19
+ import sys
20
+ import os
21
+ import argparse
22
+ from pathlib import Path
23
+ from playwright.sync_api import sync_playwright
24
+
25
+
26
+ def html_to_pdf(source: str, pdf_path: str = None, width: str = None, height: str = None, page_format: str = "A4", landscape: bool = False) -> str:
27
+ """
28
+ Convert an HTML file or URL to PDF.
29
+
30
+ Args:
31
+ source: Absolute path to an HTML file, or an HTTP/HTTPS URL.
32
+ pdf_path: Optional output PDF path. Defaults to same name as source with .pdf extension.
33
+ width: Optional page width (e.g., "210mm", "1200px"). Overrides format if set.
34
+ height: Optional page height (e.g., "297mm", "1697px"). Overrides format if set.
35
+ page_format: Page format (A4, A3, Letter, etc.). Ignored if width/height set.
36
+ landscape: If True, use landscape orientation (swaps width/height of format).
37
+
38
+ Returns:
39
+ The absolute path to the generated PDF file.
40
+ """
41
+ is_url = source.startswith("http://") or source.startswith("https://")
42
+
43
+ if not is_url:
44
+ source = os.path.abspath(source)
45
+ if not os.path.exists(source):
46
+ raise FileNotFoundError(f"HTML file not found: {source}")
47
+ file_url = Path(source).as_uri()
48
+ else:
49
+ file_url = source
50
+
51
+ if pdf_path is None:
52
+ if is_url:
53
+ from urllib.parse import urlparse
54
+ path_part = urlparse(source).path
55
+ base_name = Path(path_part).stem or "page"
56
+ pdf_path = os.path.join(os.getcwd(), base_name + ".pdf")
57
+ else:
58
+ pdf_path = source.rsplit(".", 1)[0] + ".pdf"
59
+
60
+ with sync_playwright() as p:
61
+ browser = p.chromium.launch(headless=True)
62
+ page = browser.new_page()
63
+
64
+ # 设置 viewport 尺寸,与 PDF 页面尺寸匹配,避免缩放
65
+ if width and height:
66
+ w_px = int(width.replace("px", "")) if "px" in width else 800
67
+ h_px = int(height.replace("px", "")) if "px" in height else 1100
68
+ page.set_viewport_size({"width": w_px, "height": h_px})
69
+ elif landscape:
70
+ page.set_viewport_size({"width": 1123, "height": 795}) # A4 横向 96dpi
71
+ else:
72
+ page.set_viewport_size({"width": 794, "height": 1123}) # A4 纵向 96dpi
73
+
74
+ # 强制使用 screen 媒体模式渲染(确保 Chart.js 等按屏幕 CSS 渲染)
75
+ page.emulate_media(media="screen")
76
+
77
+ page.goto(file_url, wait_until="networkidle")
78
+ page.wait_for_timeout(8000) # 等待 Chart.js 图表充分渲染(8秒)
79
+
80
+ # Build pdf parameters
81
+ pdf_params = {
82
+ "path": pdf_path,
83
+ "print_background": True,
84
+ "margin": {"top": "15mm", "bottom": "15mm", "left": "10mm", "right": "10mm"},
85
+ "display_header_footer": False,
86
+ }
87
+
88
+ if width and height:
89
+ pdf_params["width"] = width
90
+ pdf_params["height"] = height
91
+ elif landscape:
92
+ pdf_params["width"] = "297mm"
93
+ pdf_params["height"] = "210mm"
94
+ else:
95
+ pdf_params["format"] = page_format
96
+
97
+ page.pdf(**pdf_params)
98
+ browser.close()
99
+
100
+ return pdf_path
101
+
102
+
103
+ if __name__ == "__main__":
104
+ parser = argparse.ArgumentParser(
105
+ description="Convert HTML file or URL to PDF using Playwright (headless Chromium).",
106
+ formatter_class=argparse.RawDescriptionHelpFormatter,
107
+ epilog="""
108
+ Examples:
109
+ python3 html2pdf.py report.html # A4 portrait
110
+ python3 html2pdf.py report.html --landscape # A4 landscape
111
+ python3 html2pdf.py report.html --width 1200px --height 1697px # custom size
112
+ python3 html2pdf.py report.html output.pdf --format A3 # A3 format
113
+ """
114
+ )
115
+ parser.add_argument("source", help="HTML file path or HTTP/HTTPS URL")
116
+ parser.add_argument("output", nargs="?", default=None, help="Output PDF path (default: same name as source)")
117
+ parser.add_argument("--width", help="Page width (e.g., '210mm', '1200px')")
118
+ parser.add_argument("--height", help="Page height (e.g., '297mm', '1697px')")
119
+ parser.add_argument("--format", default="A4", help="Page format: A4, A3, Letter, etc. (default: A4)")
120
+ parser.add_argument("--landscape", action="store_true", help="Use landscape orientation")
121
+
122
+ args = parser.parse_args()
123
+
124
+ result = html_to_pdf(
125
+ source=args.source,
126
+ pdf_path=args.output,
127
+ width=args.width,
128
+ height=args.height,
129
+ page_format=args.format,
130
+ landscape=args.landscape,
131
+ )
132
+ print(f"PDF generated: {result}")