weaver-work-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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/dist/cmd/action.js +35 -0
  4. package/dist/cmd/commands/index.js +15 -0
  5. package/dist/cmd/config/index.js +64 -0
  6. package/dist/cmd/docs/index.js +56 -0
  7. package/dist/cmd/doctor/index.js +45 -0
  8. package/dist/cmd/e10/index.js +25 -0
  9. package/dist/cmd/index.js +18 -0
  10. package/dist/cmd/register.js +19 -0
  11. package/dist/cmd/root.js +36 -0
  12. package/dist/cmd/run/index.js +38 -0
  13. package/dist/cmd/setup/index.js +77 -0
  14. package/dist/cmd/skills/index.js +72 -0
  15. package/dist/core/config-store.js +43 -0
  16. package/dist/core/context.js +31 -0
  17. package/dist/core/errors.js +60 -0
  18. package/dist/core/hooks.js +41 -0
  19. package/dist/core/logger.js +27 -0
  20. package/dist/core/output.js +25 -0
  21. package/dist/core/write-with-retry.js +21 -0
  22. package/dist/index.js +11 -0
  23. package/dist/internal/e10/auth/commands.js +594 -0
  24. package/dist/internal/e10/auth/crypto.js +126 -0
  25. package/dist/internal/e10/auth/paths.js +12 -0
  26. package/dist/internal/e10/auth/session.js +449 -0
  27. package/dist/internal/e10/client.js +104 -0
  28. package/dist/internal/e10/context.js +20 -0
  29. package/dist/internal/e10/index.js +2 -0
  30. package/dist/internal/e10/types.js +1 -0
  31. package/dist/internal/skills/install.js +99 -0
  32. package/dist/internal/skills/reader.js +119 -0
  33. package/dist/services/command-runner.js +44 -0
  34. package/dist/shortcuts/index.js +4 -0
  35. package/dist/shortcuts/invoice/continuation.js +70 -0
  36. package/dist/shortcuts/invoice/errors.js +59 -0
  37. package/dist/shortcuts/invoice/host.js +337 -0
  38. package/dist/shortcuts/invoice/index.js +301 -0
  39. package/dist/shortcuts/invoice/manifest.js +288 -0
  40. package/dist/shortcuts/invoice/operations/add.js +106 -0
  41. package/dist/shortcuts/invoice/operations/delete.js +92 -0
  42. package/dist/shortcuts/invoice/operations/download.js +16 -0
  43. package/dist/shortcuts/invoice/operations/get.js +7 -0
  44. package/dist/shortcuts/invoice/operations/import.js +128 -0
  45. package/dist/shortcuts/invoice/operations/list.js +12 -0
  46. package/dist/shortcuts/invoice/operations/ocr-preview.js +40 -0
  47. package/dist/shortcuts/invoice/operations/registry.js +50 -0
  48. package/dist/shortcuts/invoice/operations/shared.js +640 -0
  49. package/dist/shortcuts/invoice/operations/types.js +1 -0
  50. package/dist/shortcuts/invoice/operations/update.js +109 -0
  51. package/dist/shortcuts/invoice/operations/upload.js +8 -0
  52. package/dist/shortcuts/invoice/operations/validate-preview.js +39 -0
  53. package/dist/shortcuts/invoice/operations.js +8 -0
  54. package/dist/types.js +1 -0
  55. package/docs/SKILL.md +36 -0
  56. package/docs/_catalog.md +12 -0
  57. package/docs/agent-invoice.md +53 -0
  58. package/docs/e10-auth.md +51 -0
  59. package/docs/invoice.md +99 -0
  60. package/package.json +51 -0
  61. package/scripts/clean.mjs +4 -0
  62. package/scripts/package-skill.mjs +164 -0
  63. package/scripts/sync-docs.mjs +17 -0
  64. package/skill-template/business-info.json +4 -0
  65. package/skill-template/domains/invoice.md +23 -0
  66. package/skill-template/domains/shared.md +8 -0
  67. package/skill-template/master-skill-template.md +29 -0
  68. package/skill-template/skill-template.md +60 -0
  69. package/skills/weaver-work-cli-invoice/SKILL.md +103 -0
  70. package/skills/weaver-work-cli-invoice/product.json +8 -0
  71. package/skills/weaver-work-cli-invoice/references/invoice-add.md +18 -0
  72. package/skills/weaver-work-cli-invoice/references/invoice-agent-entry.md +38 -0
  73. package/skills/weaver-work-cli-invoice/references/invoice-delete.md +20 -0
  74. package/skills/weaver-work-cli-invoice/references/invoice-detail.md +29 -0
  75. package/skills/weaver-work-cli-invoice/references/invoice-disabled-capabilities.md +26 -0
  76. package/skills/weaver-work-cli-invoice/references/invoice-download.md +22 -0
  77. package/skills/weaver-work-cli-invoice/references/invoice-enterprise-list.md +35 -0
  78. package/skills/weaver-work-cli-invoice/references/invoice-file-upload.md +28 -0
  79. package/skills/weaver-work-cli-invoice/references/invoice-import.md +28 -0
  80. package/skills/weaver-work-cli-invoice/references/invoice-ocr-preview.md +33 -0
  81. package/skills/weaver-work-cli-invoice/references/invoice-personal-list.md +47 -0
  82. package/skills/weaver-work-cli-invoice/references/invoice-update.md +38 -0
  83. package/skills/weaver-work-cli-invoice/references/invoice-validation-preview.md +26 -0
  84. package/skills/weaver-work-cli-shared/SKILL.md +53 -0
  85. package/skills/weaver-work-cli-shared/references/e10-auth-and-session.md +83 -0
  86. package/skills/weaver-work-cli-shared/references/high-risk-write.md +36 -0
  87. package/skills/weaver-work-cli-shared/references/json-output-contract.md +61 -0
  88. package/skills/weaver-work-cli-shared/references/weaver-work-cli-installation.md +61 -0
  89. package/templates/shortcut/index.ts.tpl +19 -0
  90. package/templates/shortcut/operations/ping.ts.tpl +7 -0
