@qinsilk/scm-agent-tools 0.0.0-stage → 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 (48) hide show
  1. package/README.md +48 -2
  2. package/cli/api.js +114 -0
  3. package/cli/commands/log.js +152 -0
  4. package/cli/commands/query.js +229 -0
  5. package/cli/commands/report.js +213 -0
  6. package/cli/commands/staff.js +505 -0
  7. package/cli/config.js +46 -0
  8. package/cli/scm-agent.js +105 -0
  9. package/cli/skills.js +89 -0
  10. package/cli/table.js +43 -0
  11. package/docs/cli.md +180 -0
  12. package/package.json +27 -4
  13. package/reports/excel.js +107 -0
  14. package/reports/factoryEvaluation.js +128 -0
  15. package/reports/goodsBom.js +648 -0
  16. package/reports/goodsFirstOrder.js +409 -0
  17. package/reports/goodsInventory.js +220 -0
  18. package/reports/goodsSize.js +440 -0
  19. package/reports/image.js +216 -0
  20. package/reports/leakCheck.js +694 -0
  21. package/reports/materialPurchaseExport.js +393 -0
  22. package/reports/materialTransit.js +593 -0
  23. package/reports/processFollowup.js +312 -0
  24. package/reports/produceBoard.js +353 -0
  25. package/reports/produceCost.js +366 -0
  26. package/reports/produceCostDetail.js +622 -0
  27. package/reports/produceDaily.js +485 -0
  28. package/reports/storeInGoods.js +276 -0
  29. package/reports/supplierBalance.js +671 -0
  30. package/reports/ticketStatistics.js +289 -0
  31. package/reports/util.js +83 -0
  32. package/skills/bom/345/225/206/345/223/201/344/277/241/346/201/257/SKILL.md +40 -0
  33. package/skills/qinsilk-scm/SKILL.md +95 -0
  34. package/skills//344/276/233/345/272/224/345/225/206/345/257/271/350/264/246/345/257/274/345/207/272/SKILL.md +38 -0
  35. package/skills//345/225/206/345/223/201/345/260/272/345/257/270/344/277/241/346/201/257/SKILL.md +40 -0
  36. package/skills//345/225/206/345/223/201/351/246/226/345/215/225/351/235/242/346/226/231/345/267/245/345/272/217/345/215/225/344/273/267/SKILL.md +39 -0
  37. package/skills//345/267/245/345/216/202/350/257/204/344/274/260/346/212/245/350/241/250/SKILL.md +39 -0
  38. package/skills//346/210/220/345/223/201/345/205/245/345/272/223/346/212/245/350/241/250/SKILL.md +40 -0
  39. package/skills//346/234/215/350/243/205/346/210/220/345/223/201/347/233/230/345/255/230/350/241/250/SKILL.md +39 -0
  40. package/skills//346/274/217/346/226/231/346/243/200/346/265/213/346/227/245/346/212/245/SKILL.md +40 -0
  41. package/skills//347/211/251/346/226/231/345/234/250/351/200/224/347/212/266/346/200/201/346/212/245/350/241/250/SKILL.md +40 -0
  42. package/skills//347/211/251/346/226/231/351/207/207/350/264/255/345/257/274/345/207/272/SKILL.md +39 -0
  43. package/skills//347/224/237/344/272/247/345/215/225/345/267/245/345/272/217/350/267/237/350/277/233/350/241/250/SKILL.md +39 -0
  44. package/skills//347/224/237/344/272/247/346/210/220/346/234/254/346/212/245/350/241/250/SKILL.md +41 -0
  45. package/skills//347/224/237/344/272/247/346/210/220/346/234/254/346/212/245/350/241/250/346/230/216/347/273/206/SKILL.md +40 -0
  46. package/skills//347/224/237/344/272/247/346/227/245/346/212/245/SKILL.md +42 -0
  47. package/skills//347/224/237/344/272/247/347/212/266/345/206/265/347/273/237/350/256/241/350/241/250/SKILL.md +39 -0
  48. package/skills//347/224/237/344/272/247/350/212/202/347/202/271/347/234/213/346/235/277/SKILL.md +39 -0