@@ -0,0 +1,109 @@
1
+ import { invoiceConfirmationError, invoicePartialError, invoiceValidationError, isInvoiceNetworkError } from '../errors.js';
2
+ import { applyChanges, assertEditable, changePreview, detailInfo, getDetail, normalizeChanges, normalizeScope, objectFingerprint, operateType, preview, reqBase, sessionMatches, stripForbiddenKeys, verifyChangedFields, workflow, } from './shared.js';
3
+ export class InvoiceUpdatePrepareOperation {
4
+ name = 'invoice.update.prepare';
5
+ async execute(input, { host, continuation }) {
6
+ const scope = normalizeScope(input.scope);
7
+ const info = detailInfo(await getDetail({ fid: input.fid }, host));
8
+ assertEditable(info);
9
+ const changes = normalizeChanges(input.changes);
10
+ const diffs = changePreview(info, changes).filter((item) => !Object.is(item.from, item.to));
11
+ if (!diffs.length)
12
+ throw invoiceValidationError('changes_noop', '所有修改值都与当前值相同');
13
+ const effectiveChanges = Object.fromEntries(diffs.map((item) => [item.field, item.to]));
14
+ const session = await host.getSession();
15
+ const continuationToken = await continuation.issue({
16
+ operation: 'invoice.update.apply',
17
+ workflow: workflow('invoice.update', 'AWAITING_CONFIRMATION'),
18
+ baseUrl: session.baseUrl,
19
+ profile: session.profile,
20
+ userId: session.userId,
21
+ fid: String(info.fid || info.id),
22
+ scope,
23
+ fingerprint: objectFingerprint(info),
24
+ changes: effectiveChanges,
25
+ preview: preview(info),
26
+ });
27
+ return {
28
+ status: 'AWAITING_CONFIRMATION',
29
+ data: { invoice: preview(info), changes: diffs, continuation: continuationToken, expiresInSeconds: 600, workflow: workflow('invoice.update', 'AWAITING_CONFIRMATION') },
30
+ };
31
+ }
32
+ }
33
+ export class InvoiceUpdateApplyOperation {
34
+ name = 'invoice.update.apply';
35
+ async execute(input, { host, continuation }) {
36
+ if (input.confirm !== true)
37
+ throw invoiceConfirmationError('invoice.update.apply', { hint: '重新调用时传入 confirm=true' });
38
+ const lease = await continuation.verify(input.continuation, 'invoice.update.apply');
39
+ if (!lease.scope || typeof lease.fid !== 'string' || typeof lease.fingerprint !== 'string' || !lease.changes || typeof lease.changes !== 'object') {
40
+ throw invoiceValidationError('continuation_invalid', '编辑 continuation 缺少有效上下文、目标或变更指纹');
41
+ }
42
+ const session = await host.getSession();
43
+ if (!sessionMatches(lease, session))
44
+ throw invoiceValidationError('context_mismatch', '确认上下文已变化,请重新准备编辑');
45
+ const current = detailInfo(await getDetail({ fid: lease.fid }, host));
46
+ if (objectFingerprint(current) !== lease.fingerprint)
47
+ throw invoiceValidationError('target_changed', '发票在确认后发生变化,请重新准备编辑');
48
+ assertEditable(current);
49
+ const updated = applyChanges(stripForbiddenKeys(current), normalizeChanges(lease.changes));
50
+ let result;
51
+ try {
52
+ result = await host.invoiceRequest({
53
+ path: '/api/inc/update',
54
+ method: 'POST',
55
+ body: {
56
+ mask: 0,
57
+ info: updated,
58
+ operate_type: operateType(lease.scope),
59
+ mod_file: false,
60
+ needCompareChangeField: true,
61
+ skipFlag: 0,
62
+ req_base: reqBase(),
63
+ },
64
+ });
65
+ }
66
+ catch (error) {
67
+ if (isInvoiceNetworkError(error)) {
68
+ throw invoicePartialError('编辑请求发送后连接中断,结果不确定,禁止自动重试', {
69
+ fid: lease.fid,
70
+ changes: lease.changes,
71
+ workflow: workflow('invoice.update', 'EXECUTING'),
72
+ }, { stage: 'update' });
73
+ }
74
+ throw error;
75
+ }
76
+ let after;
77
+ try {
78
+ after = detailInfo(await getDetail({ fid: lease.fid }, host));
79
+ }
80
+ catch {
81
+ throw invoicePartialError('编辑接口已返回成功,但详情回查失败,结果不确定', {
82
+ fid: lease.fid,
83
+ result,
84
+ workflow: workflow('invoice.update', 'VERIFYING'),
85
+ }, { stage: 'readback' });
86
+ }
87
+ if (!verifyChangedFields(after, lease.changes)) {
88
+ throw invoicePartialError('编辑接口已返回成功,但回查字段不一致', {
89
+ fid: lease.fid,
90
+ expected: lease.changes,
91
+ result,
92
+ workflow: workflow('invoice.update', 'VERIFYING'),
93
+ }, { stage: 'readback' });
94
+ }
95
+ return {
96
+ status: 'COMPLETE',
97
+ data: {
98
+ fid: lease.fid,
99
+ scope: lease.scope,
100
+ updated: true,
101
+ verified: true,
102
+ changes: changePreview(current, lease.changes),
103
+ info: after,
104
+ result,
105
+ workflow: workflow('invoice.update', 'COMPLETE'),
106
+ },
107
+ };
108
+ }
109
+ }
@@ -0,0 +1,8 @@
1
+ import { uploadInvoiceFile, workflow } from './shared.js';
2
+ export class InvoiceUploadOperation {
3
+ name = 'invoice.upload';
4
+ async execute(input, { host }) {
5
+ const upload = await uploadInvoiceFile(input, host);
6
+ return { status: 'UPLOADED', data: { upload, workflow: workflow('invoice.ocr.preview', 'UPLOADED') } };
7
+ }
8
+ }
@@ -0,0 +1,39 @@
1
+ import { InvoiceCliError } from '../errors.js';
2
+ import { detailInfo, getDetail, identityFingerprint, normalizeScope, operateType, reqBase, validationPreview, workflow, } from './shared.js';
3
+ export class InvoiceValidatePreviewOperation {
4
+ name = 'invoice.validate.preview';
5
+ async execute(input, { host }) {
6
+ const scope = normalizeScope(input.scope);
7
+ const detail = await getDetail({ fid: input.fid }, host);
8
+ const info = detailInfo(detail);
9
+ const result = await host.invoiceRequest({
10
+ path: '/api/inc/valid',
11
+ method: 'POST',
12
+ body: {
13
+ info,
14
+ flag: 100,
15
+ is_cut: 0,
16
+ is_save: 1,
17
+ operate_type: operateType(scope),
18
+ needLog: false,
19
+ req_base: reqBase({ numericTrace: true }),
20
+ },
21
+ });
22
+ if (!result?.info) {
23
+ throw new InvoiceCliError('api', 'validation_empty', '查验接口未返回发票信息', { details: { result }, exitCode: 1 });
24
+ }
25
+ if (identityFingerprint(result.info) !== identityFingerprint(info)) {
26
+ throw new InvoiceCliError('api', 'validation_target_mismatch', '查验返回的票面身份字段与目标发票不一致', { exitCode: 1 });
27
+ }
28
+ return {
29
+ status: 'COMPLETE',
30
+ data: {
31
+ preview: validationPreview(result.info, scope, input.fid),
32
+ info: result.info,
33
+ saved: false,
34
+ result,
35
+ workflow: workflow('invoice.validate.preview', 'COMPLETE'),
36
+ },
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,8 @@
1
+ import { getInvoiceOperation } from './operations/registry.js';
2
+ import { asObject, assertNoForbiddenKeys } from './operations/shared.js';
3
+ export async function executeInvoiceOperation(operation, rawInput, context) {
4
+ const handler = getInvoiceOperation(operation);
5
+ const input = asObject(rawInput);
6
+ assertNoForbiddenKeys(input);
7
+ return handler.execute(input, context);
8
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/docs/SKILL.md ADDED
@@ -0,0 +1,36 @@
1
+ # weaver-work-cli 使用说明
2
+
3
+ 本包提供一套纯净的 CLI 基础框架,并内置隔离的 E10 适配模块。
4
+
5
+ 模块目录分为三类:
6
+
7
+ - `src/cmd`:框架内置命令、平台命令和 `skills list/read`。
8
+ - `src/internal/e10`:E10 session、profile、auth 文件和 API client。
9
+ - `src/shortcuts`:面向人和 Agent 的高层业务 CLI 能力,按业务模块分目录;模块内每个业务 operation 用相对独立的 class 封装。
10
+ - `skills`:随包发布的 Agent Skill,按业务模块分目录;每个模块对应一个独立 Skill,入口 `SKILL.md` 只做路由,具体操作细节放到 `references/`,公共约束通过 `weaver-work-cli-shared` 复用。
11
+ - `skill-template`:新增或重写业务 Skill 的模板来源;结构参考飞书 CLI 的 `skill-template`,用于保持入口、路由关键词、业务域说明和 reference 拆分方式一致。
12
+
13
+ 使用内置 docs 命令可以把当前文档目录复制到业务工作区:
14
+
15
+ ```bash
16
+ weaver-work-cli docs copy
17
+ ```
18
+
19
+ 后续业务 shortcut 应在 `src/shortcuts/<name>` 下维护源码,在 `skills/weaver-work-cli-<name>` 下维护对应 Agent Skill,在 `_catalog.md` 和 `skill-template/business-info.json` 中补充对应的加载规则。业务 Skill 直接声明并调用 `weaver-work-cli` bin,不为每个业务模块复制 bootstrap 脚本。
20
+
21
+ 业务 Skill 目录约定:
22
+
23
+ ```text
24
+ skills/weaver-work-cli-<name>/
25
+ SKILL.md 入口路由:共享规则、schema、reference 索引、禁用边界。
26
+ product.json 可选:Skill 包元信息。
27
+ references/ 具体业务 reference,按用户意图和 operation 分文件。
28
+ ```
29
+
30
+ 入口 `SKILL.md` 不承载完整接口手册。把 schema 使用、查询、文件处理、写入确认、禁用能力等拆进 reference,让 Agent 只读取当前任务真正需要的内容。
31
+
32
+ ## 已内置业务模块
33
+
34
+ - `invoice`:E10 业票通发票管理。源码说明先读 `docs/invoice.md`;面向 Agent 时先读 `skills/weaver-work-cli-invoice/SKILL.md`,再按入口路由读取对应 reference。
35
+
36
+ Agent 调用业务能力时优先使用 `weaver-work-cli --json invoice run <operation> --input -`,不要绕过 CLI 直接拼业务 HTTP。
@@ -0,0 +1,12 @@
1
+ # weaver-work-cli 文档索引
2
+
3
+ | 用户意图 | 文档 |
4
+ | --- | --- |
5
+ | CLI 框架、钩子、system/platform/business 分层、新增模块 | `docs/SKILL.md` |
6
+ | E10 鉴权、profile、共享 API client | `docs/e10-auth.md` |
7
+ | Agent Skill 模板、业务域关键词、入口和 reference 拆分方式 | `skill-template/` |
8
+ | Agent 共享规则、安装检查、JSON 输出、高风险写入 | `skills/weaver-work-cli-shared/SKILL.md` |
9
+ | Agent 共享规则细节:安装、E10 登录、JSON 输出、写入协议 | `skills/weaver-work-cli-shared/references/` |
10
+ | 业票通发票管理、invoice operation、写操作 prepare/apply | `docs/invoice.md` |
11
+ | Agent 调用业票通 CLI 的入口路由 | `skills/weaver-work-cli-invoice/SKILL.md` |
12
+ | Agent 调用业票通 CLI 的具体 reference | `skills/weaver-work-cli-invoice/references/` |
@@ -0,0 +1,53 @@
1
+ # 业票通 Agent 说明
2
+
3
+ 本说明用于 AI Agent 调用安装后的 `weaver-work-cli`。Agent 只负责把用户意图映射到 CLI operation 和 JSON 参数;认证、Token、固定接口、写入确认、状态回查和错误判断都由 CLI 完成。
4
+
5
+ 安装后的 Agent Skill 已拆为入口和 references:
6
+
7
+ - `skills/weaver-work-cli-shared/SKILL.md`:安装检查、E10 认证、JSON 输出、高风险写入和安全边界。
8
+ - `skills/weaver-work-cli-invoice/SKILL.md`:业票通入口路由。
9
+ - `skills/weaver-work-cli-invoice/references/`:按 schema、查询、文件预览、写入、暂缓能力拆分的具体说明。
10
+
11
+ ## 准备
12
+
13
+ ```bash
14
+ weaver-work-cli --version
15
+ weaver-work-cli doctor --e10
16
+ weaver-work-cli invoice schema
17
+ ```
18
+
19
+ 如未登录,先让用户完成:
20
+
21
+ ```bash
22
+ weaver-work-cli auth login --base-url "https://weapp.xxx.cn"
23
+ ```
24
+
25
+ ## 固定入口
26
+
27
+ Agent 优先使用:
28
+
29
+ ```bash
30
+ weaver-work-cli --json invoice run <operation> --input -
31
+ ```
32
+
33
+ 不要直接 `curl`、`fetch` 或浏览器自动化访问 E10 `/api/inc/*`。不要输出或索取 Cookie、ETEAMSID、发票 Token。
34
+
35
+ ## 推荐工作流
36
+
37
+ 处理“把这张发票录入业票通”的典型流程:
38
+
39
+ 1. 查询个人票夹和企业票夹,确认目标不存在。
40
+ 2. `invoice.ocr.preview` 预览文件识别结果,预览阶段固定不保存。
41
+ 3. 用户确认后执行 `invoice.import.prepare`,输入必须显式包含 `validate` 和 `syncToOa`。
42
+ 4. 把 prepare 返回的 `continuation` 原样交给 `invoice.import.apply`,并传 `confirm=true`。
43
+ 5. 使用返回的 `fid` 回查详情,按实际结果报告查验状态、写入状态和附件信息。
44
+
45
+ 删除、编辑、新增同样必须使用 `prepare -> apply`。出现 `partial/write_uncertain` 时停止,不能自动重试写操作。
46
+
47
+ ## 暂缓能力
48
+
49
+ 以下原始接口文档存在,但当前不暴露为 Agent 操作:
50
+
51
+ - 共享发票:缺接收人的稳定 `appid/cid/uid` 解析和共享关系回查契约。
52
+ - 转让发票:会改变持有人,缺接收人侧归属回查契约。
53
+ - 标记发票:缺标签定义来源、当前关系查询和移除/替换语义。
@@ -0,0 +1,51 @@
1
+ # E10 鉴权适配模块
2
+
3
+ E10 适配模块刻意与核心 CLI 框架隔离,避免平台逻辑污染通用命令基础设施。
4
+
5
+ 存储路径与密钥:
6
+
7
+ - 鉴权根目录:`~/.weaver-work-cli/e10`
8
+ - Profile 目录:`~/.weaver-work-cli/e10/profiles/<profile>`
9
+ - 当前激活 profile:`~/.weaver-work-cli/e10/profile`
10
+ - Keychain service/account:`weaver-work-cli` / `auth-key`
11
+
12
+ `weaver-work-cli` 默认不读取、不迁移 `e10-login` / `e10-cli` 的 `~/.e10-cli`
13
+ 登录态,避免用户安装本插件后因为历史登录状态被自动授权。确需复用外部 auth
14
+ 文件时,可以显式传 `--auth <path>` 或设置 `E10_AUTH_PATH`。
15
+
16
+ 主要命令:
17
+
18
+ ```bash
19
+ weaver-work-cli auth status
20
+ weaver-work-cli auth login --base-url "https://weapp.xxx.cn"
21
+ weaver-work-cli auth oidc --base-url "https://weapp.xxx.cn"
22
+ weaver-work-cli auth set --eteamsid "PCACCOUNT_xxx" --base-url "https://weapp.xxx.cn"
23
+ weaver-work-cli auth whoami
24
+ weaver-work-cli auth whoami --live
25
+ weaver-work-cli auth logout
26
+ weaver-work-cli auth profile list
27
+ weaver-work-cli auth profile use <name>
28
+ weaver-work-cli auth export-weaver-env
29
+ ```
30
+
31
+ `auth login` 和 `auth oidc` 都走 E10 OAuth2.0 浏览器授权登录,本地默认监听
32
+ `http://127.0.0.1:19800/callback`。如端口被占用,可以使用
33
+ `--port 19801` 指定其他端口;如需要区分客户端来源,可以使用
34
+ `--agent_type <value>`,默认值为 `weaver-work-cli`。
35
+
36
+ 无法使用浏览器回调时,可以用 `auth set` 直接写入浏览器中复制到的
37
+ `ETEAMSID`。命令会调用 `teamsCheck` 解析当前用户、租户和组织信息,并写入
38
+ E10 兼容的 auth 文件。
39
+
40
+ `auth whoami` 默认读取本地缓存,不发网络请求;`auth whoami --live` 会调用
41
+ `teamsCheck` 获取实时信息。`auth export-weaver-env` 会输出
42
+ `WEAVER_BASE_URL`、`WEAVER_ETEAMSID`、`WEAVER_COOKIE`、`WEAVER_USER_AGENT` 和
43
+ `WEAVER_AGENT_TYPE`,可通过下面方式注入到当前 shell:
44
+
45
+ `auth logout` 会清理当前 profile 的本地登录凭据;也可配合
46
+ `--profile <name>` 清理指定 profile。该命令不删除 profile 配置和 keychain
47
+ 密钥。
48
+
49
+ ```bash
50
+ eval "$(weaver-work-cli auth export-weaver-env)"
51
+ ```
@@ -0,0 +1,99 @@
1
+ # 业票通发票管理 CLI
2
+
3
+ 来源目录:`weaver-e10-yepiaotong`,内容版本 `1.1.0`。本模块把接口文档中可安全自动化的能力封装为 `weaver-work-cli invoice` 命令,供 AI Agent 通过结构化 JSON 调用。
4
+
5
+ 源码位于 `src/shortcuts/invoice`。命令注册、operation manifest 和业务执行逻辑分离;具体执行逻辑在 `src/shortcuts/invoice/operations` 中按 operation class 维护。
6
+
7
+ 面向 Agent 的说明位于 `skills/weaver-work-cli-invoice`。入口 `SKILL.md` 只负责共享规则和 reference 路由;具体细节拆在 `skills/weaver-work-cli-invoice/references/`,与飞书 CLI 的业务 Skill 组织方式保持一致。
8
+
9
+ ## 公共前置
10
+
11
+ 先通过 E10 鉴权模块登录:
12
+
13
+ ```bash
14
+ weaver-work-cli auth login --base-url "https://weapp.xxx.cn"
15
+ weaver-work-cli auth status --no-check
16
+ ```
17
+
18
+ 每个业票通业务请求由 CLI 托管:
19
+
20
+ 1. 读取当前 `weaver-work-cli auth` Profile 的 E10 会话。
21
+ 2. 调用或复用 `/api/inc/proxy/getToken?appType=1` 取得发票业务 Token。
22
+ 3. 业务请求同时携带 E10 Cookie/ETEAMSID 和 `token` Header。
23
+
24
+ Agent 不得在业务 JSON 中传 `access_token`、`app_key`、`app_secret`、Cookie、ETEAMSID 或发票 Token。
25
+
26
+ ## Agent 入口
27
+
28
+ 优先使用 `invoice run <operation>`:
29
+
30
+ ```bash
31
+ weaver-work-cli invoice schema
32
+ echo '{"page_size":10,"start_pos":0}' | weaver-work-cli --json invoice run invoice.list --input -
33
+ ```
34
+
35
+ 成功和失败都使用 JSON envelope。成功写 stdout;失败写 stderr,并设置非 0 退出码。
36
+
37
+ ## Operation 目录
38
+
39
+ | Operation | 风险 | 固定规则 |
40
+ | --- | --- | --- |
41
+ | `invoice.list` | 只读 | 个人票夹固定 `flag=0` |
42
+ | `invoice.enterprise.list` | 只读 | 企业票夹固定 `flag=6`,权限失败不降级个人票夹 |
43
+ | `invoice.get` | 只读 | `fid` 优先,或按 `number` 查询 |
44
+ | `invoice.upload` | 外部制品 | 固定上传 `ocr=1` |
45
+ | `invoice.ocr.preview` | 上传+只读 | 固定 `flag=13`、`is_sync=1`、`is_save=1`,不保存票夹 |
46
+ | `invoice.validate.preview` | 外部查询 | 先取详情,固定 `flag=100`、`is_save=1`,不回写查验结果 |
47
+ | `invoice.import.prepare/apply` | 高风险写 | continuation 绑定本地文件 SHA-256;显式声明 `validate` 和 `syncToOa` |
48
+ | `invoice.add.prepare/apply` | 高风险写 | 单张票面校验、重复检查、确认后新增并回查 |
49
+ | `invoice.update.prepare/apply` | 高风险写 | 基于完整详情修改白名单字段,保留附件并回查 |
50
+ | `invoice.download` | 本地文件写 | 先校验 `fileId` 属于目标 `fid`,拒绝覆盖 |
51
+ | `invoice.delete.prepare/apply` | 高风险写 | 固定软删除 `flag=0`,确认后执行并回查 |
52
+
53
+ 共享、转让和标签暂不暴露给 Agent:原始文档未给出稳定的接收人 `appid/cid/uid` 解析来源、标签关系查询和写后回查契约。
54
+
55
+ ## 常用命令
56
+
57
+ ```bash
58
+ weaver-work-cli --json invoice list --input -
59
+ weaver-work-cli --json invoice enterprise list --input -
60
+ weaver-work-cli --json invoice get --fid <fid>
61
+ weaver-work-cli --json invoice upload --file ./invoice.pdf
62
+ weaver-work-cli --json invoice ocr preview --file ./invoice.pdf
63
+ weaver-work-cli --json invoice validate preview --fid <fid> --scope personal
64
+ weaver-work-cli --json invoice download --fid <fid> --file-id <fileId> --output ./invoice.pdf
65
+ ```
66
+
67
+ 写操作必须先 prepare,再 apply:
68
+
69
+ ```bash
70
+ echo '{"file":"./invoice.pdf","validate":false,"syncToOa":false}' \
71
+ | weaver-work-cli --json invoice import prepare --input -
72
+
73
+ echo '{"file":"./invoice.pdf","continuation":"<prepare返回值>","confirm":true}' \
74
+ | weaver-work-cli --json invoice import apply --input -
75
+ ```
76
+
77
+ `validate=false` 表示跳过查验,不能描述为“查验通过”。写操作出现网络中断、部分成功或回查失败时,CLI 返回 `partial/write_uncertain`,Agent 禁止自动重复提交。
78
+
79
+ ## 编辑白名单
80
+
81
+ `invoice.update.prepare` 只允许修改:
82
+
83
+ ```text
84
+ code
85
+ number
86
+ comm_info.pro.date
87
+ comm_info.price.amount
88
+ comm_info.price.total
89
+ comm_info.price.dtax
90
+ comm_info.buyer.company
91
+ comm_info.buyer.tcode
92
+ comm_info.payer.company
93
+ comm_info.payer.tcode
94
+ fylx
95
+ currency
96
+ currencyName
97
+ ```
98
+
99
+ 禁止修改 `fid/id`、附件关联、`sedit`、Dubbo 字段、认证字段、`skipFlag=1` 等内部控制字段。
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "weaver-work-cli",
3
+ "version": "0.1.0",
4
+ "description": "面向泛微工作工具链的可扩展 TypeScript CLI 基础框架(E10 业票通等能力)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "docs",
10
+ "skills",
11
+ "skill-template",
12
+ "templates",
13
+ "scripts"
14
+ ],
15
+ "bin": {
16
+ "weaver-work-cli": "./dist/index.js"
17
+ },
18
+ "scripts": {
19
+ "dev": "tsx src/index.ts",
20
+ "clean": "node scripts/clean.mjs",
21
+ "build": "npm run clean && tsc",
22
+ "test": "node --test --import tsx \"test/**/*.test.ts\"",
23
+ "start": "node dist/index.js",
24
+ "build:skill": "node scripts/package-skill.mjs",
25
+ "build:invoice-skill": "node scripts/package-skill.mjs invoice",
26
+ "prepublishOnly": "npm run build && npm run test",
27
+ "prepack": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "weaver",
31
+ "泛微",
32
+ "e10",
33
+ "yepiaotong",
34
+ "业票通",
35
+ "invoice",
36
+ "cli"
37
+ ],
38
+ "license": "MIT",
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "dependencies": {
43
+ "@napi-rs/keyring": "^1.3.0",
44
+ "commander": "^13.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.0.0",
48
+ "tsx": "^4.19.0",
49
+ "typescript": "^5.6.0"
50
+ }
51
+ }
@@ -0,0 +1,4 @@
1
+ import { rmSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ rmSync(resolve('dist'), { recursive: true, force: true });
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto'
4
+ import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+ import { spawn } from 'node:child_process'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
11
+ const skillsRoot = path.join(repositoryRoot, 'skills')
12
+ const sharedSkillName = 'weaver-work-cli-shared'
13
+ const [requestedSkillArg, outputArg] = process.argv.slice(2)
14
+
15
+ function sha256(value) {
16
+ return createHash('sha256').update(value).digest('hex')
17
+ }
18
+
19
+ function run(command, args, cwd) {
20
+ return new Promise((resolve, reject) => {
21
+ const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] })
22
+ let stdout = ''
23
+ let stderr = ''
24
+ child.stdout.on('data', (chunk) => { stdout += chunk })
25
+ child.stderr.on('data', (chunk) => { stderr += chunk })
26
+ child.once('error', reject)
27
+ child.once('close', (code) => resolve({ code: code ?? 1, stdout, stderr }))
28
+ })
29
+ }
30
+
31
+ function normalizeSkillName(value) {
32
+ if (!value || value === 'all') return null
33
+ if (value.includes('/') || value.includes('\\') || value === '.' || value === '..') {
34
+ throw new Error(`非法 Skill 名称:${value}`)
35
+ }
36
+ if (!/^[A-Za-z0-9._-]+$/u.test(value)) {
37
+ throw new Error(`非法 Skill 名称:${value}`)
38
+ }
39
+ return value.startsWith('weaver-work-cli-') ? value : `weaver-work-cli-${value}`
40
+ }
41
+
42
+ async function exists(filename) {
43
+ try {
44
+ await stat(filename)
45
+ return true
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ async function listSkillDirs() {
52
+ const entries = await readdir(skillsRoot, { withFileTypes: true })
53
+ const skills = []
54
+ for (const entry of entries) {
55
+ if (!entry.isDirectory()) continue
56
+ const skillName = entry.name
57
+ if (await exists(path.join(skillsRoot, skillName, 'SKILL.md'))) skills.push(skillName)
58
+ }
59
+ return skills.sort()
60
+ }
61
+
62
+ async function resolveSkillNames() {
63
+ const requestedSkill = normalizeSkillName(requestedSkillArg)
64
+ const allSkills = await listSkillDirs()
65
+ if (!requestedSkill) return allSkills
66
+ if (!allSkills.includes(requestedSkill)) throw new Error(`未知 Skill:${requestedSkill}`)
67
+ const names = []
68
+ if (requestedSkill !== sharedSkillName && allSkills.includes(sharedSkillName)) names.push(sharedSkillName)
69
+ names.push(requestedSkill)
70
+ return names
71
+ }
72
+
73
+ function defaultOutput(skillNames) {
74
+ if (requestedSkillArg && requestedSkillArg !== 'all') {
75
+ const requestedSkill = normalizeSkillName(requestedSkillArg)
76
+ return path.join(repositoryRoot, 'dist', `${requestedSkill}-skill.zip`)
77
+ }
78
+ if (skillNames.length === 1) return path.join(repositoryRoot, 'dist', `${skillNames[0]}-skill.zip`)
79
+ return path.join(repositoryRoot, 'dist', 'weaver-work-cli-skills.zip')
80
+ }
81
+
82
+ function shouldSkip(entry, relativePath) {
83
+ if (entry.name === '.DS_Store' || entry.name.startsWith('.')) return true
84
+ if (entry.isDirectory() && ['assets', 'node_modules', 'scripts'].includes(entry.name)) return true
85
+ return false
86
+ }
87
+
88
+ function assertSafeRelativePath(relativePath) {
89
+ if (/(?:token|cookie|eteamsid)/iu.test(relativePath)) {
90
+ throw new Error(`Skill ZIP 包含禁止条目:${relativePath}`)
91
+ }
92
+ }
93
+
94
+ async function copySkill(skillName, stagingRoot, packagedEntries) {
95
+ const sourceRoot = path.join(skillsRoot, skillName)
96
+ const packageRoot = path.join(stagingRoot, skillName)
97
+ await stat(path.join(sourceRoot, 'SKILL.md'))
98
+
99
+ async function copyDir(relativeDir = '') {
100
+ const sourceDir = path.join(sourceRoot, relativeDir)
101
+ const targetDir = path.join(packageRoot, relativeDir)
102
+ await mkdir(targetDir, { recursive: true, mode: 0o700 })
103
+
104
+ const entries = (await readdir(sourceDir, { withFileTypes: true }))
105
+ .sort((a, b) => a.name.localeCompare(b.name))
106
+ for (const entry of entries) {
107
+ const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name
108
+ if (shouldSkip(entry, relativePath)) continue
109
+ assertSafeRelativePath(relativePath)
110
+ const source = path.join(sourceRoot, relativePath)
111
+ const target = path.join(packageRoot, relativePath)
112
+ if (entry.isDirectory()) {
113
+ await copyDir(relativePath)
114
+ } else if (entry.isFile()) {
115
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 })
116
+ await copyFile(source, target)
117
+ packagedEntries.push(`${skillName}/${relativePath}`)
118
+ }
119
+ }
120
+ }
121
+
122
+ await copyDir()
123
+
124
+ const productJson = path.join(packageRoot, 'product.json')
125
+ if (await exists(productJson)) {
126
+ const product = JSON.parse(await readFile(productJson, 'utf8'))
127
+ if (product.product !== 'weaver-work-cli' || product.packageName !== 'weaver-work-cli') {
128
+ throw new Error(`${skillName}/product.json 包名不符合 weaver-work-cli`)
129
+ }
130
+ }
131
+ }
132
+
133
+ const skillNames = await resolveSkillNames()
134
+ if (skillNames.length === 0) throw new Error('没有可打包的 Skill')
135
+
136
+ const output = path.resolve(outputArg || defaultOutput(skillNames))
137
+ const stagingRoot = await mkdtemp(path.join(os.tmpdir(), 'weaver-work-cli-skill-'))
138
+ const packagedEntries = []
139
+
140
+ try {
141
+ await mkdir(path.dirname(output), { recursive: true, mode: 0o700 })
142
+ for (const skillName of skillNames) {
143
+ await copySkill(skillName, stagingRoot, packagedEntries)
144
+ }
145
+
146
+ await rm(output, { force: true })
147
+ const zipResult = await run('zip', ['-X', '-q', '-r', output, ...skillNames], stagingRoot)
148
+ if (zipResult.code !== 0) throw new Error(`Skill ZIP 打包失败:${zipResult.stderr.trim()}`)
149
+
150
+ const archive = await readFile(output)
151
+ const listResult = await run('unzip', ['-Z1', output], repositoryRoot)
152
+ if (listResult.code !== 0) throw new Error(`Skill ZIP 校验失败:${listResult.stderr.trim()}`)
153
+
154
+ const actualEntries = listResult.stdout.trim().split(/\r?\n/u).filter((entry) => entry && !entry.endsWith('/')).sort()
155
+ const expectedEntries = packagedEntries.sort()
156
+ if (JSON.stringify(actualEntries) !== JSON.stringify(expectedEntries)) {
157
+ throw new Error(`Skill ZIP 闭包不匹配:${JSON.stringify(actualEntries)}`)
158
+ }
159
+ if (archive.length > 256 * 1024) throw new Error(`Skill ZIP 超过 256 KiB:${archive.length}`)
160
+
161
+ console.log(JSON.stringify({ ok: true, output, bytes: (await stat(output)).size, sha256: sha256(archive), skills: skillNames, entries: actualEntries }, null, 2))
162
+ } finally {
163
+ await rm(stagingRoot, { recursive: true, force: true })
164
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, cpSync, rmSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
7
+ const source = resolve(root, 'docs');
8
+ const target = resolve(root, 'dist-docs');
9
+
10
+ if (!existsSync(source)) {
11
+ console.error('[sync-docs] docs directory does not exist: ' + source);
12
+ process.exit(1);
13
+ }
14
+
15
+ rmSync(target, { recursive: true, force: true });
16
+ cpSync(source, target, { recursive: true });
17
+ console.log('[sync-docs] copied docs to ' + target);