package/cli/skills.js ADDED
@@ -0,0 +1,89 @@
1
+ // 技能安装:把包内 skills/ 目录下的全部官方技能(qinsilk-scm 与 16 个报表技能)
2
+ // 逐个以链接方式登记到各家 AI 助手共同扫描的 ~/.agents/skills 目录。
3
+ // 链接指向 npm 全局安装位置内部,因此包升级即技能更新;
4
+ // 链接失败时降级为整目录复制(复制版不会随升级更新,输出中如实说明)。
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ function packageSkillsRoot() {
10
+ return path.join(__dirname, '..', 'skills');
11
+ }
12
+
13
+ /** 包内全部官方技能名:skills/ 下含 SKILL.md 的一级子目录 */
14
+ function packagedSkillNames(sourceRoot = packageSkillsRoot()) {
15
+ return fs
16
+ .readdirSync(sourceRoot, { withFileTypes: true })
17
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(sourceRoot, entry.name, 'SKILL.md')))
18
+ .map((entry) => entry.name);
19
+ }
20
+
21
+ /** 各家 AI 助手的技能目录;--all 时追加 Claude/Cursor 的专属目录 */
22
+ function skillTargetDirs({ all = false, home = os.homedir() } = {}) {
23
+ const dirs = [path.join(home, '.agents', 'skills')];
24
+ if (all) {
25
+ dirs.push(path.join(home, '.claude', 'skills'));
26
+ dirs.push(path.join(home, '.cursor', 'skills'));
27
+ }
28
+ return dirs;
29
+ }
30
+
31
+ function isLink(p) {
32
+ try {
33
+ return fs.lstatSync(p).isSymbolicLink();
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function copyDir(src, dest) {
40
+ fs.mkdirSync(dest, { recursive: true });
41
+ for (const name of fs.readdirSync(src)) {
42
+ const s = path.join(src, name);
43
+ const d = path.join(dest, name);
44
+ if (fs.statSync(s).isDirectory()) {
45
+ copyDir(s, d);
46
+ } else {
47
+ fs.copyFileSync(s, d);
48
+ }
49
+ }
50
+ }
51
+
52
+ /** 安装单个技能目录,返回动作描述 */
53
+ function installOne(sourceRoot, destRoot, skillName) {
54
+ const src = path.join(sourceRoot, skillName);
55
+ const dest = path.join(destRoot, skillName);
56
+ if (fs.existsSync(dest) || isLink(dest)) {
57
+ if (!isLink(dest)) {
58
+ return `skipped(${dest} 已存在且不是本工具的链接,未覆盖)`;
59
+ }
60
+ // 旧链接重建,保证指向当前包位置
61
+ fs.rmSync(dest, { recursive: true, force: true });
62
+ }
63
+ try {
64
+ // Windows 用 junction 不需要管理员权限;其他系统用符号链接
65
+ fs.symlinkSync(src, dest, process.platform === 'win32' ? 'junction' : 'dir');
66
+ return `linked(升级自动更新)`;
67
+ } catch (e) {
68
+ copyDir(src, dest);
69
+ return `copied(降级为复制,升级后需重新安装技能)`;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 安装全部官方技能。返回每个目标目录的安装结果:
75
+ * [{ dir, skills: [{ name, action }] }]
76
+ */
77
+ function installSkills({ all = false, home = os.homedir() } = {}) {
78
+ const sourceRoot = packageSkillsRoot();
79
+ const names = packagedSkillNames(sourceRoot);
80
+ return skillTargetDirs({ all, home }).map((dir) => {
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ return {
83
+ dir,
84
+ skills: names.map((name) => ({ name, action: installOne(sourceRoot, dir, name) })),
85
+ };
86
+ });
87
+ }
88
+
89
+ module.exports = { packagedSkillNames, installSkills };
package/cli/table.js ADDED
@@ -0,0 +1,43 @@
1
+ // 表格输出:无第三方依赖的简易对齐表格,供人类可读输出。
2
+ function printTable(rows, columns) {
3
+ if (!rows || !rows.length) {
4
+ console.log('(无数据)');
5
+ return;
6
+ }
7
+ const headers = columns.map((c) => c.label);
8
+ const lines = rows.map((row) => columns.map((c) => stringify(row[c.field])));
9
+ const widths = headers.map((h, i) => Math.max(displayWidth(h), ...lines.map((line) => displayWidth(line[i]))));
10
+
11
+ const print = (cells) => {
12
+ console.log(cells.map((cell, i) => pad(cell, widths[i])).join(' '));
13
+ };
14
+ print(headers);
15
+ console.log(widths.map((w) => '-'.repeat(w)).join(' '));
16
+ lines.forEach(print);
17
+ }
18
+
19
+ /** 中文字符按 2 列宽计算,保证对齐 */
20
+ function displayWidth(text) {
21
+ let width = 0;
22
+ for (const ch of String(text)) {
23
+ width += ch.charCodeAt(0) > 0xff ? 2 : 1;
24
+ }
25
+ return width;
26
+ }
27
+
28
+ function pad(text, width) {
29
+ const padding = width - displayWidth(text);
30
+ return text + ' '.repeat(Math.max(0, padding));
31
+ }
32
+
33
+ function stringify(value) {
34
+ if (value === null || value === undefined) {
35
+ return '-';
36
+ }
37
+ if (Array.isArray(value)) {
38
+ return value.join(',');
39
+ }
40
+ return String(value);
41
+ }
42
+
43
+ module.exports = { printTable };
package/docs/cli.md ADDED
@@ -0,0 +1,180 @@
1
+ # scm-agent 全部命令说明(给你或你的 AI 看)
2
+
3
+ 前置:已执行 `scm-agent login`(配置保存在 `~/.scm-agent/config.json`,权限 600)。
4
+
5
+ ## 全局参数
6
+
7
+ | 参数 | 说明 |
8
+ |---|---|
9
+ | `--json` | 以 JSON 输出结果(默认输出对齐表格) |
10
+ | `--page N` | 列表页码,从 1 开始(默认 1) |
11
+ | `--limit N` | 每页条数,最大 100(默认 20) |
12
+
13
+ ## login — 配置 AI Key
14
+
15
+ ```
16
+ scm-agent login --key qsk-xxxx [--server https://scm.qinsilk.com]
17
+ ```
18
+
19
+ 立即连接服务器验证 Key;成功输出企业/用户名并保存配置。失败不重试。`--server` 缺省生产地址;测试环境用 `https://devscm.qinsilk.com`。
20
+
21
+ ## status — 验证配置
22
+
23
+ 输出「生产ERP CLI 已配置完成,当前用户:企业 / 用户名(权限:数据只读)」。
24
+
25
+ ## skills install — 安装技能
26
+
27
+ ```
28
+ scm-agent skills install [--all]
29
+ ```
30
+
31
+ 把包内全部官方技能(`qinsilk-scm` 与 16 个报表技能)逐个登记到 `~/.agents/skills`(多家 AI 助手共同扫描的目录);`--all` 时追加 `~/.claude/skills`、`~/.cursor/skills`。每个技能输出 `linked`(链接,升级自动更新)或 `copied`(降级复制,升级后需重装技能)。
32
+
33
+ ## update — 升级
34
+
35
+ ```
36
+ scm-agent update
37
+ ```
38
+
39
+ 从 npm 重新安装最新版(`npm install -g @qinsilk/scm-agent-tools@latest`);技能与命令行一起更新。
40
+
41
+ ## goods — 商品
42
+
43
+ ```
44
+ scm-agent goods search --name A123 # 按款号/名称/条码搜,输出款号、价格、库存、生产中数量等
45
+ scm-agent goods detail --id 12345 # 详情:颜色尺码单品(价格/库存)与工序模板
46
+ ```
47
+
48
+ ## material — 物料
49
+
50
+ ```
51
+ scm-agent material search --name 华达呢 # 按名称/货号/条码搜,输出货号、价格、库存、默认供应商
52
+ scm-agent material detail --id 12345 # 详情:单品颜色、规格(属性=值)、价格
53
+ ```
54
+
55
+ ## produce — 生产单
56
+
57
+ ```
58
+ scm-agent produce list --sn A123 # 关键词:单号/款号/商品名
59
+ scm-agent produce list --state 2 --from 2026-10-01 --to 2026-10-24 # 按状态与日期区间(至少给一项条件)
60
+ scm-agent produce detail --sn SC202610240001 # 详情:颜色尺码明细 + 工序进度
61
+ ```
62
+
63
+ 状态值:1草稿 2生产中 3生产完成 4待生产 9已撤销。裁床:详情 JSON 中 `workProcesses` 里 `core=2` 的是裁床工序(`produceNumber` 目标数、`reportNumber` 已汇报数);单据级 `cutBedState`:1不含裁床 2待裁床 3裁床中 4已完成。
64
+
65
+ ## material-purchase — 物料采购单
66
+
67
+ ```
68
+ scm-agent material-purchase list --sn 供应商名 # 关键词:单号/供应商/生产单号/物料
69
+ scm-agent material-purchase list --from 2026-10-01 --to 2026-10-24
70
+ scm-agent material-purchase detail --sn CG202610240001 # 详情:物料明细(数量/单价/入库)
71
+ ```
72
+
73
+ 状态值:1草稿 2待入库 3部分入库 4全部入库 9撤销;不传状态时默认只查五种进行中的状态(与网页一致)。
74
+
75
+ ## design — 设计款与IE核价
76
+
77
+ ```
78
+ scm-agent design list --sn A123 # 关键词:款号/名称/条码/货号
79
+ scm-agent design list --state 2 --from 2026-10-01 --to 2026-10-24 # 按状态与日期区间(至少给一项条件)
80
+ scm-agent design ie-list --sn A123 # IE核价:设计款×颜色的物料/工序/费用明细
81
+ scm-agent design ie-list --from 2026-10-01 --to 2026-10-24 # 按开发日期区间,可不带条件全量翻页
82
+ ```
83
+
84
+ 设计款状态值:2设计中 3待确定 4修改中 5审核通过 6取消 15开发池 16版型资产库。`design list` 的 `--dateType` 决定日期区间作用的时间字段:update更新时间(默认)/ create创建时间 / dev开发时间;`design ie-list` 的 `--dateType` 只有 dev开发时间(默认)/ create创建时间。IE 核价行中 `materials` 物料行、`processFees` 工序费用行、`costFees` 特殊工艺费用行、`accountItems` 其他费用行;无供应商资料权限时供应商字段为空,工序开启隐藏价格时单价为空(与网页可见范围一致)。
85
+
86
+ ## log — 数据变动日志与接口日志
87
+
88
+ ```
89
+ scm-agent log db-change --keywords WL1002 # 按关键词查变动(只匹配变动数据,多个词逗号分隔且都要命中)
90
+ scm-agent log db-change --keywords WL1002 --table scm_material --from 2026-09-01
91
+ scm-agent log db-change --request-id 1790000000000_1060184727 # 查同一请求的全部数据库变动
92
+ scm-agent log api --request-id 1790000000000_1060184727 # 按请求标识查接口日志(标识来自 db-change 返回)
93
+ ```
94
+
95
+ 回答「谁在什么时候通过什么操作改了什么」的两步查询:先用 `log db-change` 按关键词(如物料货号)、表名、操作人 uid 或请求标识查数据库变动日志(时间倒序,至少给一项条件;`--from/--to` 不传默认近 30 天,跨度上限 180 天;`--uid` 可从 `log db-change` 返回行或 user-list 获得)。返回行含操作人(姓名)、操作类型(新增/修改/删除)、变动字段值(`--json` 看完整值)与请求标识;提示「还有更多」时用给出的 `--line-num/--scroll-id` 续查。再取某行的请求标识执行 `log api`,得到这次变动的操作内容(如「修改物料」)、接口路径与参数摘要(超 4000 字符截断)。
96
+
97
+ 权限与网页一致:查数据库变动日志需要「日志查询」权限;接口日志任何账号可查,但子账号只能查到自己发起的请求(他人的请求标识返回空是正常行为)。操作时间显示 `-` 的旧记录没有请求标识,查不了接口日志。
98
+
99
+ ## report — 生成官方报表
100
+
101
+ ```
102
+ scm-agent report <报表名> [参数] [--out 输出目录]
103
+ ```
104
+
105
+ 按官方技能库的格式生成 Excel/HTML 报表文件,输出文件路径与汇总。报表数据全部来自 AI 助手接口(只读)。逐单取详情的报表内置节流(约 2 秒一次请求),单量多时耗时较长属正常。
106
+
107
+ | 报表名 | 生成内容 | 主要参数 |
108
+ |---|---|---|
109
+ | `produce-daily` | 生产日报 HTML(今日入库、计划出货、生产状态、风险待料) | `--date`(默认今天) |
110
+ | `produce-board` | 生产节点看板 HTML(近 N 天生产单按工序节点排布) | `--days`(默认 7) |
111
+ | `store-in-goods` | 成品入库报表 Excel(嵌单品图) | `--from --to --sn` |
112
+ | `factory-evaluation` | 工厂评估报表 Excel(单数、裁床数、完成件数、按时交货率) | `--from --to`(默认近一个月) |
113
+ | `produce-cost` | 生产成本报表 Excel(42 列 + 六类成本差异 + 款式图) | `--from --to --state`(默认 4,2,3) |
114
+ | `produce-cost-detail` | 生产成本报表明细(每生产单一个 Excel,含五类成本明细) | `--from --to` |
115
+ | `material-transit` | 物料在途状态报表 Excel(采购在途/已入库未发料/物料缺口三 sheet) | `--days`(默认 90) |
116
+ | `material-purchase-export` | 物料采购导出 Excel(近三个月物料采购子单) | `--from --to` |
117
+ | `supplier-balance` | 供应商对账 Excel(11 表对账单,每供应商一个文件) | `--supplier --from --to` |
118
+ | `leak-check` | 漏料检测日报(每生产单一个「物料数据」Excel) | `--limit`(默认 100)`--include-zero` |
119
+ | `process-followup` | 生产单工序跟进表 Excel(需跟进的工序标红) | `--from --to` |
120
+ | `goods-inventory` | 服装成品盘存表 Excel(动态尺码列) | `--storehouse`(仓库 id) |
121
+ | `ticket-statistics` | 生产状况统计表 Excel(按部门与货号汇总领货交货) | `--from --to`(编菲日期区间,不传则不限) |
122
+ | `goods-bom` | BOM 商品信息(每商品一个 Excel:商品信息+BOM+物料单品) | `--sn --limit` |
123
+ | `goods-size` | 商品尺寸信息(每商品一个 Excel,部位纵向尺码横向) | `--sn --limit --transpose` |
124
+ | `goods-first-order` | 商品首单面料工序单价 Excel(最小公倍数合并单元格) | `--sn --limit` |
125
+
126
+ 各报表的详细说明、文件名规则与注意事项见同名技能的 SKILL.md(`skills/<中文名>/SKILL.md`)。
127
+
128
+ ## 写命令的确认方式(所有写命令通用)
129
+
130
+ 写命令(带 `--confirm` 参数的命令)分成两次交互:第一次不带 `--confirm`,只输出预览、不发任何写请求;第二次加 `--confirm` 才提交。中间的「用户是否同意」由**运行 CLI 的 Agent** 用自己的交互界面完成,不是由 CLI 完成——CLI 在 Agent 的子进程里运行,没有交互终端,无法自己弹框。
131
+
132
+ Agent 应调用 `AskUserQuestion`(Codex 里叫 `request_user_input`),给出**「确认」与「取消」两个选项**并附上改动摘要。用户选「确认」才加 `--confirm`;选「取消」不发写请求;想改参数的用户会用对话框自带的「Other」(Codex 是「None of the above」加备注)直接输入文字,此时按该文字重新解析参数、重新预览并再次确认。
133
+
134
+ 不存在这类工具的宿主(工具列表里找不到 `AskUserQuestion`)改为文字提问,让用户回复「确认」或「取消」。Codex 的 `request_user_input` 只在 Plan 模式可用,Default 模式下改用文字提问。
135
+
136
+ ## user — 员工管理
137
+
138
+ ```
139
+ scm-agent user add --name 张三 --login-name abcd --password 1234 [--roles 设计师,版师] [--phone] [--department-id] [--work-sn] [--confirm]
140
+ scm-agent user search --name 张三
141
+ scm-agent user detail --uid 888
142
+ ```
143
+
144
+ `user add` 创建子账号。**默认只预览**(展示姓名、账号、角色、密码掩码),**加 `--confirm` 才创建**;密码在命令内部做与网页一致的 md5 转换后提交,预览与结果都不显示密码。角色名支持精确或唯一前缀匹配,有歧义时报错列出候选。创建依赖进销存系统联动,失败时错误原样返回。
145
+
146
+ `user search` 按姓名、手机号或工号模糊查询,返回 uid(同名员工会同时列出,按部门区分),用于把「张三」这类姓名换成后续命令需要的 uid。
147
+
148
+ `user detail` 返回员工基本信息与拥有的角色;用户有多个角色时,改权限前必须先让用户选择改哪一个角色的。
149
+
150
+ ## role — 角色与权限管理
151
+
152
+ ```
153
+ scm-agent role list [--name 设计]
154
+ scm-agent role detail --id 5 | --name 设计师
155
+ scm-agent role add-permission --id 5 | --name 设计师 --permissions 商品管理的查看,goodsEdit [--confirm]
156
+ scm-agent role remove-permission --id 5 | --name 设计师 --permissions 商品管理的查看 [--confirm]
157
+ ```
158
+
159
+ `--permissions` 每项可用中文备注(网页权限不足提示引号内的整段文字)、英文权限 key 或菜单 id;解析顺序是「菜单 id → 权限 key → 备注精确 → 唯一包含」,解析失败列出候选。
160
+
161
+ **权限备注会重名**:真实环境 931 项菜单里有 48 个重名备注,最典型的是「商品管理的查看」同时对应按设计师(`goodsGet`)、跟单员(`goodsGetByFollower`)、品牌(`goodsGetByBrand`)、设计助理(`goodsGetByDesignAssistant`)判断四个变体。备注命中多项时命令不猜,报错列出带标题、权限 key、菜单 id 的候选,改用权限 key 或菜单 id 重新提交即可。预览里每条权限也按「备注|标题|权限key|id」完整展示,避免用户确认时分不清是哪一个。
162
+
163
+ 权限增减命令**默认只预览**:展示角色、将要增加/移除的权限、现有权限数、改动后权限数与**受影响用户名单**(改角色权限会影响该角色下全部用户);**加 `--confirm` 才执行**。系统内置角色(`role list` 中类型为「系统」)不能修改。服务端的联动权限校验(勾选 A 必须同时勾选 B)报错原样返回。
164
+
165
+ ## permission — 权限菜单查询
166
+
167
+ ```
168
+ scm-agent permission list [--keyword 查看]
169
+ ```
170
+
171
+ 权限菜单全量列表:中文备注、功能标题、权限 key、菜单 id 的对照。用于查找权限的确切名称与 id。
172
+
173
+ ## 错误码
174
+
175
+ | 码 | 含义 | 处理 |
176
+ |---|---|---|
177
+ | 481 | Key 无效/已过期/已禁用 | 回网页「AI 助手」页面重新生成或启用 |
178
+ | 482 | 权限不足 | 账号权限所致,找管理员 |
179
+ | 483 | Key 已吊销 | 回网页重新生成 |
180
+ | 484 | 请求过于频繁 | 等待一分钟后重试 |
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "@qinsilk/scm-agent-tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "秦丝生产ERP AI 助手 CLI:供 AI Agent 查询商品、物料、生产单、物料采购单数据",
5
+ "bin": {
6
+ "scm-agent": "./cli/scm-agent.js"
7
+ },
8
+ "files": [
9
+ "cli/",
10
+ "skills/",
11
+ "reports/",
12
+ "docs/cli.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "test": "node --test test/cli.test.js"
19
+ },
20
+ "dependencies": {
21
+ "exceljs": "^4.4.0",
22
+ "yargs": "^17.7.2"
23
+ },
24
+ "license": "UNLICENSED",
25
+ "private": false,
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
29
+ }
@@ -0,0 +1,107 @@
1
+ // Excel 生成公共封装:基于 exceljs,提供报表模块共用的表头样式、边框、
2
+ // 红色标注、列宽、嵌图与保存能力。各报表只描述「列与数据」,样式细节统一在这里维护。
3
+ const ExcelJS = require('exceljs');
4
+
5
+ /** 表头行样式:加粗、浅灰底、细边框 */
6
+ const HEADER_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF2F2F2' } };
7
+ const THIN_BORDER = {
8
+ top: { style: 'thin', color: { argb: 'FFCCCCCC' } },
9
+ left: { style: 'thin', color: { argb: 'FFCCCCCC' } },
10
+ bottom: { style: 'thin', color: { argb: 'FFCCCCCC' } },
11
+ right: { style: 'thin', color: { argb: 'FFCCCCCC' } },
12
+ };
13
+
14
+ /** 新建工作簿并设置默认字体(Excel 打开中文正常显示) */
15
+ function createWorkbook() {
16
+ const workbook = new ExcelJS.Workbook();
17
+ workbook.creator = 'scm-agent';
18
+ return workbook;
19
+ }
20
+
21
+ /**
22
+ * 写一张标准数据表:
23
+ * - 第一行表头(headers 数组,或对象列定义 { header, width })
24
+ * - 之后每行按 rows 二维数组或对象数组(配合 keys)写入
25
+ * 返回 worksheet,调用方可以继续做合并、嵌图、特殊标色。
26
+ */
27
+ function addSheet(workbook, name, headers, rows, { keys, headerRowHeight = 22 } = {}) {
28
+ const sheet = workbook.addWorksheet(name, {
29
+ views: [{ state: 'frozen', ySplit: 1 }],
30
+ });
31
+ sheet.addRow(headers.map((h) => (typeof h === 'object' ? h.header : h)));
32
+ const headerRow = sheet.getRow(1);
33
+ headerRow.height = headerRowHeight;
34
+ headerRow.font = { bold: true };
35
+ headerRow.eachCell((cell) => {
36
+ cell.fill = HEADER_FILL;
37
+ cell.border = THIN_BORDER;
38
+ cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
39
+ });
40
+ headers.forEach((h, i) => {
41
+ sheet.getColumn(i + 1).width = (typeof h === 'object' ? h.width : undefined) || 14;
42
+ });
43
+ for (const row of rows) {
44
+ const values = keys ? keys.map((k) => (typeof k === 'function' ? k(row) : row[k])) : row;
45
+ const added = sheet.addRow(values);
46
+ added.eachCell((cell) => {
47
+ cell.border = THIN_BORDER;
48
+ cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
49
+ });
50
+ }
51
+ return sheet;
52
+ }
53
+
54
+ /**
55
+ * 把某一行整行或部分单元格字体标红(原 Python 报表的「需跟进/差异」红色标注)。
56
+ * rowIndex 从 1 开始(与 exceljs 一致,含表头行)。
57
+ */
58
+ function markRowRed(sheet, rowIndex, { columns } = {}) {
59
+ const row = sheet.getRow(rowIndex);
60
+ row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
61
+ if (!columns || columns.includes(colNumber)) {
62
+ cell.font = { ...(cell.font || {}), color: { argb: 'FFCC0000' }, bold: true };
63
+ }
64
+ });
65
+ }
66
+
67
+ /** 单元格字体标色:color 形如 'FFCC0000'(ARGB) */
68
+ function markCellColor(sheet, rowIndex, colIndex, argb, { bold = true } = {}) {
69
+ const cell = sheet.getRow(rowIndex).getCell(colIndex);
70
+ cell.font = { ...(cell.font || {}), color: { argb }, bold };
71
+ }
72
+
73
+ /**
74
+ * 在单元格嵌图。image 是 downloadImage 的返回值 { buffer, extension }。
75
+ * ext 作为 exceljs 的 base64 前缀依据。
76
+ */
77
+ function embedImage(workbook, sheet, image, rowIndex, colIndex, { width = 60, height = 60 } = {}) {
78
+ if (!image) {
79
+ return;
80
+ }
81
+ const imageId = workbook.addImage({
82
+ buffer: image.buffer,
83
+ extension: image.extension === 'jpg' ? 'jpeg' : image.extension,
84
+ });
85
+ sheet.addImage(imageId, {
86
+ tl: { row: rowIndex - 1, col: colIndex - 1 },
87
+ ext: { width, height },
88
+ });
89
+ }
90
+
91
+ /** 保存文件。目录不存在时自动创建 */
92
+ async function saveWorkbook(workbook, filePath) {
93
+ const fs = require('fs');
94
+ const path = require('path');
95
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
96
+ await workbook.xlsx.writeFile(filePath);
97
+ return filePath;
98
+ }
99
+
100
+ module.exports = {
101
+ createWorkbook,
102
+ addSheet,
103
+ markRowRed,
104
+ markCellColor,
105
+ embedImage,
106
+ saveWorkbook,
107
+ };
@@ -0,0 +1,128 @@
1
+ // 工厂评估报表:按加工厂汇总生产明细(单数、下单数、裁床数、完成件数、按时交货率)。
2
+ // 聚合由服务端完成,这里只分页拉取汇总结果并按原脚本的列布局写 Excel。
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { call } = require('../cli/api');
6
+ const { dateStr, shiftDate, timestamp, fetchAll } = require('./util');
7
+ const { createWorkbook, saveWorkbook } = require('./excel');
8
+
9
+ // 汇总说明行文字:解释按时交货率的分母口径,与原脚本一致
10
+ const RULE_TEXT = '汇总规则:按时交货率 = 按时交货单数 / 预计交货日期早于今天的生产单数;预计交货日期为空或未到期不计入分母';
11
+
12
+ // 列定义:(标题, DTO 字段, 类型)。数值列右对齐,百分比列带两位小数百分比格式
13
+ const COLUMNS = [
14
+ { header: '工厂', field: 'factory', kind: 'text', width: 22 },
15
+ { header: '单数', field: 'orders', kind: 'number', width: 10 },
16
+ { header: '下单数', field: 'sumNumber', kind: 'number', width: 12 },
17
+ { header: '裁床数', field: 'sumCutBedNumber', kind: 'number', width: 12 },
18
+ { header: '完成件数(生产入库单)', field: 'produceStoreInNumber', kind: 'number', width: 22 },
19
+ { header: '完成件数(成品入库)', field: 'produceInNumber', kind: 'number', width: 20 },
20
+ { header: '按时交货率', field: 'onTimeRate', kind: 'percent', width: 12 },
21
+ ];
22
+
23
+ const BORDER = {
24
+ top: { style: 'thin', color: { argb: 'FF000000' } },
25
+ left: { style: 'thin', color: { argb: 'FF000000' } },
26
+ bottom: { style: 'thin', color: { argb: 'FF000000' } },
27
+ right: { style: 'thin', color: { argb: 'FF000000' } },
28
+ };
29
+ // 表头与标题底色:浅蓝,与原脚本一致
30
+ const HEADER_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8F1FB' } };
31
+
32
+ /** 数值列的空值兜底为 0(与原脚本的 number() 一致) */
33
+ function number(value) {
34
+ const parsed = Number(value);
35
+ return Number.isFinite(parsed) ? parsed : 0;
36
+ }
37
+
38
+ /**
39
+ * 生成工厂评估报表 Excel。
40
+ * argv.from/to:业务日期区间 yyyy-MM-dd;默认结束日为今天、起始日往前 30 个日历日
41
+ * (即起始日 = 今天 - 29 天,与原脚本和后端默认口径一致)。
42
+ */
43
+ async function generate(argv = {}) {
44
+ const to = argv.to || dateStr();
45
+ const from = argv.from || shiftDate(-29, new Date(`${to}T00:00:00`));
46
+ if (from > to) {
47
+ throw new Error('起始日期不能晚于结束日期');
48
+ }
49
+
50
+ const outDir = argv.out || process.cwd();
51
+ const filePath = path.resolve(outDir, `工厂评估报表${timestamp()}.xlsx`);
52
+ if (fs.existsSync(filePath)) {
53
+ throw new Error(`输出文件已存在:${filePath};如需替换请先删除旧文件或更换输出目录`);
54
+ }
55
+
56
+ // 服务端按加工厂聚合并排序(单数降序、工厂名升序),这里分页取全量汇总行
57
+ const { list: summary } = await fetchAll(
58
+ (page, limit) => call('GET', '/api/agent/v1/report/factory-evaluation', { from, to, page, limit })
59
+ );
60
+
61
+ const workbook = createWorkbook();
62
+ const sheet = workbook.addWorksheet('工厂评估报表', {
63
+ views: [{ state: 'frozen', ySplit: 3 }],
64
+ pageSetup: { orientation: 'landscape', fitToPage: true, fitToWidth: 1, fitToHeight: 0 },
65
+ });
66
+
67
+ // 第 1 行:表标题,整行合并
68
+ const titleRow = sheet.addRow(['工厂评估报表']);
69
+ titleRow.height = 26;
70
+ titleRow.getCell(1).font = { bold: true };
71
+ titleRow.getCell(1).fill = HEADER_FILL;
72
+ titleRow.getCell(1).alignment = { vertical: 'middle', horizontal: 'center' };
73
+
74
+ // 第 2 行:汇总规则说明,左对齐整行合并
75
+ const ruleRow = sheet.addRow([RULE_TEXT]);
76
+ ruleRow.height = 20;
77
+ ruleRow.getCell(1).alignment = { vertical: 'middle', horizontal: 'left', wrapText: true };
78
+
79
+ // 第 3 行:表头
80
+ const headerRow = sheet.addRow(COLUMNS.map((column) => column.header));
81
+ headerRow.height = 30;
82
+ headerRow.eachCell((cell) => {
83
+ cell.font = { bold: true };
84
+ cell.fill = HEADER_FILL;
85
+ cell.border = BORDER;
86
+ cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
87
+ });
88
+
89
+ // 数据行:按时交货率为空(无已到期生产单)时写文本「-」,不能写成 0
90
+ for (const item of summary) {
91
+ const values = COLUMNS.map((column) => {
92
+ if (column.kind === 'percent') {
93
+ return item.onTimeRate == null ? '-' : number(item.onTimeRate) / 100;
94
+ }
95
+ return column.kind === 'text' ? item[column.field] : number(item[column.field]);
96
+ });
97
+ const row = sheet.addRow(values);
98
+ row.height = 20;
99
+ row.eachCell((cell, colNumber) => {
100
+ const kind = COLUMNS[colNumber - 1].kind;
101
+ cell.border = BORDER;
102
+ cell.alignment = {
103
+ vertical: 'middle',
104
+ horizontal: kind === 'text' ? 'left' : 'right',
105
+ wrapText: kind === 'text',
106
+ };
107
+ if (kind === 'percent' && cell.value !== '-') {
108
+ cell.numFmt = '0.00%';
109
+ }
110
+ });
111
+ }
112
+
113
+ COLUMNS.forEach((column, i) => {
114
+ sheet.getColumn(i + 1).width = column.width;
115
+ });
116
+ sheet.mergeCells(1, 1, 1, COLUMNS.length);
117
+ sheet.mergeCells(2, 1, 2, COLUMNS.length);
118
+
119
+ await saveWorkbook(workbook, filePath);
120
+
121
+ const orderTotal = summary.reduce((sum, item) => sum + number(item.orders), 0);
122
+ return {
123
+ filePaths: [filePath],
124
+ summary: `工厂评估报表已生成:业务日期 ${from} ~ ${to},生产单 ${orderTotal} 张汇总为 ${summary.length} 个加工厂`,
125
+ };
126
+ }
127
+
128
+ module.exports = { generate };