@crabcloud/cli 0.5.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crab.js +1984 -202
- package/dist/skills/crabcloud/SKILL.md +16 -5
- package/dist/skills/crabcloud-hr/SKILL.md +77 -19
- package/dist/skills/crabcloud-shop/SKILL.md +29 -9
- package/package.json +1 -1
package/dist/crab.js
CHANGED
|
@@ -6091,16 +6091,18 @@ var SHOP_PERMISSIONS = ["products", "orders", "aftercare", "customers", "marketi
|
|
|
6091
6091
|
var shopCategoryMoveInput = object({
|
|
6092
6092
|
direction: _enum(["up", "down"])
|
|
6093
6093
|
});
|
|
6094
|
-
var shopSpecGroupCreateInput = object({
|
|
6095
|
-
name: string2().trim().min(1).max(30),
|
|
6096
|
-
values: array(string2().trim().min(1).max(60)).min(1).max(20)
|
|
6097
|
-
});
|
|
6098
|
-
var shopSpecGroupUpdateInput = shopSpecGroupCreateInput.partial();
|
|
6099
6094
|
var comboValues = array(string2().min(1).max(60)).min(1).max(24);
|
|
6100
6095
|
var shopStoreCreateInput = object({
|
|
6101
6096
|
name: string2().min(1).max(60),
|
|
6102
6097
|
tagline: string2().max(200).default("")
|
|
6103
6098
|
});
|
|
6099
|
+
var shopStoreUpdateInput = object({
|
|
6100
|
+
name: string2().min(1).max(60),
|
|
6101
|
+
tagline: string2().max(200),
|
|
6102
|
+
description: string2().max(2000),
|
|
6103
|
+
logoObjectId: string2().min(1).nullable(),
|
|
6104
|
+
skuPrefix: string2().regex(/^[A-Za-z0-9]{0,8}$/)
|
|
6105
|
+
}).partial();
|
|
6104
6106
|
var shopCategoryCreateInput = object({
|
|
6105
6107
|
name: string2().min(1).max(60),
|
|
6106
6108
|
parentId: string2().min(1).nullish()
|
|
@@ -6111,31 +6113,61 @@ var shopCategoryUpdateInput = object({
|
|
|
6111
6113
|
}).partial();
|
|
6112
6114
|
var shopVariantInput = object({
|
|
6113
6115
|
combo: comboValues,
|
|
6114
|
-
priceCents: number2().int().min(0).max(1e8),
|
|
6116
|
+
priceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6117
|
+
compareAtPriceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6115
6118
|
stock: number2().int().min(0).max(1e7).nullish(),
|
|
6116
|
-
enabled: boolean2().default(true)
|
|
6119
|
+
enabled: boolean2().default(true),
|
|
6120
|
+
code: string2().max(64).nullish()
|
|
6117
6121
|
});
|
|
6118
6122
|
var shopProductCreateInput = object({
|
|
6119
6123
|
kind: _enum(["digital", "physical"]),
|
|
6124
|
+
variantMode: _enum(["single", "multi"]),
|
|
6120
6125
|
name: string2().min(1).max(140),
|
|
6121
6126
|
description: string2().max(2000).default(""),
|
|
6127
|
+
content: string2().max(30000).default(""),
|
|
6122
6128
|
categoryId: string2().min(1).nullish(),
|
|
6123
6129
|
status: _enum(["draft", "active", "archived"]).default("draft"),
|
|
6124
6130
|
priceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6131
|
+
compareAtPriceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6125
6132
|
stock: number2().int().min(0).max(1e7).nullish(),
|
|
6133
|
+
refundable: boolean2().nullish(),
|
|
6126
6134
|
deliveryType: _enum(["file", "license"]).nullish(),
|
|
6127
6135
|
deliveryFileId: string2().min(1).nullish(),
|
|
6136
|
+
imageObjectId: string2().min(1).nullish(),
|
|
6137
|
+
gallery: array(string2().min(1)).max(8).nullish(),
|
|
6128
6138
|
specAxes: array(object({ name: string2().min(1).max(30), values: comboValues })).max(3).nullish(),
|
|
6129
|
-
|
|
6130
|
-
|
|
6139
|
+
variants: array(shopVariantInput).max(24).nullish(),
|
|
6140
|
+
code: string2().max(64).nullish(),
|
|
6141
|
+
maxPerOrder: number2().int().min(1).max(1e4).nullish(),
|
|
6142
|
+
maxPerBuyer: number2().int().min(1).max(1e4).nullish()
|
|
6143
|
+
});
|
|
6144
|
+
var shopProductUpdateInput = shopProductCreateInput.extend({
|
|
6145
|
+
status: _enum(["draft", "active", "archived"]).optional(),
|
|
6146
|
+
description: string2().max(2000).optional(),
|
|
6147
|
+
content: string2().max(30000).optional()
|
|
6148
|
+
}).partial();
|
|
6149
|
+
var shopStockAdjustTarget = object({
|
|
6150
|
+
variantId: string2().min(1).nullish(),
|
|
6151
|
+
amount: number2().int().min(0).max(1e7)
|
|
6131
6152
|
});
|
|
6132
|
-
var shopProductUpdateInput = shopProductCreateInput.partial();
|
|
6133
6153
|
var shopStockAdjustInput = object({
|
|
6134
6154
|
mode: _enum(["in", "out", "set"]),
|
|
6135
|
-
amount: number2().int().min(0).max(1e7),
|
|
6136
|
-
variantId: string2().min(1).nullish(),
|
|
6137
6155
|
reason: _enum(["purchase", "manual"]).nullish(),
|
|
6138
|
-
note: string2().max(200).nullish()
|
|
6156
|
+
note: string2().max(200).nullish(),
|
|
6157
|
+
targets: array(shopStockAdjustTarget).min(1).max(50)
|
|
6158
|
+
}).superRefine((val, ctx) => {
|
|
6159
|
+
if (val.reason === "purchase" && val.mode !== "in") {
|
|
6160
|
+
ctx.addIssue({ code: "custom", path: ["reason"], message: "reason 'purchase' only applies to inbound" });
|
|
6161
|
+
}
|
|
6162
|
+
if (val.mode !== "set" && val.targets.some((t) => t.amount === 0)) {
|
|
6163
|
+
ctx.addIssue({ code: "custom", path: ["targets"], message: "amount must be positive for in/out" });
|
|
6164
|
+
}
|
|
6165
|
+
});
|
|
6166
|
+
var shopListedStockInput = object({
|
|
6167
|
+
targets: array(object({
|
|
6168
|
+
variantId: string2().min(1).nullish(),
|
|
6169
|
+
stock: number2().int().min(0).max(1e7)
|
|
6170
|
+
})).min(1).max(50)
|
|
6139
6171
|
});
|
|
6140
6172
|
var shopStripeConnectInput = object({
|
|
6141
6173
|
secretKey: string2().min(1).max(255).regex(/^(sk|rk)_(test|live)_/, "must be a Stripe secret key (sk_…/rk_…)"),
|
|
@@ -6219,15 +6251,20 @@ var hrEventTypes = [
|
|
|
6219
6251
|
var hrTaskStatuses = ["alert", "confirm", "queued", "done"];
|
|
6220
6252
|
var hrTaskDecisions = ["approved", "rejected"];
|
|
6221
6253
|
var optionalDay = number2().int().positive().nullable().optional();
|
|
6222
|
-
var hrOrgCreateInput = object({
|
|
6223
|
-
name: string2().trim().min(1).max(80)
|
|
6224
|
-
});
|
|
6225
6254
|
var hrOrgUpdateInput = object({
|
|
6226
6255
|
name: string2().trim().min(1).max(80)
|
|
6227
6256
|
});
|
|
6228
6257
|
var hrMemberAddInput = object({
|
|
6229
6258
|
username: string2().trim().toLowerCase().min(3).max(32),
|
|
6230
|
-
title: string2().trim().max(40).optional()
|
|
6259
|
+
title: string2().trim().max(40).optional(),
|
|
6260
|
+
permissions: record(_enum(hrPermissionFlags), boolean2()).optional()
|
|
6261
|
+
});
|
|
6262
|
+
var hrInviteUpdateInput = object({
|
|
6263
|
+
permissions: record(_enum(hrPermissionFlags), boolean2())
|
|
6264
|
+
});
|
|
6265
|
+
var hrOrgApplicationCreateInput = object({
|
|
6266
|
+
name: string2().trim().min(1).max(80),
|
|
6267
|
+
note: string2().trim().max(200).default("")
|
|
6231
6268
|
});
|
|
6232
6269
|
var hrMemberUpdateInput = object({
|
|
6233
6270
|
role: _enum(["admin", "member"]).optional(),
|
|
@@ -6398,6 +6435,10 @@ var scopes = [
|
|
|
6398
6435
|
"storage.delete",
|
|
6399
6436
|
"contacts.read",
|
|
6400
6437
|
"contacts.write",
|
|
6438
|
+
"addresses.read",
|
|
6439
|
+
"addresses.write",
|
|
6440
|
+
"favorites.read",
|
|
6441
|
+
"favorites.write",
|
|
6401
6442
|
"collab.read",
|
|
6402
6443
|
"collab.write",
|
|
6403
6444
|
"hr.read",
|
|
@@ -6411,6 +6452,8 @@ var scopeGroups = [
|
|
|
6411
6452
|
{ id: "storage", scopes: ["storage.read", "storage.write", "storage.delete"] },
|
|
6412
6453
|
{ id: "collab", scopes: ["collab.read", "collab.write"] },
|
|
6413
6454
|
{ id: "contacts", scopes: ["contacts.read", "contacts.write"] },
|
|
6455
|
+
{ id: "addresses", scopes: ["addresses.read", "addresses.write"] },
|
|
6456
|
+
{ id: "favorites", scopes: ["favorites.read", "favorites.write"] },
|
|
6414
6457
|
{ id: "hr", scopes: ["hr.read", "hr.write"] },
|
|
6415
6458
|
{ id: "agent", scopes: ["agent.read", "agent.write"] }
|
|
6416
6459
|
];
|
|
@@ -6509,7 +6552,7 @@ var agentTokenSummarySchema = object({
|
|
|
6509
6552
|
scopes: array(_enum(scopes)),
|
|
6510
6553
|
collabSpace: string2().nullable().optional(),
|
|
6511
6554
|
hrOrg: string2().nullable().optional(),
|
|
6512
|
-
kind: _enum(["agent", "api"]).optional(),
|
|
6555
|
+
kind: _enum(["agent", "api", "mobile"]).optional(),
|
|
6513
6556
|
keyHint: string2().optional(),
|
|
6514
6557
|
createdAt: number2(),
|
|
6515
6558
|
lastUsedAt: number2().nullable(),
|
|
@@ -6520,6 +6563,21 @@ var apiKeyCreateInput = object({
|
|
|
6520
6563
|
name: string2().trim().min(1).max(64),
|
|
6521
6564
|
scopes: array(string2().trim().min(1).max(40)).min(1).max(32)
|
|
6522
6565
|
});
|
|
6566
|
+
var mobileTokenCreateInput = object({
|
|
6567
|
+
name: string2().trim().min(1).max(64)
|
|
6568
|
+
});
|
|
6569
|
+
var agentPushPlatforms = ["android", "ios", "web"];
|
|
6570
|
+
var agentPushPrefsSchema = object({
|
|
6571
|
+
approvals: boolean2().optional(),
|
|
6572
|
+
tasks: boolean2().optional(),
|
|
6573
|
+
cron: boolean2().optional()
|
|
6574
|
+
});
|
|
6575
|
+
var agentPushRegisterInput = object({
|
|
6576
|
+
platform: _enum(agentPushPlatforms),
|
|
6577
|
+
token: string2().min(10).max(2000),
|
|
6578
|
+
name: string2().trim().max(64).optional(),
|
|
6579
|
+
prefs: agentPushPrefsSchema.optional()
|
|
6580
|
+
});
|
|
6523
6581
|
var oauthProviders = ["google", "reai", "github"];
|
|
6524
6582
|
var oauthUnbindInput = object({ provider: _enum(oauthProviders) });
|
|
6525
6583
|
var oauthOnboardingConfirmInput = object({
|
|
@@ -6572,6 +6630,9 @@ var adminShopStoreStatusInput = object({
|
|
|
6572
6630
|
var adminShopApplicationRejectInput = object({
|
|
6573
6631
|
note: string2().trim().min(1).max(500)
|
|
6574
6632
|
});
|
|
6633
|
+
var adminHrApplicationRejectInput = object({
|
|
6634
|
+
note: string2().trim().min(1).max(500)
|
|
6635
|
+
});
|
|
6575
6636
|
var subscriptionChangeInput = object({
|
|
6576
6637
|
tierId: string2().trim().min(1).max(50)
|
|
6577
6638
|
});
|
|
@@ -6686,6 +6747,27 @@ var contactQueryInput = object({
|
|
|
6686
6747
|
limit: number2().int().min(1).max(50).default(20),
|
|
6687
6748
|
offset: number2().int().min(0).default(0)
|
|
6688
6749
|
});
|
|
6750
|
+
var addressCreateInput = object({
|
|
6751
|
+
name: string2().trim().min(1).max(64),
|
|
6752
|
+
phone: string2().trim().min(1).max(32),
|
|
6753
|
+
region: string2().trim().max(128).optional().default(""),
|
|
6754
|
+
detail: string2().trim().min(1).max(256),
|
|
6755
|
+
isDefault: boolean2().optional().default(false)
|
|
6756
|
+
});
|
|
6757
|
+
var addressUpdateInput = object({
|
|
6758
|
+
name: string2().trim().min(1).max(64).optional(),
|
|
6759
|
+
phone: string2().trim().min(1).max(32).optional(),
|
|
6760
|
+
region: string2().trim().max(128).optional(),
|
|
6761
|
+
detail: string2().trim().min(1).max(256).optional(),
|
|
6762
|
+
isDefault: boolean2().optional()
|
|
6763
|
+
});
|
|
6764
|
+
var FAVORITE_KINDS = ["shop.product"];
|
|
6765
|
+
var favoriteKindSchema = _enum(FAVORITE_KINDS);
|
|
6766
|
+
var favoritePutInput = object({
|
|
6767
|
+
kind: favoriteKindSchema,
|
|
6768
|
+
refId: string2().trim().min(1).max(64),
|
|
6769
|
+
meta: record(string2(), unknown()).optional().default({})
|
|
6770
|
+
});
|
|
6689
6771
|
var collabTaskStatuses = ["open", "done"];
|
|
6690
6772
|
var collabBoostKinds = ["tasks", "members"];
|
|
6691
6773
|
var COLLAB_RECORD_BODY_MAX = 20000;
|
|
@@ -7258,6 +7340,22 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
7258
7340
|
contacts invite [--reset] [--json] 我的邀请码、链接与统计(--reset 重置,旧链接失效)
|
|
7259
7341
|
contacts invitees [--offset N] [--json]
|
|
7260
7342
|
我邀请的人(直接下级 + 间接计数)
|
|
7343
|
+
addresses list [--json] 收货地址簿(默认地址在前,上限 20 条)
|
|
7344
|
+
addresses add --name 收件人 --phone 电话 --detail 详址 [--region 省/市/区] [--default] [--json]
|
|
7345
|
+
添加地址(--default 同时清掉旧默认)
|
|
7346
|
+
addresses update <id|名称> [--name] [--phone] [--region] [--detail] [--default|--no-default] [--json]
|
|
7347
|
+
更新地址(只改给出的字段)
|
|
7348
|
+
addresses set-default <id|名称> 设为默认地址(服务端保证默认唯一,旧默认自动让位)
|
|
7349
|
+
addresses rm <id|名称> 删除地址(应用侧已落库的快照不受影响)
|
|
7350
|
+
favorites list [--kind shop.product] [--offset N] [--json]
|
|
7351
|
+
收藏列表(kind 点分式「模块.资源」,v1 仅 shop.product)
|
|
7352
|
+
favorites add <refId> [--kind shop.product] [--meta productName=名称,storeId=店铺id] [--json]
|
|
7353
|
+
收藏(幂等,重复执行返回现有条目;meta 是收藏时快照,
|
|
7354
|
+
shop.product 建议 productName + storeId 供展示兜底)
|
|
7355
|
+
favorites check <refId> [--kind shop.product] [--json]
|
|
7356
|
+
查询单条是否已收藏
|
|
7357
|
+
favorites rm <refId> [--kind shop.product]
|
|
7358
|
+
取消收藏(幂等,不存在也成功)
|
|
7261
7359
|
storage ls [--q 关键字] [--source files|media|mail|collab|agent] [--category <id|none>]
|
|
7262
7360
|
[--type doc|image|video|archive|code] [--tag a,b] [--trash]
|
|
7263
7361
|
[--shared] [--recent] [--encrypted only|plain] [--message-id <id>]
|
|
@@ -7320,16 +7418,23 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
7320
7418
|
post <taskId> (--body "..." | --body-file <path>) [--attach <fileId,...>] [--link <url,...>] [--json]
|
|
7321
7419
|
推送任务记录(糖命令:等价 collab task records 的写入)
|
|
7322
7420
|
hr orgs [--json] 我的组织列表(hr 命令需令牌绑定组织:login --scopes hr --org org:<名称>)
|
|
7323
|
-
hr org
|
|
7421
|
+
hr org apply <名称> [--note 说明] [--json]
|
|
7422
|
+
申请创建公司(审核-计费制:通过时扣 1 积分自动建司)
|
|
7423
|
+
hr org application [--json] 查我的建司申请进度(pending / approved / rejected)
|
|
7424
|
+
hr org rename <新名称> [--org <arg>] [--json]
|
|
7425
|
+
组织改名(org.manage)
|
|
7324
7426
|
hr members [--org <arg>] [--json] 成员列表(角色与权限 flags)
|
|
7325
|
-
hr member add <用户名> [--title 头衔] [--org <arg>]
|
|
7326
|
-
|
|
7427
|
+
hr member add <用户名> [--title 头衔] [--permissions org.manage=1,...] [--org <arg>]
|
|
7428
|
+
邀请平台账号入组织(默认只读,--permissions 定生效权限)
|
|
7327
7429
|
hr member set <memberId> [--role admin|member] [--permissions org.manage=1,...] [--title]
|
|
7328
7430
|
角色与权限逐项配置(owner 恒全权不可改)
|
|
7329
7431
|
hr member rm <memberId> 移除成员
|
|
7330
|
-
hr invite [--reset] [--org <arg>]
|
|
7432
|
+
hr invite [--permissions org.manage=1,...] [--reset] [--org <arg>]
|
|
7433
|
+
常驻邀请链接(/join 加入页;--permissions 改加入后默认权限,
|
|
7434
|
+
未指定项视为关;--reset 作废旧链,配置保留)
|
|
7331
7435
|
hr people [--q 关键字] [--status active|probation|resigned] [--dept] [--limit N] [--offset N] [--json]
|
|
7332
7436
|
花名册(身份证/薪资对令牌永不外泄,输出如实按无权限转述)
|
|
7437
|
+
hr stats [--org <arg>] [--json] 花名册统计带(在职/试用/离职 + 合同与试用期风险计数)
|
|
7333
7438
|
hr person <id> [--json] 人员详情
|
|
7334
7439
|
hr person create --name "姓名" --hired-at YYYY-MM-DD [--dept] [--role] [--probation-end] [--contract-type] [--contract-end] [--mobile] [--email] [--notes] [--json]
|
|
7335
7440
|
建档(自动落入职事件)
|
|
@@ -7337,19 +7442,36 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
7337
7442
|
编辑(转正/离职状态流转自动落痕)
|
|
7338
7443
|
hr person offboard <id> [--date YYYY-MM-DD] [--json]
|
|
7339
7444
|
办理离职(档案封存,记录依法保留)
|
|
7445
|
+
hr person rm <id> 删除人员(仅零记录档案可删;有事件时间线一律 409)
|
|
7340
7446
|
hr event add <personId> --type <onboard|contract|change|attendance|payroll|award|asset|offboard|note> --note "..." [--at YYYY-MM-DD] [--json]
|
|
7341
7447
|
事件落痕(时间线即证据链)
|
|
7342
7448
|
hr events [--person <id>] [--type] [--limit N] [--offset N] [--json]
|
|
7343
7449
|
事件时间线
|
|
7344
7450
|
hr import <文件.csv|.tsv|.json> [--org <arg>] [--json]
|
|
7345
7451
|
批量建档(≤200 行;CSV/TSV 表头自动识别,姓名/入职列必填)
|
|
7452
|
+
hr dicts [--org <arg>] [--json] 字典全量(部门 / 岗位)
|
|
7453
|
+
hr dict add <dept|position> <名称> [--org <arg>] [--json]
|
|
7454
|
+
新增字典项(在用项禁删,服务端 409)
|
|
7455
|
+
hr dict rm <entryId> [--org <arg>] 删除字典项
|
|
7346
7456
|
hr tasks [--status alert|confirm|queued|done] [--kind] [--limit N] [--offset N] [--json]
|
|
7347
7457
|
任务列表
|
|
7348
7458
|
hr task create --kind <attendance|payroll|contract|...> --title "..." [--node] [--detail] [--due YYYY-MM-DD] [--json]
|
|
7349
7459
|
新建任务
|
|
7350
7460
|
hr task update <id> [--status ...] [--node] [--detail] [--due YYYY-MM-DD] [--json]
|
|
7351
7461
|
任务流转
|
|
7352
|
-
hr
|
|
7462
|
+
hr task link <taskId> [--reset] [--org <arg>] [--json]
|
|
7463
|
+
申请单免登录填单链接(入职/转正/离职单发候选人;--reset 作废旧链)
|
|
7464
|
+
hr apply-link [--reset] [--org <arg>] [--json]
|
|
7465
|
+
组织常驻入职收集链接(候选人提交即自动生成入职申请单)
|
|
7466
|
+
hr announce list [--all] [--limit N] [--offset N] [--org <arg>] [--json]
|
|
7467
|
+
公告列表(默认只看生效;--all 含已撤回)
|
|
7468
|
+
hr announce post <标题> --body "..." [--org <arg>] [--json]
|
|
7469
|
+
发布公告(org.manage;对象 = 组织全体成员)
|
|
7470
|
+
hr announce withdraw <公告id> [--org <arg>] [--json]
|
|
7471
|
+
撤回公告(发布者本人或 org.manage;留档不删)
|
|
7472
|
+
hr inbox [--seen] [--org <arg>] [--json]
|
|
7473
|
+
收件箱聚合(未读公告 + 待拍板任务 + 提醒;--seen 标记已读)
|
|
7474
|
+
hr desk [--org <arg>] [--json] 工作台聚合(Agent 首入口:统计 + 提醒 + 任务三段 + 公告 + 动态,一次取全)
|
|
7353
7475
|
shop status [--json] 店铺列表 + 开通申请进度(多店全列,★ = 当前店)
|
|
7354
7476
|
shop use <slug> 切换当前管理的店铺(持久保存;店级命令缺省作用于它,
|
|
7355
7477
|
--store 可单条临时覆盖;不带参数显示当前店)
|
|
@@ -7374,12 +7496,37 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
7374
7496
|
同级内上移/下移一位(边界静默不动)
|
|
7375
7497
|
shop category delete <id或名称> --store <slug> [--json]
|
|
7376
7498
|
删除分类(有子级拒绝;叶子删除后商品迁入未分类)
|
|
7377
|
-
shop
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
shop
|
|
7381
|
-
shop
|
|
7382
|
-
|
|
7499
|
+
shop products [--status draft|active|archived] [--category <id|名称>] [--q 文本]
|
|
7500
|
+
[--limit N --offset N] [--json]
|
|
7501
|
+
商品列表(多规格行库存 = 变体合计;q 按名称/描述匹配)
|
|
7502
|
+
shop product <id|SKU码> [--json] 商品详情(先 id 直查,404 再按店内 SKU 码反查)
|
|
7503
|
+
shop product create --kind physical|digital --name 名称 [--variant-mode single|multi]
|
|
7504
|
+
[--spec-axis "颜色=红,蓝"]… [--price-cents N] [--compare-at-cents N]
|
|
7505
|
+
[--category <id|名称>] [--description …] [--code <SKU码>] [--stock N]
|
|
7506
|
+
[--refundable on|off] [--max-per-order N] [--max-per-buyer N]
|
|
7507
|
+
[--delivery file|license --delivery-file <对象id>] [--status draft|active] [--json]
|
|
7508
|
+
新建商品(类型与规格模式创建即锁定;多规格组合价
|
|
7509
|
+
后续 product spec 配置;金额一律整数分)
|
|
7510
|
+
shop product update <id|SKU码> [--name …] [--description …] [--category <id|名称|none>]
|
|
7511
|
+
[--price-cents N] [--compare-at-cents N|clear] [--status draft|active|archived]
|
|
7512
|
+
[--refundable on|off] [--max-per-order N|clear] [--max-per-buyer N|clear]
|
|
7513
|
+
[--code <SKU码>] [--json]
|
|
7514
|
+
更新商品(类型与规格模式不可变;改组合价用 product
|
|
7515
|
+
spec,改库存用 stock / listed)
|
|
7516
|
+
shop product spec <id|SKU码> --set "<值1,值2>=价格分[,划线价分][,on|off]" … [--json]
|
|
7517
|
+
多规格组合整组配置(值按轴顺序;未提及组合保持
|
|
7518
|
+
原值;不改库存;被台账引用的组合只能关不能删)
|
|
7519
|
+
shop product archive <id|SKU码> [--json]
|
|
7520
|
+
下架归档(软删;订单快照不受影响)
|
|
7521
|
+
shop stock in|out|set <id|SKU码> --amount N [--variant <组合码|变体id|"值1/值2">]
|
|
7522
|
+
[--reason purchase] --note "说明" [--json]
|
|
7523
|
+
库存动账(实物动仓库账;数字限量同命令走上架账;
|
|
7524
|
+
in/out ≥1;set 或未给 --reason 必须带 --note;
|
|
7525
|
+
退货入库/发货出库由售后与发货端点自动落账)
|
|
7526
|
+
shop listed <id|SKU码> --stock N [--variant <组合码|变体id|"值1/值2">] [--json]
|
|
7527
|
+
实物销售库存(上架额度)直设,0 ≤ N ≤ 实际库存
|
|
7528
|
+
shop movements [--product <id|SKU码>] [--limit N --offset N] [--json]
|
|
7529
|
+
出入库台账(新→旧;--product 按商品过滤)
|
|
7383
7530
|
credits [--json] 积分余额(月送/充值/合计;需 credits.read scope)
|
|
7384
7531
|
credits history [--limit N] [--offset N] [--json]
|
|
7385
7532
|
积分流水(变动 ±:消耗为负;需 credits.read)
|
|
@@ -8130,6 +8277,322 @@ async function contactsInvitees() {
|
|
|
8130
8277
|
console.log(`(还有更多:contacts invitees --offset ${page.nextOffset})`);
|
|
8131
8278
|
return 0;
|
|
8132
8279
|
}
|
|
8280
|
+
function resolveAddressArg(list, arg) {
|
|
8281
|
+
const byId = list.find((item) => item.id === arg);
|
|
8282
|
+
if (byId)
|
|
8283
|
+
return { ok: true, id: byId.id };
|
|
8284
|
+
const byName = list.filter((item) => item.name === arg);
|
|
8285
|
+
if (byName.length === 1)
|
|
8286
|
+
return { ok: true, id: byName[0].id };
|
|
8287
|
+
if (byName.length > 1) {
|
|
8288
|
+
return {
|
|
8289
|
+
ok: false,
|
|
8290
|
+
failure: "ambiguous",
|
|
8291
|
+
error: `有 ${byName.length} 条同名地址,请改用 id:
|
|
8292
|
+
` + byName.map((item) => ` ${item.id} ${item.name} ${item.phone} ${item.detail}`).join(`
|
|
8293
|
+
`)
|
|
8294
|
+
};
|
|
8295
|
+
}
|
|
8296
|
+
return { ok: false, failure: "missing", error: "地址不存在或已删除(crab addresses list 查看现有条目)。" };
|
|
8297
|
+
}
|
|
8298
|
+
async function requireAddressList() {
|
|
8299
|
+
const credentials = requireCredentials();
|
|
8300
|
+
if (typeof credentials === "number")
|
|
8301
|
+
return credentials;
|
|
8302
|
+
const res = await api2(credentials.apiBase, "/v1/addresses", { headers: authHeaders(credentials) });
|
|
8303
|
+
if (res.status !== 200) {
|
|
8304
|
+
console.error(errorMessage(res.body, "获取地址簿失败。"));
|
|
8305
|
+
return exitCodeForApiError(res.body);
|
|
8306
|
+
}
|
|
8307
|
+
return { credentials, list: res.body.data };
|
|
8308
|
+
}
|
|
8309
|
+
function addressLine(address) {
|
|
8310
|
+
const region = address.region ? `${address.region} · ` : "";
|
|
8311
|
+
return `${address.id} ${address.name} ${address.phone} ${region}${address.detail}${address.isDefault ? " [默认]" : ""}`;
|
|
8312
|
+
}
|
|
8313
|
+
async function addressesList() {
|
|
8314
|
+
const loaded = await requireAddressList();
|
|
8315
|
+
if (typeof loaded === "number")
|
|
8316
|
+
return loaded;
|
|
8317
|
+
if (hasFlag("--json")) {
|
|
8318
|
+
console.log(JSON.stringify(loaded.list, null, 2));
|
|
8319
|
+
return 0;
|
|
8320
|
+
}
|
|
8321
|
+
if (loaded.list.length === 0) {
|
|
8322
|
+
console.log("地址簿还是空的。");
|
|
8323
|
+
return 0;
|
|
8324
|
+
}
|
|
8325
|
+
for (const address of loaded.list)
|
|
8326
|
+
console.log(addressLine(address));
|
|
8327
|
+
console.log(`共 ${loaded.list.length} 条`);
|
|
8328
|
+
return 0;
|
|
8329
|
+
}
|
|
8330
|
+
function addressFieldFlags() {
|
|
8331
|
+
const body = {};
|
|
8332
|
+
let hasAny = false;
|
|
8333
|
+
for (const [flag, key] of [
|
|
8334
|
+
["--name", "name"],
|
|
8335
|
+
["--phone", "phone"],
|
|
8336
|
+
["--region", "region"],
|
|
8337
|
+
["--detail", "detail"]
|
|
8338
|
+
]) {
|
|
8339
|
+
const value = readFlag(flag);
|
|
8340
|
+
if (value !== undefined) {
|
|
8341
|
+
body[key] = value;
|
|
8342
|
+
hasAny = true;
|
|
8343
|
+
}
|
|
8344
|
+
}
|
|
8345
|
+
if (hasFlag("--default")) {
|
|
8346
|
+
body.isDefault = true;
|
|
8347
|
+
hasAny = true;
|
|
8348
|
+
} else if (hasFlag("--no-default")) {
|
|
8349
|
+
body.isDefault = false;
|
|
8350
|
+
hasAny = true;
|
|
8351
|
+
}
|
|
8352
|
+
return { body, hasAny };
|
|
8353
|
+
}
|
|
8354
|
+
async function addressesAdd() {
|
|
8355
|
+
const name = readFlag("--name");
|
|
8356
|
+
const phone = readFlag("--phone");
|
|
8357
|
+
const detail = readFlag("--detail");
|
|
8358
|
+
if (!name || !phone || !detail) {
|
|
8359
|
+
console.error("用法:crab addresses add --name 收件人 --phone 电话 --detail 详址 [--region 省/市/区] [--default] [--json]");
|
|
8360
|
+
return EXIT_USAGE;
|
|
8361
|
+
}
|
|
8362
|
+
const credentials = requireCredentials();
|
|
8363
|
+
if (typeof credentials === "number")
|
|
8364
|
+
return credentials;
|
|
8365
|
+
const { body } = addressFieldFlags();
|
|
8366
|
+
const res = await api2(credentials.apiBase, "/v1/addresses", {
|
|
8367
|
+
method: "POST",
|
|
8368
|
+
headers: authHeaders(credentials),
|
|
8369
|
+
body: JSON.stringify({ name, phone, detail, region: readFlag("--region") ?? "", ...body })
|
|
8370
|
+
});
|
|
8371
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
8372
|
+
console.error(errorMessage(res.body, "添加失败。"));
|
|
8373
|
+
return exitCodeForApiError(res.body);
|
|
8374
|
+
}
|
|
8375
|
+
const address = res.body.data;
|
|
8376
|
+
if (hasFlag("--json")) {
|
|
8377
|
+
console.log(JSON.stringify(address, null, 2));
|
|
8378
|
+
return 0;
|
|
8379
|
+
}
|
|
8380
|
+
console.log(`✓ 已添加:${address.name}(id ${address.id})${address.isDefault ? ",已设为默认" : ""}`);
|
|
8381
|
+
return 0;
|
|
8382
|
+
}
|
|
8383
|
+
async function addressesUpdate(idOrName) {
|
|
8384
|
+
if (!idOrName) {
|
|
8385
|
+
console.error("用法:crab addresses update <id|名称> [--name 名] [--phone 电话] [--region 省/市/区] [--detail 详址] [--default|--no-default] [--json]");
|
|
8386
|
+
return EXIT_USAGE;
|
|
8387
|
+
}
|
|
8388
|
+
const { body, hasAny } = addressFieldFlags();
|
|
8389
|
+
if (!hasAny) {
|
|
8390
|
+
console.error("没有给出要更新的字段(--name/--phone/--region/--detail/--default)。");
|
|
8391
|
+
return EXIT_USAGE;
|
|
8392
|
+
}
|
|
8393
|
+
const loaded = await requireAddressList();
|
|
8394
|
+
if (typeof loaded === "number")
|
|
8395
|
+
return loaded;
|
|
8396
|
+
const resolved = resolveAddressArg(loaded.list, idOrName);
|
|
8397
|
+
if (!resolved.ok) {
|
|
8398
|
+
console.error(resolved.error);
|
|
8399
|
+
return resolved.failure === "ambiguous" ? EXIT_USAGE : EXIT_NOT_FOUND;
|
|
8400
|
+
}
|
|
8401
|
+
const res = await api2(loaded.credentials.apiBase, `/v1/addresses/${encodeURIComponent(resolved.id)}`, {
|
|
8402
|
+
method: "PATCH",
|
|
8403
|
+
headers: authHeaders(loaded.credentials),
|
|
8404
|
+
body: JSON.stringify(body)
|
|
8405
|
+
});
|
|
8406
|
+
if (res.status !== 200) {
|
|
8407
|
+
console.error(errorMessage(res.body, "更新失败。"));
|
|
8408
|
+
return exitCodeForApiError(res.body);
|
|
8409
|
+
}
|
|
8410
|
+
const address = res.body.data;
|
|
8411
|
+
if (hasFlag("--json")) {
|
|
8412
|
+
console.log(JSON.stringify(address, null, 2));
|
|
8413
|
+
return 0;
|
|
8414
|
+
}
|
|
8415
|
+
console.log(`✓ 已更新:${addressLine(address)}`);
|
|
8416
|
+
return 0;
|
|
8417
|
+
}
|
|
8418
|
+
async function addressesSetDefault(idOrName) {
|
|
8419
|
+
if (!idOrName) {
|
|
8420
|
+
console.error("用法:crab addresses set-default <id|名称>");
|
|
8421
|
+
return EXIT_USAGE;
|
|
8422
|
+
}
|
|
8423
|
+
const loaded = await requireAddressList();
|
|
8424
|
+
if (typeof loaded === "number")
|
|
8425
|
+
return loaded;
|
|
8426
|
+
const resolved = resolveAddressArg(loaded.list, idOrName);
|
|
8427
|
+
if (!resolved.ok) {
|
|
8428
|
+
console.error(resolved.error);
|
|
8429
|
+
return resolved.failure === "ambiguous" ? EXIT_USAGE : EXIT_NOT_FOUND;
|
|
8430
|
+
}
|
|
8431
|
+
const res = await api2(loaded.credentials.apiBase, `/v1/addresses/${encodeURIComponent(resolved.id)}`, {
|
|
8432
|
+
method: "PATCH",
|
|
8433
|
+
headers: authHeaders(loaded.credentials),
|
|
8434
|
+
body: JSON.stringify({ isDefault: true })
|
|
8435
|
+
});
|
|
8436
|
+
if (res.status !== 200) {
|
|
8437
|
+
console.error(errorMessage(res.body, "设置默认地址失败。"));
|
|
8438
|
+
return exitCodeForApiError(res.body);
|
|
8439
|
+
}
|
|
8440
|
+
const address = res.body.data;
|
|
8441
|
+
if (hasFlag("--json")) {
|
|
8442
|
+
console.log(JSON.stringify(address, null, 2));
|
|
8443
|
+
return 0;
|
|
8444
|
+
}
|
|
8445
|
+
console.log(`✓ 默认地址已切换:${address.name}(${address.region ? `${address.region} · ` : ""}${address.detail})`);
|
|
8446
|
+
return 0;
|
|
8447
|
+
}
|
|
8448
|
+
async function addressesRm(idOrName) {
|
|
8449
|
+
if (!idOrName) {
|
|
8450
|
+
console.error("用法:crab addresses rm <id|名称>");
|
|
8451
|
+
return EXIT_USAGE;
|
|
8452
|
+
}
|
|
8453
|
+
const loaded = await requireAddressList();
|
|
8454
|
+
if (typeof loaded === "number")
|
|
8455
|
+
return loaded;
|
|
8456
|
+
const resolved = resolveAddressArg(loaded.list, idOrName);
|
|
8457
|
+
if (!resolved.ok) {
|
|
8458
|
+
console.error(resolved.error);
|
|
8459
|
+
return resolved.failure === "ambiguous" ? EXIT_USAGE : EXIT_NOT_FOUND;
|
|
8460
|
+
}
|
|
8461
|
+
const res = await api2(loaded.credentials.apiBase, `/v1/addresses/${encodeURIComponent(resolved.id)}`, {
|
|
8462
|
+
method: "DELETE",
|
|
8463
|
+
headers: authHeaders(loaded.credentials)
|
|
8464
|
+
});
|
|
8465
|
+
if (res.status !== 200) {
|
|
8466
|
+
console.error(errorMessage(res.body, "删除失败。"));
|
|
8467
|
+
return exitCodeForApiError(res.body);
|
|
8468
|
+
}
|
|
8469
|
+
console.log(`✓ 已删除(${resolved.id});已下单订单不受影响(结账用的是下单时的地址快照)。`);
|
|
8470
|
+
return 0;
|
|
8471
|
+
}
|
|
8472
|
+
var FAVORITE_KINDS_CLI = ["shop.product"];
|
|
8473
|
+
function favoriteKindFlag() {
|
|
8474
|
+
return readFlag("--kind") ?? FAVORITE_KINDS_CLI[0];
|
|
8475
|
+
}
|
|
8476
|
+
function parseMetaFlag() {
|
|
8477
|
+
const raw = readFlag("--meta");
|
|
8478
|
+
if (raw === undefined)
|
|
8479
|
+
return {};
|
|
8480
|
+
const meta = {};
|
|
8481
|
+
for (const pair of raw.split(",")) {
|
|
8482
|
+
const eq = pair.indexOf("=");
|
|
8483
|
+
if (eq <= 0)
|
|
8484
|
+
return null;
|
|
8485
|
+
meta[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
8486
|
+
}
|
|
8487
|
+
return meta;
|
|
8488
|
+
}
|
|
8489
|
+
function favoriteLine(favorite) {
|
|
8490
|
+
const productName = typeof favorite.meta.productName === "string" ? favorite.meta.productName : "";
|
|
8491
|
+
const storeId = typeof favorite.meta.storeId === "string" ? ` 店 ${favorite.meta.storeId}` : "";
|
|
8492
|
+
const name = productName ? `${productName}(${favorite.refId})` : favorite.refId;
|
|
8493
|
+
return `${favorite.id} ${name}${storeId} ${favorite.kind} ${new Date(favorite.createdAt).toLocaleDateString("zh-CN")}`;
|
|
8494
|
+
}
|
|
8495
|
+
async function favoritesList() {
|
|
8496
|
+
const credentials = requireCredentials();
|
|
8497
|
+
if (typeof credentials === "number")
|
|
8498
|
+
return credentials;
|
|
8499
|
+
const kind = favoriteKindFlag();
|
|
8500
|
+
const params = new URLSearchParams({ kind });
|
|
8501
|
+
const offset = readFlag("--offset");
|
|
8502
|
+
if (offset)
|
|
8503
|
+
params.set("offset", offset);
|
|
8504
|
+
params.set("limit", readFlag("--limit") ?? "30");
|
|
8505
|
+
const res = await api2(credentials.apiBase, `/v1/favorites?${params}`, { headers: authHeaders(credentials) });
|
|
8506
|
+
if (res.status !== 200) {
|
|
8507
|
+
console.error(errorMessage(res.body, "获取收藏失败。"));
|
|
8508
|
+
return exitCodeForApiError(res.body);
|
|
8509
|
+
}
|
|
8510
|
+
const page = res.body.data;
|
|
8511
|
+
if (hasFlag("--json")) {
|
|
8512
|
+
console.log(JSON.stringify(page, null, 2));
|
|
8513
|
+
return 0;
|
|
8514
|
+
}
|
|
8515
|
+
if (page.items.length === 0) {
|
|
8516
|
+
console.log(`还没有 ${kind} 收藏。`);
|
|
8517
|
+
return 0;
|
|
8518
|
+
}
|
|
8519
|
+
for (const favorite of page.items)
|
|
8520
|
+
console.log(favoriteLine(favorite));
|
|
8521
|
+
console.log(`共 ${page.total} 条`);
|
|
8522
|
+
if (page.nextOffset !== null)
|
|
8523
|
+
console.log(`(还有更多:favorites list --offset ${page.nextOffset})`);
|
|
8524
|
+
return 0;
|
|
8525
|
+
}
|
|
8526
|
+
async function favoritesAdd(refId) {
|
|
8527
|
+
if (!refId) {
|
|
8528
|
+
console.error("用法:crab favorites add <refId> [--kind shop.product] [--meta productName=名称,storeId=店铺id] [--json]");
|
|
8529
|
+
return EXIT_USAGE;
|
|
8530
|
+
}
|
|
8531
|
+
const meta = parseMetaFlag();
|
|
8532
|
+
if (meta === null) {
|
|
8533
|
+
console.error("--meta 格式不正确:应为 k=v,k=v(逗号分隔、每个键值对首个 = 分隔)。");
|
|
8534
|
+
return EXIT_USAGE;
|
|
8535
|
+
}
|
|
8536
|
+
const credentials = requireCredentials();
|
|
8537
|
+
if (typeof credentials === "number")
|
|
8538
|
+
return credentials;
|
|
8539
|
+
const kind = favoriteKindFlag();
|
|
8540
|
+
const res = await api2(credentials.apiBase, "/v1/favorites", {
|
|
8541
|
+
method: "PUT",
|
|
8542
|
+
headers: authHeaders(credentials),
|
|
8543
|
+
body: JSON.stringify({ kind, refId, meta })
|
|
8544
|
+
});
|
|
8545
|
+
if (res.status !== 200) {
|
|
8546
|
+
console.error(errorMessage(res.body, "收藏失败。"));
|
|
8547
|
+
return exitCodeForApiError(res.body);
|
|
8548
|
+
}
|
|
8549
|
+
const favorite = res.body.data;
|
|
8550
|
+
if (hasFlag("--json")) {
|
|
8551
|
+
console.log(JSON.stringify(favorite, null, 2));
|
|
8552
|
+
return 0;
|
|
8553
|
+
}
|
|
8554
|
+
console.log(`✓ 已收藏:${favoriteLine(favorite)}`);
|
|
8555
|
+
return 0;
|
|
8556
|
+
}
|
|
8557
|
+
async function favoritesCheck(refId) {
|
|
8558
|
+
if (!refId) {
|
|
8559
|
+
console.error("用法:crab favorites check <refId> [--kind shop.product] [--json]");
|
|
8560
|
+
return EXIT_USAGE;
|
|
8561
|
+
}
|
|
8562
|
+
const credentials = requireCredentials();
|
|
8563
|
+
if (typeof credentials === "number")
|
|
8564
|
+
return credentials;
|
|
8565
|
+
const kind = favoriteKindFlag();
|
|
8566
|
+
const res = await api2(credentials.apiBase, `/v1/favorites/${encodeURIComponent(kind)}/${encodeURIComponent(refId)}`, { headers: authHeaders(credentials) });
|
|
8567
|
+
if (res.status !== 200) {
|
|
8568
|
+
console.error(errorMessage(res.body, "查询收藏状态失败。"));
|
|
8569
|
+
return exitCodeForApiError(res.body);
|
|
8570
|
+
}
|
|
8571
|
+
const { favorited } = res.body.data;
|
|
8572
|
+
if (hasFlag("--json")) {
|
|
8573
|
+
console.log(JSON.stringify({ favorited }, null, 2));
|
|
8574
|
+
return 0;
|
|
8575
|
+
}
|
|
8576
|
+
console.log(favorited ? `✓ 已收藏(${kind} · ${refId})` : `未收藏(${kind} · ${refId})`);
|
|
8577
|
+
return 0;
|
|
8578
|
+
}
|
|
8579
|
+
async function favoritesRm(refId) {
|
|
8580
|
+
if (!refId) {
|
|
8581
|
+
console.error("用法:crab favorites rm <refId> [--kind shop.product]");
|
|
8582
|
+
return EXIT_USAGE;
|
|
8583
|
+
}
|
|
8584
|
+
const credentials = requireCredentials();
|
|
8585
|
+
if (typeof credentials === "number")
|
|
8586
|
+
return credentials;
|
|
8587
|
+
const kind = favoriteKindFlag();
|
|
8588
|
+
const res = await api2(credentials.apiBase, `/v1/favorites/${encodeURIComponent(kind)}/${encodeURIComponent(refId)}`, { method: "DELETE", headers: authHeaders(credentials) });
|
|
8589
|
+
if (res.status !== 200) {
|
|
8590
|
+
console.error(errorMessage(res.body, "取消收藏失败。"));
|
|
8591
|
+
return exitCodeForApiError(res.body);
|
|
8592
|
+
}
|
|
8593
|
+
console.log(`✓ 已取消收藏(${kind} · ${refId})`);
|
|
8594
|
+
return 0;
|
|
8595
|
+
}
|
|
8133
8596
|
var STORAGE_SOURCES = ["files", "media", "mail", "collab", "agent"];
|
|
8134
8597
|
var STORAGE_TYPES = ["doc", "image", "video", "archive", "code"];
|
|
8135
8598
|
var MIME_BY_EXT = {
|
|
@@ -9634,14 +10097,42 @@ var HR_VALUED_FLAGS = [
|
|
|
9634
10097
|
"--due",
|
|
9635
10098
|
"--limit",
|
|
9636
10099
|
"--offset",
|
|
9637
|
-
"--q"
|
|
10100
|
+
"--q",
|
|
10101
|
+
"--body",
|
|
10102
|
+
"--meta"
|
|
9638
10103
|
];
|
|
9639
10104
|
var HR_PERSON_STATUSES = ["active", "probation", "resigned"];
|
|
9640
10105
|
var HR_TASK_STATUSES = ["alert", "confirm", "queued", "done"];
|
|
9641
10106
|
var HR_EVENT_TYPES = ["onboard", "contract", "change", "attendance", "payroll", "award", "asset", "offboard", "note"];
|
|
10107
|
+
var HR_PERMISSION_FLAGS = ["org.manage", "people.write", "tasks.write", "sensitive.read"];
|
|
10108
|
+
var HR_DICT_TYPES = ["dept", "position"];
|
|
9642
10109
|
function hrOrgToken(org) {
|
|
9643
10110
|
return `org:${org.name}`;
|
|
9644
10111
|
}
|
|
10112
|
+
function hrWebBase(apiBase) {
|
|
10113
|
+
if (/127\.0\.0\.1:14321|localhost:14321/.test(apiBase))
|
|
10114
|
+
return "http://127.0.0.1:14342";
|
|
10115
|
+
try {
|
|
10116
|
+
return `https://hr.${new URL(apiBase).host.replace(/^api\./, "")}`;
|
|
10117
|
+
} catch {
|
|
10118
|
+
return apiBase;
|
|
10119
|
+
}
|
|
10120
|
+
}
|
|
10121
|
+
function parseHrPermissions(raw) {
|
|
10122
|
+
if (raw === undefined)
|
|
10123
|
+
return;
|
|
10124
|
+
const out = { "org.manage": false, "people.write": false, "tasks.write": false, "sensitive.read": false };
|
|
10125
|
+
for (const pair of raw.split(",").map((x) => x.trim()).filter(Boolean)) {
|
|
10126
|
+
const [key, value] = pair.split("=");
|
|
10127
|
+
if (!key || !HR_PERMISSION_FLAGS.includes(key.trim())) {
|
|
10128
|
+
console.error(`未知权限项:${pair}
|
|
10129
|
+
可用:${HR_PERMISSION_FLAGS.join(", ")}`);
|
|
10130
|
+
return null;
|
|
10131
|
+
}
|
|
10132
|
+
out[key.trim()] = value !== "0" && value?.toLowerCase() !== "false";
|
|
10133
|
+
}
|
|
10134
|
+
return out;
|
|
10135
|
+
}
|
|
9645
10136
|
async function resolveHrOrg(credentials, arg) {
|
|
9646
10137
|
const res = await api2(credentials.apiBase, "/v1/hr/orgs", { headers: authHeaders(credentials) });
|
|
9647
10138
|
if (res.status !== 200) {
|
|
@@ -9654,7 +10145,7 @@ async function resolveHrOrg(credentials, arg) {
|
|
|
9654
10145
|
}
|
|
9655
10146
|
const orgs = res.body.data.orgs;
|
|
9656
10147
|
if (orgs.length === 0) {
|
|
9657
|
-
console.error("你还没有加入任何公司 / 组织(在 hr-web
|
|
10148
|
+
console.error("你还没有加入任何公司 / 组织(在 hr-web 申请创建,或 crab hr org apply <名称> 提交申请)。");
|
|
9658
10149
|
return EXIT_NOT_FOUND;
|
|
9659
10150
|
}
|
|
9660
10151
|
const raw = (arg ?? "").trim();
|
|
@@ -10193,166 +10684,919 @@ async function shopCategoryDelete() {
|
|
|
10193
10684
|
console.log(`✓ 已删除分类:${target.name}(旗下商品迁入「未分类」)`);
|
|
10194
10685
|
return 0;
|
|
10195
10686
|
}
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10687
|
+
var SHOP_PRODUCT_VALUED_FLAGS = [
|
|
10688
|
+
"--store",
|
|
10689
|
+
"--status",
|
|
10690
|
+
"--category",
|
|
10691
|
+
"--q",
|
|
10692
|
+
"--limit",
|
|
10693
|
+
"--offset",
|
|
10694
|
+
"--product",
|
|
10695
|
+
"--kind",
|
|
10696
|
+
"--variant-mode",
|
|
10697
|
+
"--name",
|
|
10698
|
+
"--price-cents",
|
|
10699
|
+
"--compare-at-cents",
|
|
10700
|
+
"--description",
|
|
10701
|
+
"--code",
|
|
10702
|
+
"--stock",
|
|
10703
|
+
"--refundable",
|
|
10704
|
+
"--max-per-order",
|
|
10705
|
+
"--max-per-buyer",
|
|
10706
|
+
"--delivery",
|
|
10707
|
+
"--delivery-file",
|
|
10708
|
+
"--variant",
|
|
10709
|
+
"--amount",
|
|
10710
|
+
"--reason",
|
|
10711
|
+
"--note",
|
|
10712
|
+
"--set",
|
|
10713
|
+
"--spec-axis"
|
|
10714
|
+
];
|
|
10715
|
+
function readFlagAll(flag) {
|
|
10716
|
+
const out = [];
|
|
10717
|
+
for (let i = 0;i < args.length; i++) {
|
|
10718
|
+
if (args[i] === flag && i + 1 < args.length)
|
|
10719
|
+
out.push(args[i + 1]);
|
|
10201
10720
|
}
|
|
10202
|
-
return
|
|
10721
|
+
return out;
|
|
10203
10722
|
}
|
|
10204
|
-
function
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
const byName = list.filter((g) => g.name === ref);
|
|
10209
|
-
if (byName.length === 1)
|
|
10210
|
-
return byName[0];
|
|
10211
|
-
if (byName.length > 1)
|
|
10212
|
-
return `名称「${ref}」有 ${byName.length} 个同名模板,请用 id 指定(crab shop specs 查看)`;
|
|
10213
|
-
return `规格模板不存在:${ref}(crab shop specs 查看)`;
|
|
10723
|
+
function fmtMoney(cents) {
|
|
10724
|
+
if (cents === null || cents === undefined)
|
|
10725
|
+
return "—";
|
|
10726
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
10214
10727
|
}
|
|
10215
|
-
function
|
|
10216
|
-
|
|
10217
|
-
|
|
10728
|
+
function parseCentsFlag(raw, flagName) {
|
|
10729
|
+
if (raw === undefined || raw.trim() === "") {
|
|
10730
|
+
console.error(`缺少 ${flagName}(金额一律整数分,如 1990 = $19.90)`);
|
|
10218
10731
|
return null;
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10732
|
+
}
|
|
10733
|
+
const n = Number(raw.trim());
|
|
10734
|
+
if (!Number.isSafeInteger(n) || n < 0 || n > 1e8) {
|
|
10735
|
+
console.error(`${flagName} 必须是非负整数分(禁止元/浮点),如 1990 = $19.90`);
|
|
10222
10736
|
return null;
|
|
10223
10737
|
}
|
|
10224
|
-
return
|
|
10738
|
+
return n;
|
|
10225
10739
|
}
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
return
|
|
10230
|
-
const store = await resolveStoreArg(credentials);
|
|
10231
|
-
if (typeof store === "number")
|
|
10232
|
-
return store;
|
|
10233
|
-
const groups = await fetchSpecGroups(credentials, store.id);
|
|
10234
|
-
if (typeof groups === "number")
|
|
10235
|
-
return groups;
|
|
10236
|
-
if (hasFlag("--json")) {
|
|
10237
|
-
console.log(JSON.stringify(groups, null, 2));
|
|
10238
|
-
return 0;
|
|
10740
|
+
function parseIntFlag(raw, flagName, minN) {
|
|
10741
|
+
if (raw === undefined || raw.trim() === "") {
|
|
10742
|
+
console.error(`缺少 ${flagName}`);
|
|
10743
|
+
return null;
|
|
10239
10744
|
}
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10745
|
+
const n = Number(raw.trim());
|
|
10746
|
+
if (!Number.isSafeInteger(n) || n < minN) {
|
|
10747
|
+
console.error(`${flagName} 必须是 ≥${minN} 的整数`);
|
|
10748
|
+
return null;
|
|
10243
10749
|
}
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10750
|
+
return n;
|
|
10751
|
+
}
|
|
10752
|
+
function parseOnOffFlag(raw, flagName) {
|
|
10753
|
+
if (raw !== "on" && raw !== "off") {
|
|
10754
|
+
console.error(`${flagName} 只接受 on | off`);
|
|
10755
|
+
return null;
|
|
10247
10756
|
}
|
|
10248
|
-
return
|
|
10757
|
+
return raw === "on";
|
|
10249
10758
|
}
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
if (
|
|
10258
|
-
|
|
10259
|
-
|
|
10759
|
+
function shopStatusLabel(status) {
|
|
10760
|
+
return status === "active" ? "在售" : status === "draft" ? "草稿" : "已归档";
|
|
10761
|
+
}
|
|
10762
|
+
function shopKindLabel(kind) {
|
|
10763
|
+
return kind === "physical" ? "实物" : "数字";
|
|
10764
|
+
}
|
|
10765
|
+
function fmtListedStock(kind, stock) {
|
|
10766
|
+
if (stock === null)
|
|
10767
|
+
return kind === "physical" ? "待补(挂出不可买)" : "不限";
|
|
10768
|
+
return String(stock);
|
|
10769
|
+
}
|
|
10770
|
+
async function fetchShopProduct(credentials, ref) {
|
|
10771
|
+
const direct = await api2(credentials.apiBase, `/v1/shop/products/${encodeURIComponent(ref)}`, { headers: authHeaders(credentials) });
|
|
10772
|
+
if (direct.status === 200)
|
|
10773
|
+
return direct.body.data.product;
|
|
10774
|
+
if (direct.status !== 404) {
|
|
10775
|
+
console.error(errorMessage(direct.body, "获取商品失败。"));
|
|
10776
|
+
return exitCodeForApiError(direct.body);
|
|
10260
10777
|
}
|
|
10261
10778
|
const store = await resolveStoreArg(credentials);
|
|
10262
10779
|
if (typeof store === "number")
|
|
10263
10780
|
return store;
|
|
10264
|
-
const
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
});
|
|
10269
|
-
if (res.status !== 201) {
|
|
10270
|
-
console.error(errorMessage(res.body, "创建规格模板失败。"));
|
|
10271
|
-
return exitCodeForApiError(res.body);
|
|
10781
|
+
const lookup = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/products/lookup?code=${encodeURIComponent(ref)}`, { headers: authHeaders(credentials) });
|
|
10782
|
+
if (lookup.status !== 200) {
|
|
10783
|
+
console.error(`找不到商品:${ref}(不是商品 id,也不是店内 SKU 码;crab shop products 查看)`);
|
|
10784
|
+
return EXIT_NOT_FOUND;
|
|
10272
10785
|
}
|
|
10273
|
-
const
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10786
|
+
const productId = lookup.body.data.productId;
|
|
10787
|
+
const detail = await api2(credentials.apiBase, `/v1/shop/products/${productId}`, { headers: authHeaders(credentials) });
|
|
10788
|
+
if (detail.status !== 200) {
|
|
10789
|
+
console.error(errorMessage(detail.body, "获取商品失败。"));
|
|
10790
|
+
return exitCodeForApiError(detail.body);
|
|
10277
10791
|
}
|
|
10278
|
-
|
|
10279
|
-
|
|
10792
|
+
return detail.body.data.product;
|
|
10793
|
+
}
|
|
10794
|
+
async function resolveCategoryFlag(credentials, storeId, ref, allowNone) {
|
|
10795
|
+
if (allowNone && ref === "none")
|
|
10796
|
+
return { id: null };
|
|
10797
|
+
const categories = await fetchStoreCategories(credentials, storeId);
|
|
10798
|
+
if (typeof categories === "number")
|
|
10799
|
+
return categories;
|
|
10800
|
+
const hit = resolveCategoryRef(categories, ref);
|
|
10801
|
+
if (typeof hit === "string") {
|
|
10802
|
+
console.error(hit);
|
|
10803
|
+
return EXIT_NOT_FOUND;
|
|
10804
|
+
}
|
|
10805
|
+
return { id: hit.id };
|
|
10806
|
+
}
|
|
10807
|
+
function resolveVariantRef(product, ref) {
|
|
10808
|
+
const normalized = ref.replace(/\s*\/\s*/g, "/");
|
|
10809
|
+
return product.variants.find((x) => x.id === ref) ?? product.variants.find((x) => x.code === ref) ?? product.variants.find((x) => x.combo.join("/") === normalized) ?? null;
|
|
10280
10810
|
}
|
|
10281
|
-
|
|
10811
|
+
function cartesianCombos(axes) {
|
|
10812
|
+
let acc = [[]];
|
|
10813
|
+
for (const axis of axes)
|
|
10814
|
+
acc = acc.flatMap((prefix) => axis.values.map((value) => [...prefix, value]));
|
|
10815
|
+
return acc;
|
|
10816
|
+
}
|
|
10817
|
+
function variantChoiceList(product) {
|
|
10818
|
+
return product.variants.map((v) => ` ${v.code} ${v.combo.join(" / ")}(id ${v.id})`).join(`
|
|
10819
|
+
`);
|
|
10820
|
+
}
|
|
10821
|
+
async function shopProductsList() {
|
|
10282
10822
|
const credentials = requireCredentials();
|
|
10283
10823
|
if (typeof credentials === "number")
|
|
10284
10824
|
return credentials;
|
|
10285
|
-
const
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
const
|
|
10289
|
-
if (
|
|
10290
|
-
console.error("
|
|
10825
|
+
const store = await resolveStoreArg(credentials);
|
|
10826
|
+
if (typeof store === "number")
|
|
10827
|
+
return store;
|
|
10828
|
+
const status = readFlag("--status");
|
|
10829
|
+
if (status && !["draft", "active", "archived"].includes(status)) {
|
|
10830
|
+
console.error("--status 只接受 draft | active | archived");
|
|
10831
|
+
return EXIT_USAGE;
|
|
10832
|
+
}
|
|
10833
|
+
const params = new URLSearchParams;
|
|
10834
|
+
if (status)
|
|
10835
|
+
params.set("status", status);
|
|
10836
|
+
const categoryRef = readFlag("--category");
|
|
10837
|
+
if (categoryRef) {
|
|
10838
|
+
const cat = await resolveCategoryFlag(credentials, store.id, categoryRef, true);
|
|
10839
|
+
if (typeof cat === "number")
|
|
10840
|
+
return cat;
|
|
10841
|
+
params.set("categoryId", cat.id ?? "none");
|
|
10842
|
+
}
|
|
10843
|
+
const q = readFlag("--q");
|
|
10844
|
+
if (q)
|
|
10845
|
+
params.set("q", q);
|
|
10846
|
+
const limit = readFlag("--limit");
|
|
10847
|
+
if (limit)
|
|
10848
|
+
params.set("limit", limit);
|
|
10849
|
+
const offset = readFlag("--offset");
|
|
10850
|
+
if (offset)
|
|
10851
|
+
params.set("offset", offset);
|
|
10852
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/products?${params.toString()}`, { headers: authHeaders(credentials) });
|
|
10853
|
+
if (res.status !== 200) {
|
|
10854
|
+
console.error(errorMessage(res.body, "获取商品列表失败。"));
|
|
10855
|
+
return exitCodeForApiError(res.body);
|
|
10856
|
+
}
|
|
10857
|
+
const data = res.body.data;
|
|
10858
|
+
if (hasFlag("--json")) {
|
|
10859
|
+
console.log(JSON.stringify(data, null, 2));
|
|
10860
|
+
return 0;
|
|
10861
|
+
}
|
|
10862
|
+
if (data.products.length === 0) {
|
|
10863
|
+
console.log(`「${store.name}」还没有符合条件的商品。新建:crab shop product create --kind physical --name 名称 --variant-mode single`);
|
|
10864
|
+
return 0;
|
|
10865
|
+
}
|
|
10866
|
+
for (const p of data.products) {
|
|
10867
|
+
const shape = p.hasVariants ? `多规格·${p.variants.length} 组合` : "单规格";
|
|
10868
|
+
let price;
|
|
10869
|
+
if (p.hasVariants && p.variants.length > 0) {
|
|
10870
|
+
const prices = p.variants.map((v) => v.priceCents);
|
|
10871
|
+
const min = Math.min(...prices);
|
|
10872
|
+
const max = Math.max(...prices);
|
|
10873
|
+
price = min === max ? fmtMoney(min) : `${fmtMoney(min)} ~ ${fmtMoney(max)}`;
|
|
10874
|
+
} else {
|
|
10875
|
+
price = fmtMoney(p.priceCents);
|
|
10876
|
+
}
|
|
10877
|
+
const stock = p.hasVariants ? p.variants.some((v) => v.stock === null) ? "上架 不限" : `上架 ${p.variants.reduce((a, v) => a + (v.stock ?? 0), 0)}` : `上架 ${fmtListedStock(p.kind, p.stock)}`;
|
|
10878
|
+
const warehouse = p.kind === "physical" ? ` · 仓库 ${p.hasVariants ? p.variants.every((v) => v.warehouseStock === null) ? "—" : p.variants.reduce((a, v) => a + (v.warehouseStock ?? 0), 0) : p.warehouseStock ?? 0}` : "";
|
|
10879
|
+
console.log(`${p.id} [${shopStatusLabel(p.status)}] ${p.name} ${shopKindLabel(p.kind)}·${shape} ${price} ${stock}${warehouse}${p.code ? ` ${p.code}` : ""}`);
|
|
10880
|
+
}
|
|
10881
|
+
console.log(`共 ${data.total} 件商品(本页 ${data.products.length} 件;--offset 翻页)`);
|
|
10882
|
+
return 0;
|
|
10883
|
+
}
|
|
10884
|
+
async function shopProductDetail(ref) {
|
|
10885
|
+
const credentials = requireCredentials();
|
|
10886
|
+
if (typeof credentials === "number")
|
|
10887
|
+
return credentials;
|
|
10888
|
+
if (!ref) {
|
|
10889
|
+
console.error("用法:crab shop product <id|SKU码> [--json]");
|
|
10890
|
+
return EXIT_USAGE;
|
|
10891
|
+
}
|
|
10892
|
+
const product = await fetchShopProduct(credentials, ref);
|
|
10893
|
+
if (typeof product === "number")
|
|
10894
|
+
return product;
|
|
10895
|
+
if (hasFlag("--json")) {
|
|
10896
|
+
console.log(JSON.stringify(product, null, 2));
|
|
10897
|
+
return 0;
|
|
10898
|
+
}
|
|
10899
|
+
let categoryName = product.categoryId ?? "未分类";
|
|
10900
|
+
if (product.categoryId) {
|
|
10901
|
+
const categories = await fetchStoreCategories(credentials, product.storeId);
|
|
10902
|
+
if (typeof categories !== "number")
|
|
10903
|
+
categoryName = categories.find((c) => c.id === product.categoryId)?.name ?? product.categoryId;
|
|
10904
|
+
}
|
|
10905
|
+
console.log(`${product.name} [${shopStatusLabel(product.status)}]`);
|
|
10906
|
+
console.log(` ${shopKindLabel(product.kind)} · ${product.hasVariants ? "多规格" : "单规格"} 分类:${categoryName} id:${product.id}`);
|
|
10907
|
+
if (product.code)
|
|
10908
|
+
console.log(` SKU 码:${product.code}`);
|
|
10909
|
+
const priceParts = [`基础价:${fmtMoney(product.priceCents)}`];
|
|
10910
|
+
if (product.compareAtPriceCents !== null)
|
|
10911
|
+
priceParts.push(`划线价:${fmtMoney(product.compareAtPriceCents)}`);
|
|
10912
|
+
priceParts.push(`退款:${product.refundable ? "开" : "关"}`);
|
|
10913
|
+
if (product.maxPerOrder !== null)
|
|
10914
|
+
priceParts.push(`单笔限购 ${product.maxPerOrder}`);
|
|
10915
|
+
if (product.maxPerBuyer !== null)
|
|
10916
|
+
priceParts.push(`每买家限购 ${product.maxPerBuyer}`);
|
|
10917
|
+
console.log(` ${priceParts.join(" ")}`);
|
|
10918
|
+
if (product.kind === "digital") {
|
|
10919
|
+
console.log(` 交付:${product.deliveryType === "license" ? "license 密钥" : "文件下载"}${product.deliveryFileId ? `(对象 ${product.deliveryFileId})` : ""}`);
|
|
10920
|
+
} else if (product.hasVariants) {
|
|
10921
|
+
const listedStr = product.variants.some((v) => v.stock === null) ? "不限" : `合计 ${product.variants.reduce((a, v) => a + (v.stock ?? 0), 0)}`;
|
|
10922
|
+
const whStr = product.variants.every((v) => v.warehouseStock === null) ? "—" : `合计 ${product.variants.reduce((a, v) => a + (v.warehouseStock ?? 0), 0)}`;
|
|
10923
|
+
console.log(` 库存:上架 ${listedStr} · 仓库 ${whStr}`);
|
|
10924
|
+
} else {
|
|
10925
|
+
console.log(` 库存:上架 ${fmtListedStock(product.kind, product.stock)}${product.kind === "physical" ? ` · 仓库 ${product.warehouseStock ?? 0}` : ""}`);
|
|
10926
|
+
}
|
|
10927
|
+
if (product.specAxes.length > 0) {
|
|
10928
|
+
console.log(` 规格轴:${product.specAxes.map((a) => `${a.name}(${a.values.join("、")})`).join(" ")}`);
|
|
10929
|
+
}
|
|
10930
|
+
if (product.hasVariants) {
|
|
10931
|
+
if (product.variants.length === 0) {
|
|
10932
|
+
console.log(' 组合:待配置——crab shop product spec <id> --set "值1,值2=价格分" 配置组合价格后才能上架');
|
|
10933
|
+
} else {
|
|
10934
|
+
console.log(` 组合(${product.variants.length}):`);
|
|
10935
|
+
for (const v of product.variants) {
|
|
10936
|
+
const price = `${fmtMoney(v.priceCents)}${v.compareAtPriceCents !== null ? `(划线 ${fmtMoney(v.compareAtPriceCents)})` : ""}`;
|
|
10937
|
+
const stock = `上架 ${v.stock === null ? "不限" : v.stock}${v.warehouseStock !== null ? ` · 仓库 ${v.warehouseStock}` : ""}`;
|
|
10938
|
+
console.log(` ${v.code} ${v.combo.join(" / ")} ${price} ${stock} ${v.enabled ? "在售" : "停售"}`);
|
|
10939
|
+
}
|
|
10940
|
+
}
|
|
10941
|
+
}
|
|
10942
|
+
if (product.gallery.length > 0)
|
|
10943
|
+
console.log(` 图册:${product.gallery.length} 张(第一张为主图)`);
|
|
10944
|
+
if (product.description)
|
|
10945
|
+
console.log(` 描述:${product.description.length > 100 ? `${product.description.slice(0, 100)}…` : product.description}`);
|
|
10946
|
+
console.log(` 更新:${formatTime(product.updatedAt)}${product.status === "archived" ? "(恢复上架:product update <id> --status draft)" : ""}`);
|
|
10947
|
+
return 0;
|
|
10948
|
+
}
|
|
10949
|
+
async function shopProductCreate() {
|
|
10950
|
+
const credentials = requireCredentials();
|
|
10951
|
+
if (typeof credentials === "number")
|
|
10952
|
+
return credentials;
|
|
10953
|
+
const kind = readFlag("--kind");
|
|
10954
|
+
if (kind !== "physical" && kind !== "digital") {
|
|
10955
|
+
console.error(`用法:crab shop product create --kind physical|digital --name 名称 [--variant-mode single|multi] [--spec-axis "颜色=红,蓝"]… [--price-cents N] [--status draft|active] …
|
|
10956
|
+
必填:--kind(类型,创建后不可变)与 --name;金额一律整数分`);
|
|
10291
10957
|
return EXIT_USAGE;
|
|
10292
10958
|
}
|
|
10293
|
-
|
|
10959
|
+
const name = readFlag("--name")?.trim();
|
|
10960
|
+
if (!name) {
|
|
10961
|
+
console.error("缺少 --name 名称");
|
|
10962
|
+
return EXIT_USAGE;
|
|
10963
|
+
}
|
|
10964
|
+
const variantMode = readFlag("--variant-mode") ?? "single";
|
|
10965
|
+
if (variantMode !== "single" && variantMode !== "multi") {
|
|
10966
|
+
console.error("--variant-mode 只接受 single | multi(创建即锁定,之后不可改)");
|
|
10294
10967
|
return EXIT_USAGE;
|
|
10968
|
+
}
|
|
10969
|
+
const axisArgs = readFlagAll("--spec-axis");
|
|
10970
|
+
if (variantMode === "single" && axisArgs.length > 0) {
|
|
10971
|
+
console.error("单规格商品没有规格轴(规格模式创建即锁定);要规格就 --variant-mode multi");
|
|
10972
|
+
return EXIT_USAGE;
|
|
10973
|
+
}
|
|
10974
|
+
const specAxes = [];
|
|
10975
|
+
for (const raw of axisArgs) {
|
|
10976
|
+
const eq = raw.indexOf("=");
|
|
10977
|
+
if (eq <= 0) {
|
|
10978
|
+
console.error(`--spec-axis 格式:轴名=值1,值2(如 "颜色=红,蓝"),收到「${raw}」`);
|
|
10979
|
+
return EXIT_USAGE;
|
|
10980
|
+
}
|
|
10981
|
+
const axisName = raw.slice(0, eq).trim();
|
|
10982
|
+
const values = raw.slice(eq + 1).split(/[,,、]+/).map((x) => x.trim()).filter(Boolean);
|
|
10983
|
+
if (!axisName || values.length === 0) {
|
|
10984
|
+
console.error(`--spec-axis 轴名与选项值都不能为空:「${raw}」`);
|
|
10985
|
+
return EXIT_USAGE;
|
|
10986
|
+
}
|
|
10987
|
+
specAxes.push({ name: axisName, values });
|
|
10988
|
+
}
|
|
10989
|
+
if (specAxes.length > 3) {
|
|
10990
|
+
console.error("规格轴最多 3 条(--spec-axis 可重复出现)");
|
|
10991
|
+
return EXIT_USAGE;
|
|
10992
|
+
}
|
|
10993
|
+
if (variantMode === "multi") {
|
|
10994
|
+
const comboCount = specAxes.reduce((acc, a) => acc * a.values.length, 1);
|
|
10995
|
+
if (comboCount > 24) {
|
|
10996
|
+
console.error(`规格组合 ${comboCount} 个超出上限 24——减少轴或选项值`);
|
|
10997
|
+
return EXIT_USAGE;
|
|
10998
|
+
}
|
|
10999
|
+
}
|
|
11000
|
+
const body = { kind, variantMode, name };
|
|
11001
|
+
if (variantMode === "multi" && specAxes.length > 0)
|
|
11002
|
+
body.specAxes = specAxes;
|
|
11003
|
+
const status = readFlag("--status");
|
|
11004
|
+
if (status) {
|
|
11005
|
+
if (status !== "draft" && status !== "active") {
|
|
11006
|
+
console.error("--status 创建时只接受 draft | active");
|
|
11007
|
+
return EXIT_USAGE;
|
|
11008
|
+
}
|
|
11009
|
+
body.status = status;
|
|
11010
|
+
}
|
|
11011
|
+
const categoryRef = readFlag("--category");
|
|
11012
|
+
if (categoryRef) {
|
|
11013
|
+
const store = await resolveStoreArg(credentials);
|
|
11014
|
+
if (typeof store === "number")
|
|
11015
|
+
return store;
|
|
11016
|
+
const cat = await resolveCategoryFlag(credentials, store.id, categoryRef, false);
|
|
11017
|
+
if (typeof cat === "number")
|
|
11018
|
+
return cat;
|
|
11019
|
+
body.categoryId = cat.id;
|
|
11020
|
+
}
|
|
11021
|
+
const description = readFlag("--description");
|
|
11022
|
+
if (description !== undefined)
|
|
11023
|
+
body.description = description;
|
|
11024
|
+
const priceCents = readFlag("--price-cents");
|
|
11025
|
+
if (priceCents !== undefined) {
|
|
11026
|
+
const parsed = parseCentsFlag(priceCents, "--price-cents");
|
|
11027
|
+
if (parsed === null)
|
|
11028
|
+
return EXIT_USAGE;
|
|
11029
|
+
body.priceCents = parsed;
|
|
11030
|
+
}
|
|
11031
|
+
const compareAt = readFlag("--compare-at-cents");
|
|
11032
|
+
if (compareAt !== undefined) {
|
|
11033
|
+
const parsed = parseCentsFlag(compareAt, "--compare-at-cents");
|
|
11034
|
+
if (parsed === null)
|
|
11035
|
+
return EXIT_USAGE;
|
|
11036
|
+
body.compareAtPriceCents = parsed;
|
|
11037
|
+
}
|
|
11038
|
+
const code = readFlag("--code");
|
|
11039
|
+
if (code !== undefined) {
|
|
11040
|
+
if (variantMode === "multi") {
|
|
11041
|
+
console.error("多规格商品的码在每个组合上(组合配置时自动/指定分配),商品级 --code 不适用");
|
|
11042
|
+
return EXIT_USAGE;
|
|
11043
|
+
}
|
|
11044
|
+
body.code = code.trim();
|
|
11045
|
+
}
|
|
11046
|
+
const stockRaw = readFlag("--stock");
|
|
11047
|
+
if (stockRaw !== undefined) {
|
|
11048
|
+
if (variantMode === "multi") {
|
|
11049
|
+
console.error("多规格商品的库存挂在组合上:创建后用 crab shop stock(仓库账)/ crab shop listed(上架额度)按组合调整");
|
|
11050
|
+
return EXIT_USAGE;
|
|
11051
|
+
}
|
|
11052
|
+
const parsed = parseIntFlag(stockRaw, "--stock", 0);
|
|
11053
|
+
if (parsed === null)
|
|
11054
|
+
return EXIT_USAGE;
|
|
11055
|
+
body.stock = parsed;
|
|
11056
|
+
}
|
|
11057
|
+
const refundable = readFlag("--refundable");
|
|
11058
|
+
if (refundable !== undefined) {
|
|
11059
|
+
const parsed = parseOnOffFlag(refundable, "--refundable");
|
|
11060
|
+
if (parsed === null)
|
|
11061
|
+
return EXIT_USAGE;
|
|
11062
|
+
body.refundable = parsed;
|
|
11063
|
+
}
|
|
11064
|
+
for (const [flag, key] of [["--max-per-order", "maxPerOrder"], ["--max-per-buyer", "maxPerBuyer"]]) {
|
|
11065
|
+
const raw = readFlag(flag);
|
|
11066
|
+
if (raw !== undefined) {
|
|
11067
|
+
const parsed = parseIntFlag(raw, flag, 1);
|
|
11068
|
+
if (parsed === null)
|
|
11069
|
+
return EXIT_USAGE;
|
|
11070
|
+
body[key] = parsed;
|
|
11071
|
+
}
|
|
11072
|
+
}
|
|
11073
|
+
const deliveryType = readFlag("--delivery");
|
|
11074
|
+
const deliveryFile = readFlag("--delivery-file");
|
|
11075
|
+
if (deliveryType !== undefined || deliveryFile !== undefined) {
|
|
11076
|
+
if (kind !== "digital") {
|
|
11077
|
+
console.error("--delivery / --delivery-file 只适用于数字商品(实物无交付形态)");
|
|
11078
|
+
return EXIT_USAGE;
|
|
11079
|
+
}
|
|
11080
|
+
if (deliveryType !== undefined) {
|
|
11081
|
+
if (deliveryType !== "file" && deliveryType !== "license") {
|
|
11082
|
+
console.error("--delivery 只接受 file | license");
|
|
11083
|
+
return EXIT_USAGE;
|
|
11084
|
+
}
|
|
11085
|
+
body.deliveryType = deliveryType;
|
|
11086
|
+
}
|
|
11087
|
+
if (deliveryFile !== undefined)
|
|
11088
|
+
body.deliveryFileId = deliveryFile;
|
|
11089
|
+
}
|
|
10295
11090
|
const store = await resolveStoreArg(credentials);
|
|
10296
11091
|
if (typeof store === "number")
|
|
10297
11092
|
return store;
|
|
10298
|
-
const
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
11093
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/products`, {
|
|
11094
|
+
method: "POST",
|
|
11095
|
+
headers: authHeaders(credentials),
|
|
11096
|
+
body: JSON.stringify(body)
|
|
11097
|
+
});
|
|
11098
|
+
if (res.status !== 201) {
|
|
11099
|
+
console.error(errorMessage(res.body, "创建商品失败。"));
|
|
11100
|
+
if (res.body?.error?.code === "specs_required") {
|
|
11101
|
+
console.error('提示:多规格商品上架前必须配置完整组合——crab shop product spec <id> --set "值1,值2=价格分"');
|
|
11102
|
+
}
|
|
11103
|
+
return exitCodeForApiError(res.body);
|
|
11104
|
+
}
|
|
11105
|
+
const product = res.body.data.product;
|
|
11106
|
+
if (hasFlag("--json")) {
|
|
11107
|
+
console.log(JSON.stringify(product, null, 2));
|
|
11108
|
+
return 0;
|
|
11109
|
+
}
|
|
11110
|
+
console.log(`✓ 已创建商品(${shopStatusLabel(product.status)}):${product.name} ${product.id}`);
|
|
11111
|
+
if (product.hasVariants) {
|
|
11112
|
+
console.log(` 规格轴:${product.specAxes.map((a) => `${a.name}(${a.values.join("、")})`).join(" ")}`);
|
|
11113
|
+
console.log(` 下一步:crab shop product spec ${product.id} --set "值1,值2=价格分" 配置组合价格(配置完整后才能上架)`);
|
|
11114
|
+
} else {
|
|
11115
|
+
console.log(` SKU 码:${product.code}`);
|
|
11116
|
+
console.log(` 下一步:crab shop product update ${product.id} --price-cents 1990 --status active 上架(金额整数分)`);
|
|
11117
|
+
}
|
|
11118
|
+
return 0;
|
|
11119
|
+
}
|
|
11120
|
+
async function shopProductUpdate() {
|
|
11121
|
+
const credentials = requireCredentials();
|
|
11122
|
+
if (typeof credentials === "number")
|
|
11123
|
+
return credentials;
|
|
11124
|
+
const target = shopPositionals(3, SHOP_PRODUCT_VALUED_FLAGS)[0];
|
|
11125
|
+
if (!target) {
|
|
11126
|
+
console.error(`用法:crab shop product update <id|SKU码> [--name …] [--description …] [--category <id|名称|none>] [--price-cents N] [--compare-at-cents N|clear] [--status draft|active|archived] [--refundable on|off] [--max-per-order N|clear] [--max-per-buyer N|clear] [--code <SKU码>] [--json]
|
|
11127
|
+
只提交给出的字段;改多规格组合用 product spec,改库存用 stock / listed`);
|
|
11128
|
+
return EXIT_USAGE;
|
|
11129
|
+
}
|
|
11130
|
+
const product = await fetchShopProduct(credentials, target);
|
|
11131
|
+
if (typeof product === "number")
|
|
11132
|
+
return product;
|
|
11133
|
+
const body = {};
|
|
11134
|
+
const name = readFlag("--name");
|
|
11135
|
+
if (name !== undefined) {
|
|
11136
|
+
if (!name.trim()) {
|
|
11137
|
+
console.error("--name 不能为空");
|
|
11138
|
+
return EXIT_USAGE;
|
|
11139
|
+
}
|
|
11140
|
+
body.name = name.trim();
|
|
11141
|
+
}
|
|
11142
|
+
const description = readFlag("--description");
|
|
11143
|
+
if (description !== undefined)
|
|
11144
|
+
body.description = description;
|
|
11145
|
+
const categoryRef = readFlag("--category");
|
|
11146
|
+
if (categoryRef !== undefined) {
|
|
11147
|
+
const cat = await resolveCategoryFlag(credentials, product.storeId, categoryRef, true);
|
|
11148
|
+
if (typeof cat === "number")
|
|
11149
|
+
return cat;
|
|
11150
|
+
body.categoryId = cat.id;
|
|
11151
|
+
}
|
|
11152
|
+
const status = readFlag("--status");
|
|
11153
|
+
if (status !== undefined) {
|
|
11154
|
+
if (!["draft", "active", "archived"].includes(status)) {
|
|
11155
|
+
console.error("--status 只接受 draft | active | archived");
|
|
11156
|
+
return EXIT_USAGE;
|
|
11157
|
+
}
|
|
11158
|
+
body.status = status;
|
|
11159
|
+
}
|
|
11160
|
+
const priceRaw = readFlag("--price-cents");
|
|
11161
|
+
if (priceRaw !== undefined) {
|
|
11162
|
+
const parsed = parseCentsFlag(priceRaw, "--price-cents");
|
|
11163
|
+
if (parsed === null)
|
|
11164
|
+
return EXIT_USAGE;
|
|
11165
|
+
body.priceCents = parsed;
|
|
11166
|
+
}
|
|
11167
|
+
const compareAtRaw = readFlag("--compare-at-cents");
|
|
11168
|
+
if (compareAtRaw !== undefined) {
|
|
11169
|
+
if (compareAtRaw === "clear")
|
|
11170
|
+
body.compareAtPriceCents = null;
|
|
11171
|
+
else {
|
|
11172
|
+
const parsed = parseCentsFlag(compareAtRaw, "--compare-at-cents");
|
|
11173
|
+
if (parsed === null)
|
|
11174
|
+
return EXIT_USAGE;
|
|
11175
|
+
body.compareAtPriceCents = parsed;
|
|
11176
|
+
}
|
|
11177
|
+
}
|
|
11178
|
+
const refundable = readFlag("--refundable");
|
|
11179
|
+
if (refundable !== undefined) {
|
|
11180
|
+
const parsed = parseOnOffFlag(refundable, "--refundable");
|
|
11181
|
+
if (parsed === null)
|
|
11182
|
+
return EXIT_USAGE;
|
|
11183
|
+
body.refundable = parsed;
|
|
11184
|
+
}
|
|
11185
|
+
for (const [flag, key] of [["--max-per-order", "maxPerOrder"], ["--max-per-buyer", "maxPerBuyer"]]) {
|
|
11186
|
+
const raw = readFlag(flag);
|
|
11187
|
+
if (raw !== undefined) {
|
|
11188
|
+
if (raw === "clear")
|
|
11189
|
+
body[key] = null;
|
|
11190
|
+
else {
|
|
11191
|
+
const parsed = parseIntFlag(raw, flag, 1);
|
|
11192
|
+
if (parsed === null)
|
|
11193
|
+
return EXIT_USAGE;
|
|
11194
|
+
body[key] = parsed;
|
|
11195
|
+
}
|
|
11196
|
+
}
|
|
11197
|
+
}
|
|
11198
|
+
const code = readFlag("--code");
|
|
11199
|
+
if (code !== undefined) {
|
|
11200
|
+
if (product.hasVariants) {
|
|
11201
|
+
console.error("多规格商品的码在每个组合上(商品级 --code 不适用,服务端会忽略)");
|
|
11202
|
+
return EXIT_USAGE;
|
|
11203
|
+
}
|
|
11204
|
+
body.code = code.trim();
|
|
10305
11205
|
}
|
|
10306
|
-
|
|
11206
|
+
if (Object.keys(body).length === 0) {
|
|
11207
|
+
console.error("没有任何要更新的字段——至少给一个 --name / --price-cents / --status …");
|
|
11208
|
+
return EXIT_USAGE;
|
|
11209
|
+
}
|
|
11210
|
+
const res = await api2(credentials.apiBase, `/v1/shop/products/${product.id}`, {
|
|
10307
11211
|
method: "PATCH",
|
|
10308
11212
|
headers: authHeaders(credentials),
|
|
10309
|
-
body: JSON.stringify(
|
|
11213
|
+
body: JSON.stringify(body)
|
|
10310
11214
|
});
|
|
10311
11215
|
if (res.status !== 200) {
|
|
10312
|
-
console.error(errorMessage(res.body, "
|
|
11216
|
+
console.error(errorMessage(res.body, "更新商品失败。"));
|
|
11217
|
+
return exitCodeForApiError(res.body);
|
|
11218
|
+
}
|
|
11219
|
+
const updated = res.body.data.product;
|
|
11220
|
+
if (hasFlag("--json")) {
|
|
11221
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
11222
|
+
return 0;
|
|
11223
|
+
}
|
|
11224
|
+
console.log(`✓ 已更新:${updated.name} [${shopStatusLabel(updated.status)}] ${updated.id}`);
|
|
11225
|
+
console.log(` 基础价:${fmtMoney(updated.priceCents)}${updated.compareAtPriceCents !== null ? `(划线 ${fmtMoney(updated.compareAtPriceCents)})` : ""}`);
|
|
11226
|
+
return 0;
|
|
11227
|
+
}
|
|
11228
|
+
function parseSpecSet(raw, axes) {
|
|
11229
|
+
const eq = raw.indexOf("=");
|
|
11230
|
+
if (eq <= 0)
|
|
11231
|
+
return `--set 格式:"<值1,值2>=价格分[,划线价分|clear][,on|off]"(如 "红,M=1990,2500,on"),收到「${raw}」`;
|
|
11232
|
+
const combo = raw.slice(0, eq).split(/[,,、]+/).map((x) => x.trim());
|
|
11233
|
+
if (combo.length !== axes.length) {
|
|
11234
|
+
return `组合需要 ${axes.length} 个值(轴顺序:${axes.map((a) => a.name).join(" / ")}),收到「${raw.slice(0, eq)}」`;
|
|
11235
|
+
}
|
|
11236
|
+
for (let i = 0;i < combo.length; i++) {
|
|
11237
|
+
if (!axes[i].values.includes(combo[i])) {
|
|
11238
|
+
return `值「${combo[i]}」不在规格轴「${axes[i].name}」的选项里(可选:${axes[i].values.join("、")})`;
|
|
11239
|
+
}
|
|
11240
|
+
}
|
|
11241
|
+
const right = raw.slice(eq + 1).split(/[,,、]+/).map((x) => x.trim());
|
|
11242
|
+
const price = Number(right[0]);
|
|
11243
|
+
if (!Number.isSafeInteger(price) || price < 0) {
|
|
11244
|
+
return `--set 价格分必须是非负整数(如 1990 = $19.90),收到「${right[0]}」`;
|
|
11245
|
+
}
|
|
11246
|
+
const entry = { combo, price };
|
|
11247
|
+
for (const extra of right.slice(1)) {
|
|
11248
|
+
if (extra === "on" || extra === "off")
|
|
11249
|
+
entry.enabled = extra === "on";
|
|
11250
|
+
else if (extra === "clear")
|
|
11251
|
+
entry.compareAt = null;
|
|
11252
|
+
else {
|
|
11253
|
+
const n = Number(extra);
|
|
11254
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
11255
|
+
return `--set 第 2 段起应为划线价分、clear 或 on|off,收到「${extra}」`;
|
|
11256
|
+
entry.compareAt = n;
|
|
11257
|
+
}
|
|
11258
|
+
}
|
|
11259
|
+
return entry;
|
|
11260
|
+
}
|
|
11261
|
+
async function shopProductSpec() {
|
|
11262
|
+
const credentials = requireCredentials();
|
|
11263
|
+
if (typeof credentials === "number")
|
|
11264
|
+
return credentials;
|
|
11265
|
+
const target = shopPositionals(3, SHOP_PRODUCT_VALUED_FLAGS)[0];
|
|
11266
|
+
const sets = readFlagAll("--set");
|
|
11267
|
+
if (!target || sets.length === 0) {
|
|
11268
|
+
console.error(`用法:crab shop product spec <id|SKU码> --set "<值1,值2>=价格分[,划线价分|clear][,on|off]" … [--json]
|
|
11269
|
+
值按规格轴顺序给(值1=第一轴);未提及的组合保持原值;本命令不改库存(动账 stock / 上架额度 listed);规格轴在创建时锁定,轴数量首次保存即锁`);
|
|
11270
|
+
return EXIT_USAGE;
|
|
11271
|
+
}
|
|
11272
|
+
const product = await fetchShopProduct(credentials, target);
|
|
11273
|
+
if (typeof product === "number")
|
|
11274
|
+
return product;
|
|
11275
|
+
if (!product.hasVariants) {
|
|
11276
|
+
console.error("单规格商品没有规格组合——改价格用 crab shop product update <id> --price-cents N");
|
|
11277
|
+
return EXIT_USAGE;
|
|
11278
|
+
}
|
|
11279
|
+
if (product.specAxes.length === 0) {
|
|
11280
|
+
console.error("该商品还没有规格轴(多规格商品创建时用 --spec-axis 配置;存量无轴商品暂不支持 CLI 加轴,到商家控制台操作)");
|
|
11281
|
+
return EXIT_USAGE;
|
|
11282
|
+
}
|
|
11283
|
+
const overrides = new Map;
|
|
11284
|
+
for (const raw of sets) {
|
|
11285
|
+
const parsed = parseSpecSet(raw, product.specAxes);
|
|
11286
|
+
if (typeof parsed === "string") {
|
|
11287
|
+
console.error(parsed);
|
|
11288
|
+
return EXIT_USAGE;
|
|
11289
|
+
}
|
|
11290
|
+
overrides.set(JSON.stringify(parsed.combo), parsed);
|
|
11291
|
+
}
|
|
11292
|
+
const prevByKey = new Map(product.variants.map((v) => [v.comboKey, v]));
|
|
11293
|
+
const combos = cartesianCombos(product.specAxes);
|
|
11294
|
+
if (combos.length > 24) {
|
|
11295
|
+
console.error(`规格组合 ${combos.length} 个超出上限 24`);
|
|
11296
|
+
return EXIT_USAGE;
|
|
11297
|
+
}
|
|
11298
|
+
const unresolved = combos.filter((combo) => {
|
|
11299
|
+
const key = JSON.stringify(combo);
|
|
11300
|
+
return !overrides.has(key) && !prevByKey.has(key) && product.priceCents === null;
|
|
11301
|
+
});
|
|
11302
|
+
if (unresolved.length > 0) {
|
|
11303
|
+
console.error(`以下新组合未给价格且商品无基础价可沿用:${unresolved.map((c) => c.join("/")).join("、")}
|
|
11304
|
+
用 --set 补价格,或先 crab shop product update <id> --price-cents N 设基础价`);
|
|
11305
|
+
return EXIT_USAGE;
|
|
11306
|
+
}
|
|
11307
|
+
const variants = combos.map((combo) => {
|
|
11308
|
+
const key = JSON.stringify(combo);
|
|
11309
|
+
const prev = prevByKey.get(key);
|
|
11310
|
+
const o = overrides.get(key);
|
|
11311
|
+
const row = { combo, enabled: o?.enabled ?? prev?.enabled ?? true };
|
|
11312
|
+
const price = o ? o.price : prev?.priceCents;
|
|
11313
|
+
if (price !== undefined)
|
|
11314
|
+
row.priceCents = price;
|
|
11315
|
+
row.compareAtPriceCents = o ? o.compareAt !== undefined ? o.compareAt : prev?.compareAtPriceCents ?? null : prev?.compareAtPriceCents ?? null;
|
|
11316
|
+
if (prev) {
|
|
11317
|
+
row.stock = prev.stock;
|
|
11318
|
+
row.code = prev.code;
|
|
11319
|
+
}
|
|
11320
|
+
return row;
|
|
11321
|
+
});
|
|
11322
|
+
const res = await api2(credentials.apiBase, `/v1/shop/products/${product.id}`, {
|
|
11323
|
+
method: "PATCH",
|
|
11324
|
+
headers: authHeaders(credentials),
|
|
11325
|
+
body: JSON.stringify({ specAxes: product.specAxes, variants })
|
|
11326
|
+
});
|
|
11327
|
+
if (res.status !== 200) {
|
|
11328
|
+
console.error(errorMessage(res.body, "配置组合失败。"));
|
|
11329
|
+
const code = res.body?.error?.code;
|
|
11330
|
+
if (code === "spec_combo_in_ledger")
|
|
11331
|
+
console.error('提示:被出入库台账引用的组合不能删除——用 --set "…,off" 关闭在售代替');
|
|
11332
|
+
if (code === "exceeds_warehouse")
|
|
11333
|
+
console.error("提示:组合销售库存不能超过实际库存(存量组合原值回写不该触发;检查商品是否有超账警示态)");
|
|
11334
|
+
return exitCodeForApiError(res.body);
|
|
11335
|
+
}
|
|
11336
|
+
const updated = res.body.data.product;
|
|
11337
|
+
if (hasFlag("--json")) {
|
|
11338
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
11339
|
+
return 0;
|
|
11340
|
+
}
|
|
11341
|
+
console.log(`✓ 已配置 ${updated.variants.length} 个组合(${updated.variants.filter((v) => v.enabled).length} 个在售):`);
|
|
11342
|
+
for (const v of updated.variants) {
|
|
11343
|
+
const price = `${fmtMoney(v.priceCents)}${v.compareAtPriceCents !== null ? `(划线 ${fmtMoney(v.compareAtPriceCents)})` : ""}`;
|
|
11344
|
+
const stock = `上架 ${v.stock === null ? "不限" : v.stock}${v.warehouseStock !== null ? ` · 仓库 ${v.warehouseStock}` : ""}`;
|
|
11345
|
+
console.log(` ${v.code} ${v.combo.join(" / ")} ${price} ${stock} ${v.enabled ? "在售" : "停售"}`);
|
|
11346
|
+
}
|
|
11347
|
+
console.log("改库存:crab shop stock in|out|set <id>(仓库账)· crab shop listed <id>(上架额度,实物)");
|
|
11348
|
+
return 0;
|
|
11349
|
+
}
|
|
11350
|
+
async function shopProductArchive() {
|
|
11351
|
+
const credentials = requireCredentials();
|
|
11352
|
+
if (typeof credentials === "number")
|
|
11353
|
+
return credentials;
|
|
11354
|
+
const target = shopPositionals(3, SHOP_PRODUCT_VALUED_FLAGS)[0];
|
|
11355
|
+
if (!target) {
|
|
11356
|
+
console.error(`用法:crab shop product archive <id|SKU码> [--json]
|
|
11357
|
+
下架归档(软删):买家不可见,历史订单快照不受影响;恢复用 product update <id> --status draft`);
|
|
11358
|
+
return EXIT_USAGE;
|
|
11359
|
+
}
|
|
11360
|
+
const product = await fetchShopProduct(credentials, target);
|
|
11361
|
+
if (typeof product === "number")
|
|
11362
|
+
return product;
|
|
11363
|
+
const res = await api2(credentials.apiBase, `/v1/shop/products/${product.id}`, { method: "DELETE", headers: authHeaders(credentials) });
|
|
11364
|
+
if (res.status !== 200) {
|
|
11365
|
+
console.error(errorMessage(res.body, "归档商品失败。"));
|
|
10313
11366
|
return exitCodeForApiError(res.body);
|
|
10314
11367
|
}
|
|
10315
|
-
const group = res.body.data.group;
|
|
10316
11368
|
if (hasFlag("--json"))
|
|
10317
|
-
console.log(JSON.stringify(
|
|
11369
|
+
console.log(JSON.stringify({ ok: true, id: product.id }, null, 2));
|
|
10318
11370
|
else
|
|
10319
|
-
console.log(`✓
|
|
11371
|
+
console.log(`✓ 已归档:${product.name} ${product.id}(恢复:crab shop product update ${product.id} --status draft)`);
|
|
10320
11372
|
return 0;
|
|
10321
11373
|
}
|
|
10322
|
-
async function
|
|
11374
|
+
async function shopStockAdjust(mode) {
|
|
10323
11375
|
const credentials = requireCredentials();
|
|
10324
11376
|
if (typeof credentials === "number")
|
|
10325
11377
|
return credentials;
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
11378
|
+
if (mode !== "in" && mode !== "out" && mode !== "set") {
|
|
11379
|
+
console.error(`用法:crab shop stock in|out|set <id|SKU码> --amount N [--variant <组合码|变体id|"值1/值2">] [--reason purchase] --note "说明" [--json]
|
|
11380
|
+
in 入库 / out 出库(变动量 ≥1)/ set 盘点设定(仓库账 = N,可为 0)
|
|
11381
|
+
--reason purchase 仅入库有效;set 或未给 --reason(按 manual 落账)必须带 --note
|
|
11382
|
+
退货入库与发货出库由售后/发货端点自动落账,勿用本命令手工登记`);
|
|
11383
|
+
return EXIT_USAGE;
|
|
11384
|
+
}
|
|
11385
|
+
const target = shopPositionals(3, SHOP_PRODUCT_VALUED_FLAGS)[0];
|
|
11386
|
+
if (!target) {
|
|
11387
|
+
console.error("缺少商品 <id|SKU码>");
|
|
11388
|
+
return EXIT_USAGE;
|
|
11389
|
+
}
|
|
11390
|
+
const amountRaw = readFlag("--amount");
|
|
11391
|
+
if (amountRaw === undefined) {
|
|
11392
|
+
console.error("缺少 --amount N(in/out 为变动量 ≥1;set 为盘点后的实际库存数 ≥0)");
|
|
11393
|
+
return EXIT_USAGE;
|
|
11394
|
+
}
|
|
11395
|
+
const amount = Number(amountRaw);
|
|
11396
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || mode !== "set" && amount === 0) {
|
|
11397
|
+
console.error(mode === "set" ? "--amount 必须是 ≥0 的整数(盘点后的实际库存数)" : "--amount 必须是 ≥1 的整数(in/out 为变动量)");
|
|
11398
|
+
return EXIT_USAGE;
|
|
11399
|
+
}
|
|
11400
|
+
const reason = readFlag("--reason");
|
|
11401
|
+
if (reason !== undefined && reason !== "purchase" && reason !== "manual") {
|
|
11402
|
+
console.error("--reason 只接受 purchase(采购入库)| manual");
|
|
11403
|
+
return EXIT_USAGE;
|
|
11404
|
+
}
|
|
11405
|
+
if (reason === "purchase" && mode !== "in") {
|
|
11406
|
+
console.error("--reason purchase 仅入库(in)有效;出库/盘点一律落 manual");
|
|
11407
|
+
return EXIT_USAGE;
|
|
11408
|
+
}
|
|
11409
|
+
const note = readFlag("--note");
|
|
11410
|
+
if ((mode === "set" || reason === undefined) && !note?.trim()) {
|
|
11411
|
+
console.error('盘点(set)或未指定 --reason(按 manual 落账)必须带 --note "原因说明"(台账留痕)');
|
|
11412
|
+
return EXIT_USAGE;
|
|
11413
|
+
}
|
|
11414
|
+
const product = await fetchShopProduct(credentials, target);
|
|
11415
|
+
if (typeof product === "number")
|
|
11416
|
+
return product;
|
|
11417
|
+
const targets = [];
|
|
11418
|
+
const variantRef = readFlag("--variant");
|
|
11419
|
+
if (product.hasVariants) {
|
|
11420
|
+
if (!variantRef) {
|
|
11421
|
+
console.error(`多规格商品必须用 --variant 指定组合(组合码 / 变体 id / "值1/值2"):
|
|
11422
|
+
${variantChoiceList(product)}`);
|
|
11423
|
+
return EXIT_USAGE;
|
|
11424
|
+
}
|
|
11425
|
+
const variant = resolveVariantRef(product, variantRef);
|
|
11426
|
+
if (!variant) {
|
|
11427
|
+
console.error(`找不到组合:${variantRef}(可用组合:
|
|
11428
|
+
${variantChoiceList(product)})`);
|
|
11429
|
+
return EXIT_USAGE;
|
|
11430
|
+
}
|
|
11431
|
+
targets.push({ variantId: variant.id, amount });
|
|
11432
|
+
} else {
|
|
11433
|
+
if (variantRef !== undefined) {
|
|
11434
|
+
console.error("单规格商品没有组合,去掉 --variant");
|
|
11435
|
+
return EXIT_USAGE;
|
|
11436
|
+
}
|
|
11437
|
+
targets.push({ amount });
|
|
11438
|
+
}
|
|
11439
|
+
const body = { mode, targets };
|
|
11440
|
+
if (reason)
|
|
11441
|
+
body.reason = reason;
|
|
11442
|
+
if (note?.trim())
|
|
11443
|
+
body.note = note.trim();
|
|
11444
|
+
const res = await api2(credentials.apiBase, `/v1/shop/products/${product.id}/stock`, {
|
|
11445
|
+
method: "POST",
|
|
11446
|
+
headers: authHeaders(credentials),
|
|
11447
|
+
body: JSON.stringify(body)
|
|
11448
|
+
});
|
|
11449
|
+
if (res.status !== 200) {
|
|
11450
|
+
console.error(errorMessage(res.body, "库存动账失败。"));
|
|
11451
|
+
const code = res.body?.error?.code;
|
|
11452
|
+
if (code === "insufficient_stock")
|
|
11453
|
+
console.error("提示:库存不足——先入库:crab shop stock in …");
|
|
11454
|
+
if (code === "unlimited_stock")
|
|
11455
|
+
console.error("提示:不限量库存不能出库——先用 stock set 转限量(盘点语义)再操作");
|
|
11456
|
+
return exitCodeForApiError(res.body);
|
|
11457
|
+
}
|
|
11458
|
+
const items = res.body.data.items;
|
|
11459
|
+
if (hasFlag("--json")) {
|
|
11460
|
+
console.log(JSON.stringify(res.body.data, null, 2));
|
|
11461
|
+
return 0;
|
|
11462
|
+
}
|
|
11463
|
+
const after = items[0];
|
|
11464
|
+
const label = mode === "in" ? "入库" : mode === "out" ? "出库" : "盘点";
|
|
11465
|
+
console.log(`✓ ${label} ${amount}:${product.name}`);
|
|
11466
|
+
if (after) {
|
|
11467
|
+
const parts = [];
|
|
11468
|
+
if (after.warehouseStock !== null)
|
|
11469
|
+
parts.push(`仓库 ${after.warehouseStock}`);
|
|
11470
|
+
parts.push(`上架 ${after.stock === null ? "不限" : after.stock}`);
|
|
11471
|
+
console.log(` 现值:${parts.join(" · ")}`);
|
|
11472
|
+
}
|
|
11473
|
+
if (product.kind === "physical") {
|
|
11474
|
+
console.log(" 提示:动的是仓库账;上架(销售)额度另用 crab shop listed 设定(≤ 仓库账)");
|
|
11475
|
+
}
|
|
11476
|
+
return 0;
|
|
11477
|
+
}
|
|
11478
|
+
async function shopListedStock() {
|
|
11479
|
+
const credentials = requireCredentials();
|
|
11480
|
+
if (typeof credentials === "number")
|
|
11481
|
+
return credentials;
|
|
11482
|
+
const target = shopPositionals(2, SHOP_PRODUCT_VALUED_FLAGS)[0];
|
|
11483
|
+
if (!target) {
|
|
11484
|
+
console.error(`用法:crab shop listed <id|SKU码> --stock N [--variant <组合码|变体id|"值1/值2">] [--json]
|
|
11485
|
+
实物销售库存(上架额度)直设:0 ≤ N ≤ 实际库存,不够先 crab shop stock in 入库;数字商品无仓库层(限售额度用 stock in/set 起账)`);
|
|
11486
|
+
return EXIT_USAGE;
|
|
11487
|
+
}
|
|
11488
|
+
const stockRaw = readFlag("--stock");
|
|
11489
|
+
if (stockRaw === undefined) {
|
|
11490
|
+
console.error("缺少 --stock N(上架额度,0 ~ 实际库存)");
|
|
11491
|
+
return EXIT_USAGE;
|
|
11492
|
+
}
|
|
11493
|
+
const stock = Number(stockRaw);
|
|
11494
|
+
if (!Number.isSafeInteger(stock) || stock < 0) {
|
|
11495
|
+
console.error("--stock 必须是 ≥0 的整数");
|
|
10331
11496
|
return EXIT_USAGE;
|
|
10332
11497
|
}
|
|
11498
|
+
const product = await fetchShopProduct(credentials, target);
|
|
11499
|
+
if (typeof product === "number")
|
|
11500
|
+
return product;
|
|
11501
|
+
if (product.kind !== "physical") {
|
|
11502
|
+
console.error("数字商品没有仓库层,销售库存用 crab shop stock in/set 起账");
|
|
11503
|
+
return EXIT_USAGE;
|
|
11504
|
+
}
|
|
11505
|
+
const targets = [];
|
|
11506
|
+
const variantRef = readFlag("--variant");
|
|
11507
|
+
if (product.hasVariants) {
|
|
11508
|
+
if (!variantRef) {
|
|
11509
|
+
console.error(`多规格商品必须用 --variant 指定组合:
|
|
11510
|
+
${variantChoiceList(product)}`);
|
|
11511
|
+
return EXIT_USAGE;
|
|
11512
|
+
}
|
|
11513
|
+
const variant = resolveVariantRef(product, variantRef);
|
|
11514
|
+
if (!variant) {
|
|
11515
|
+
console.error(`找不到组合:${variantRef}(可用组合:
|
|
11516
|
+
${variantChoiceList(product)})`);
|
|
11517
|
+
return EXIT_USAGE;
|
|
11518
|
+
}
|
|
11519
|
+
targets.push({ variantId: variant.id, stock });
|
|
11520
|
+
} else {
|
|
11521
|
+
if (variantRef !== undefined) {
|
|
11522
|
+
console.error("单规格商品没有组合,去掉 --variant");
|
|
11523
|
+
return EXIT_USAGE;
|
|
11524
|
+
}
|
|
11525
|
+
targets.push({ stock });
|
|
11526
|
+
}
|
|
11527
|
+
const res = await api2(credentials.apiBase, `/v1/shop/products/${product.id}/listed-stock`, {
|
|
11528
|
+
method: "POST",
|
|
11529
|
+
headers: authHeaders(credentials),
|
|
11530
|
+
body: JSON.stringify({ targets })
|
|
11531
|
+
});
|
|
11532
|
+
if (res.status !== 200) {
|
|
11533
|
+
console.error(errorMessage(res.body, "设定销售库存失败。"));
|
|
11534
|
+
const code = res.body?.error?.code;
|
|
11535
|
+
if (code === "exceeds_warehouse")
|
|
11536
|
+
console.error('提示:上架额度不能超过实际库存——先入库再上调:crab shop stock in <id> --amount N --note "…"');
|
|
11537
|
+
return exitCodeForApiError(res.body);
|
|
11538
|
+
}
|
|
11539
|
+
const items = res.body.data.items;
|
|
11540
|
+
if (hasFlag("--json")) {
|
|
11541
|
+
console.log(JSON.stringify(res.body.data, null, 2));
|
|
11542
|
+
return 0;
|
|
11543
|
+
}
|
|
11544
|
+
const after = items[0];
|
|
11545
|
+
console.log(`✓ 已设上架额度 ${stock}:${product.name}${after?.warehouseStock !== null && after?.warehouseStock !== undefined ? `(仓库 ${after.warehouseStock})` : ""}`);
|
|
11546
|
+
return 0;
|
|
11547
|
+
}
|
|
11548
|
+
var MOVEMENT_REASON_LABELS = {
|
|
11549
|
+
purchase: "采购入库",
|
|
11550
|
+
sale: "销售",
|
|
11551
|
+
manual: "手工调整",
|
|
11552
|
+
restore: "退款回补",
|
|
11553
|
+
shipment: "发货出库"
|
|
11554
|
+
};
|
|
11555
|
+
async function shopMovements() {
|
|
11556
|
+
const credentials = requireCredentials();
|
|
11557
|
+
if (typeof credentials === "number")
|
|
11558
|
+
return credentials;
|
|
10333
11559
|
const store = await resolveStoreArg(credentials);
|
|
10334
11560
|
if (typeof store === "number")
|
|
10335
11561
|
return store;
|
|
10336
|
-
const
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
11562
|
+
const params = new URLSearchParams;
|
|
11563
|
+
const productRef = readFlag("--product");
|
|
11564
|
+
if (productRef) {
|
|
11565
|
+
const product = await fetchShopProduct(credentials, productRef);
|
|
11566
|
+
if (typeof product === "number")
|
|
11567
|
+
return product;
|
|
11568
|
+
params.set("productId", product.id);
|
|
10343
11569
|
}
|
|
10344
|
-
const
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
11570
|
+
const limit = readFlag("--limit");
|
|
11571
|
+
if (limit)
|
|
11572
|
+
params.set("limit", limit);
|
|
11573
|
+
const offset = readFlag("--offset");
|
|
11574
|
+
if (offset)
|
|
11575
|
+
params.set("offset", offset);
|
|
11576
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/movements?${params.toString()}`, { headers: authHeaders(credentials) });
|
|
10348
11577
|
if (res.status !== 200) {
|
|
10349
|
-
console.error(errorMessage(res.body, "
|
|
11578
|
+
console.error(errorMessage(res.body, "获取出入库台账失败。"));
|
|
10350
11579
|
return exitCodeForApiError(res.body);
|
|
10351
11580
|
}
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
11581
|
+
const data = res.body.data;
|
|
11582
|
+
if (hasFlag("--json")) {
|
|
11583
|
+
console.log(JSON.stringify(data, null, 2));
|
|
11584
|
+
return 0;
|
|
11585
|
+
}
|
|
11586
|
+
if (data.movements.length === 0) {
|
|
11587
|
+
console.log("还没有出入库流水(入库 crab shop stock in / 发货与退款自动落账)。");
|
|
11588
|
+
return 0;
|
|
11589
|
+
}
|
|
11590
|
+
for (const m of data.movements) {
|
|
11591
|
+
const arrow = m.direction === "in" ? "入库 +" : "出库 -";
|
|
11592
|
+
const tier = m.tier === "warehouse" ? "仓库" : "上架";
|
|
11593
|
+
const combo = m.variantCombo ? `(${m.variantCombo.join("/")})` : "";
|
|
11594
|
+
const reason = MOVEMENT_REASON_LABELS[m.reason] ?? m.reason;
|
|
11595
|
+
const order = m.orderId ? ` 订单 ${m.orderId}` : "";
|
|
11596
|
+
console.log(`${formatTime(m.createdAt)} ${arrow}${m.quantity} [${tier}] ${m.productName}${combo} ${reason}${m.note ? ` ${m.note}` : ""}${order}`);
|
|
11597
|
+
}
|
|
11598
|
+
const offsetN = Number.parseInt(offset ?? "0", 10) || 0;
|
|
11599
|
+
console.log(`共 ${data.total} 条流水(本页 ${data.movements.length} 条${offsetN > 0 ? `,自第 ${offsetN + 1} 条` : ""};--offset 翻页)`);
|
|
10356
11600
|
return 0;
|
|
10357
11601
|
}
|
|
10358
11602
|
async function hrOrgsList() {
|
|
@@ -10378,29 +11622,96 @@ async function hrOrgsList() {
|
|
|
10378
11622
|
}
|
|
10379
11623
|
return 0;
|
|
10380
11624
|
}
|
|
10381
|
-
async function
|
|
11625
|
+
async function hrOrgApply(name) {
|
|
10382
11626
|
if (!name) {
|
|
10383
|
-
console.error("用法:crab hr org
|
|
11627
|
+
console.error("用法:crab hr org apply <名称> [--note 说明] [--json](审核通过扣 1 积分自动建司)");
|
|
10384
11628
|
return EXIT_USAGE;
|
|
10385
11629
|
}
|
|
11630
|
+
const note = readFlag("--note") ?? "";
|
|
10386
11631
|
const credentials = requireCredentials();
|
|
10387
11632
|
if (typeof credentials === "number")
|
|
10388
11633
|
return credentials;
|
|
10389
|
-
const res = await api2(credentials.apiBase, "/v1/hr/orgs", {
|
|
11634
|
+
const res = await api2(credentials.apiBase, "/v1/hr/orgs/applications", {
|
|
10390
11635
|
method: "POST",
|
|
10391
11636
|
headers: authHeaders(credentials),
|
|
10392
|
-
body: JSON.stringify({ name })
|
|
11637
|
+
body: JSON.stringify({ name, note })
|
|
10393
11638
|
});
|
|
10394
11639
|
if (res.status !== 200 && res.status !== 201) {
|
|
10395
|
-
|
|
11640
|
+
const code = res.body?.error?.code;
|
|
11641
|
+
if (res.status === 409 || code === "application_exists" || code === "conflict") {
|
|
11642
|
+
console.error("你已有一条待审核的建司申请(crab hr org application 查进度)。");
|
|
11643
|
+
return exitCodeForApiError(res.body);
|
|
11644
|
+
}
|
|
11645
|
+
console.error(errorMessage(res.body, "提交申请失败。"));
|
|
11646
|
+
return exitCodeForApiError(res.body);
|
|
11647
|
+
}
|
|
11648
|
+
const application = res.body.data.application;
|
|
11649
|
+
if (hasFlag("--json"))
|
|
11650
|
+
console.log(JSON.stringify(application, null, 2));
|
|
11651
|
+
else {
|
|
11652
|
+
console.log(`✓ 已提交建司申请「${application.name}」· id: ${application.id}`);
|
|
11653
|
+
console.log("审核通过后公司自动出现(通过时扣 1 积分);进度:crab hr org application");
|
|
11654
|
+
}
|
|
11655
|
+
return 0;
|
|
11656
|
+
}
|
|
11657
|
+
async function hrOrgApplication() {
|
|
11658
|
+
const credentials = requireCredentials();
|
|
11659
|
+
if (typeof credentials === "number")
|
|
11660
|
+
return credentials;
|
|
11661
|
+
const res = await api2(credentials.apiBase, "/v1/hr/orgs/applications/mine", { headers: authHeaders(credentials) });
|
|
11662
|
+
if (res.status !== 200) {
|
|
11663
|
+
console.error(errorMessage(res.body, "查询申请进度失败。"));
|
|
10396
11664
|
return exitCodeForApiError(res.body);
|
|
10397
11665
|
}
|
|
10398
|
-
const
|
|
11666
|
+
const application = res.body.data.application;
|
|
10399
11667
|
if (hasFlag("--json")) {
|
|
10400
|
-
console.log(JSON.stringify(
|
|
11668
|
+
console.log(JSON.stringify(application, null, 2));
|
|
11669
|
+
return 0;
|
|
11670
|
+
}
|
|
11671
|
+
if (!application) {
|
|
11672
|
+
console.log("还没有建司申请(crab hr org apply <名称> 提交)。");
|
|
10401
11673
|
return 0;
|
|
10402
11674
|
}
|
|
10403
|
-
|
|
11675
|
+
const day = new Date(application.createdAt).toISOString().slice(0, 10);
|
|
11676
|
+
if (application.status === "pending") {
|
|
11677
|
+
console.log(`审核中:${application.name}(${day} 提交)· id: ${application.id}`);
|
|
11678
|
+
console.log("通过时扣 1 积分并自动建司,无需其他操作。");
|
|
11679
|
+
} else if (application.status === "approved") {
|
|
11680
|
+
console.log(`✓ 已通过:${application.name}(${day} 提交)· 公司已建立`);
|
|
11681
|
+
console.log("绑定新组织:crab login --scopes hr --org org:<公司名>");
|
|
11682
|
+
} else {
|
|
11683
|
+
console.log(`✗ 已驳回:${application.name}(${day} 提交)`);
|
|
11684
|
+
if (application.reviewNote)
|
|
11685
|
+
console.log(` 驳回意见:${application.reviewNote}`);
|
|
11686
|
+
console.log("可修改后重新提交:crab hr org apply <名称> [--note 说明]");
|
|
11687
|
+
}
|
|
11688
|
+
return 0;
|
|
11689
|
+
}
|
|
11690
|
+
async function hrOrgRename() {
|
|
11691
|
+
const name = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
11692
|
+
if (!name) {
|
|
11693
|
+
console.error("用法:crab hr org rename <新名称> [--org <arg>] [--json]");
|
|
11694
|
+
return EXIT_USAGE;
|
|
11695
|
+
}
|
|
11696
|
+
const credentials = requireCredentials();
|
|
11697
|
+
if (typeof credentials === "number")
|
|
11698
|
+
return credentials;
|
|
11699
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11700
|
+
if (typeof org === "number")
|
|
11701
|
+
return org;
|
|
11702
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}`, {
|
|
11703
|
+
method: "PATCH",
|
|
11704
|
+
headers: authHeaders(credentials),
|
|
11705
|
+
body: JSON.stringify({ name })
|
|
11706
|
+
});
|
|
11707
|
+
if (res.status !== 200) {
|
|
11708
|
+
console.error(errorMessage(res.body, "组织改名失败。"));
|
|
11709
|
+
return exitCodeForApiError(res.body);
|
|
11710
|
+
}
|
|
11711
|
+
if (hasFlag("--json"))
|
|
11712
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
11713
|
+
else
|
|
11714
|
+
console.log(`✓ 已改名:${org.name} → ${name}(org: 令牌绑定标识随新名变化,注意重新授权)。`);
|
|
10404
11715
|
return 0;
|
|
10405
11716
|
}
|
|
10406
11717
|
async function hrMembersList() {
|
|
@@ -10441,10 +11752,13 @@ async function hrMemberAdd() {
|
|
|
10441
11752
|
return EXIT_USAGE;
|
|
10442
11753
|
}
|
|
10443
11754
|
const title = readFlag("--title");
|
|
11755
|
+
const permissions = parseHrPermissions(readFlag("--permissions"));
|
|
11756
|
+
if (permissions === null)
|
|
11757
|
+
return EXIT_USAGE;
|
|
10444
11758
|
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members`, {
|
|
10445
11759
|
method: "POST",
|
|
10446
11760
|
headers: authHeaders(credentials),
|
|
10447
|
-
body: JSON.stringify({ username: target, ...title ? { title } : {} })
|
|
11761
|
+
body: JSON.stringify({ username: target, ...title ? { title } : {}, ...permissions ? { permissions } : {} })
|
|
10448
11762
|
});
|
|
10449
11763
|
if (res.status !== 200 && res.status !== 201) {
|
|
10450
11764
|
console.error(errorMessage(res.body, "邀请成员失败。"));
|
|
@@ -10452,21 +11766,11 @@ async function hrMemberAdd() {
|
|
|
10452
11766
|
}
|
|
10453
11767
|
if (hasFlag("--json"))
|
|
10454
11768
|
console.log(JSON.stringify(res.body.data, null, 2));
|
|
10455
|
-
else
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
}
|
|
10459
|
-
function parsePermissionFlags(text) {
|
|
10460
|
-
if (!text)
|
|
10461
|
-
return;
|
|
10462
|
-
const out = {};
|
|
10463
|
-
for (const pair of text.split(",").map((x) => x.trim()).filter(Boolean)) {
|
|
10464
|
-
const [key, value] = pair.split("=");
|
|
10465
|
-
if (!key)
|
|
10466
|
-
continue;
|
|
10467
|
-
out[key.trim()] = value !== "0" && value?.toLowerCase() !== "false";
|
|
11769
|
+
else {
|
|
11770
|
+
const on = permissions ? Object.entries(permissions).filter(([, v]) => v).map(([k]) => k).join(",") : "";
|
|
11771
|
+
console.log(`✓ 已把 ${target} 加入 ${org.name}(member${on ? `,权限:${on}` : ",只读"};用 hr member set 调整)。`);
|
|
10468
11772
|
}
|
|
10469
|
-
return
|
|
11773
|
+
return 0;
|
|
10470
11774
|
}
|
|
10471
11775
|
async function hrMemberSet() {
|
|
10472
11776
|
const credentials = requireCredentials();
|
|
@@ -10477,7 +11781,9 @@ async function hrMemberSet() {
|
|
|
10477
11781
|
return org;
|
|
10478
11782
|
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
10479
11783
|
const role = readFlag("--role");
|
|
10480
|
-
const permissions =
|
|
11784
|
+
const permissions = parseHrPermissions(readFlag("--permissions"));
|
|
11785
|
+
if (permissions === null)
|
|
11786
|
+
return EXIT_USAGE;
|
|
10481
11787
|
const title = readFlag("--title");
|
|
10482
11788
|
if (!target || !role && !permissions && title === undefined) {
|
|
10483
11789
|
console.error("用法:crab hr member set <memberId> [--role admin|member] [--permissions org.manage=1,people.write=1,tasks.write=1,sensitive.read=0] [--title 头衔] [--org <arg>]");
|
|
@@ -10536,6 +11842,28 @@ async function hrInvite() {
|
|
|
10536
11842
|
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10537
11843
|
if (typeof org === "number")
|
|
10538
11844
|
return org;
|
|
11845
|
+
const permissionsInput = parseHrPermissions(readFlag("--permissions"));
|
|
11846
|
+
if (permissionsInput === null)
|
|
11847
|
+
return EXIT_USAGE;
|
|
11848
|
+
if (permissionsInput) {
|
|
11849
|
+
const patch = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/invite`, {
|
|
11850
|
+
method: "PATCH",
|
|
11851
|
+
headers: authHeaders(credentials),
|
|
11852
|
+
body: JSON.stringify({ permissions: permissionsInput })
|
|
11853
|
+
});
|
|
11854
|
+
if (patch.status !== 200) {
|
|
11855
|
+
console.error(errorMessage(patch.body, "更新邀请默认权限失败。"));
|
|
11856
|
+
return exitCodeForApiError(patch.body);
|
|
11857
|
+
}
|
|
11858
|
+
const updated = patch.body.data.invite;
|
|
11859
|
+
if (hasFlag("--json")) {
|
|
11860
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
11861
|
+
return 0;
|
|
11862
|
+
}
|
|
11863
|
+
const on = Object.entries(updated.permissions).filter(([, v]) => v).map(([k]) => k);
|
|
11864
|
+
console.log(`✓ 已更新加入后默认权限:${on.length ? on.join(", ") : "全只读"}(链接不变)。`);
|
|
11865
|
+
return 0;
|
|
11866
|
+
}
|
|
10539
11867
|
const path = hasFlag("--reset") ? "/invite/reset" : "/invite";
|
|
10540
11868
|
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}${path}`, { method: hasFlag("--reset") ? "POST" : "GET", headers: authHeaders(credentials) });
|
|
10541
11869
|
if (res.status !== 200) {
|
|
@@ -10543,14 +11871,15 @@ async function hrInvite() {
|
|
|
10543
11871
|
return exitCodeForApiError(res.body);
|
|
10544
11872
|
}
|
|
10545
11873
|
const invite = res.body.data.invite;
|
|
10546
|
-
const
|
|
10547
|
-
const link = `${main}/register?invite=${invite.token}`;
|
|
11874
|
+
const link = `${hrWebBase(credentials.apiBase)}/join/${invite.token}`;
|
|
10548
11875
|
if (hasFlag("--json"))
|
|
10549
11876
|
console.log(JSON.stringify({ ...invite, link }, null, 2));
|
|
10550
11877
|
else {
|
|
10551
11878
|
if (hasFlag("--reset"))
|
|
10552
|
-
console.log("✓
|
|
11879
|
+
console.log("✓ 已重置(旧链接立即失效,默认权限配置保留)。");
|
|
11880
|
+
const on = Object.entries(invite.permissions).filter(([, v]) => v).map(([k]) => k);
|
|
10553
11881
|
console.log(`邀请链接:${link}`);
|
|
11882
|
+
console.log(`加入后默认权限:${on.length ? on.join(", ") : "全只读"}(修改:--permissions org.manage=1,...)`);
|
|
10554
11883
|
console.log("未注册的老板 / 同事打开此链接,注册后自动加入组织。");
|
|
10555
11884
|
}
|
|
10556
11885
|
return 0;
|
|
@@ -10619,13 +11948,20 @@ async function hrPersonDetail(id) {
|
|
|
10619
11948
|
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10620
11949
|
if (typeof org === "number")
|
|
10621
11950
|
return org;
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
11951
|
+
let person;
|
|
11952
|
+
let offset = 0;
|
|
11953
|
+
while (person === undefined) {
|
|
11954
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people?limit=200&offset=${offset}`, { headers: authHeaders(credentials) });
|
|
11955
|
+
if (res.status !== 200) {
|
|
11956
|
+
console.error(errorMessage(res.body, "获取人员失败。"));
|
|
11957
|
+
return exitCodeForApiError(res.body);
|
|
11958
|
+
}
|
|
11959
|
+
const page = res.body.data;
|
|
11960
|
+
person = page.people.find((p) => p.id === id);
|
|
11961
|
+
offset += page.people.length;
|
|
11962
|
+
if (person === undefined && (page.people.length === 0 || offset >= page.total))
|
|
11963
|
+
break;
|
|
10626
11964
|
}
|
|
10627
|
-
const people = res.body.data.people;
|
|
10628
|
-
const person = people.find((p) => p.id === id);
|
|
10629
11965
|
if (!person) {
|
|
10630
11966
|
console.error("人员不存在或不属于当前组织。");
|
|
10631
11967
|
return EXIT_NOT_FOUND;
|
|
@@ -10814,6 +12150,73 @@ async function hrPersonOffboard() {
|
|
|
10814
12150
|
console.log(`✓ ${person?.name ?? target} 已离职(档案封存,离职事件已落时间线)。`);
|
|
10815
12151
|
return 0;
|
|
10816
12152
|
}
|
|
12153
|
+
async function hrPersonRm() {
|
|
12154
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12155
|
+
if (!target) {
|
|
12156
|
+
console.error("用法:crab hr person rm <id> [--org <arg>] [--json](仅零记录档案可删;有事件时间线走离职封存)");
|
|
12157
|
+
return EXIT_USAGE;
|
|
12158
|
+
}
|
|
12159
|
+
const credentials = requireCredentials();
|
|
12160
|
+
if (typeof credentials === "number")
|
|
12161
|
+
return credentials;
|
|
12162
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12163
|
+
if (typeof org === "number")
|
|
12164
|
+
return org;
|
|
12165
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/${encodeURIComponent(target)}`, {
|
|
12166
|
+
method: "DELETE",
|
|
12167
|
+
headers: authHeaders(credentials)
|
|
12168
|
+
});
|
|
12169
|
+
if (res.status !== 200) {
|
|
12170
|
+
const code = res.body?.error?.code;
|
|
12171
|
+
if (res.status === 409 || code === "conflict") {
|
|
12172
|
+
console.error("该档案已有事件记录(时间线即证据链),不能删除——请走离职封存:crab hr person offboard。");
|
|
12173
|
+
return exitCodeForApiError(res.body);
|
|
12174
|
+
}
|
|
12175
|
+
console.error(errorMessage(res.body, "删除人员失败。"));
|
|
12176
|
+
return exitCodeForApiError(res.body);
|
|
12177
|
+
}
|
|
12178
|
+
if (hasFlag("--json"))
|
|
12179
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
12180
|
+
else
|
|
12181
|
+
console.log("✓ 已删除(零记录档案)。");
|
|
12182
|
+
return 0;
|
|
12183
|
+
}
|
|
12184
|
+
function hrStatsLines(s) {
|
|
12185
|
+
const lines = [`在职 ${s.active} · 试用 ${s.probation} · 离职 ${s.resigned} · 共 ${s.total} 人`];
|
|
12186
|
+
const risks = [];
|
|
12187
|
+
if (s.contractDueSoon)
|
|
12188
|
+
risks.push(`合同 60 天内到期 ${s.contractDueSoon} 人`);
|
|
12189
|
+
if (s.contractOverdue)
|
|
12190
|
+
risks.push(`合同已到期未续签 ${s.contractOverdue} 人`);
|
|
12191
|
+
if (s.probationDueSoon)
|
|
12192
|
+
risks.push(`试用期 30 天内届满 ${s.probationDueSoon} 人`);
|
|
12193
|
+
if (s.probationOverdue)
|
|
12194
|
+
risks.push(`试用期已过未转正 ${s.probationOverdue} 人`);
|
|
12195
|
+
lines.push(risks.length ? `风险:${risks.join(" · ")}` : "风险:无");
|
|
12196
|
+
return lines;
|
|
12197
|
+
}
|
|
12198
|
+
async function hrStats() {
|
|
12199
|
+
const credentials = requireCredentials();
|
|
12200
|
+
if (typeof credentials === "number")
|
|
12201
|
+
return credentials;
|
|
12202
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12203
|
+
if (typeof org === "number")
|
|
12204
|
+
return org;
|
|
12205
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/stats`, { headers: authHeaders(credentials) });
|
|
12206
|
+
if (res.status !== 200) {
|
|
12207
|
+
console.error(errorMessage(res.body, "获取统计失败。"));
|
|
12208
|
+
return exitCodeForApiError(res.body);
|
|
12209
|
+
}
|
|
12210
|
+
const stats = res.body.data;
|
|
12211
|
+
if (hasFlag("--json")) {
|
|
12212
|
+
console.log(JSON.stringify({ ...stats, org: { id: org.id, name: org.name } }, null, 2));
|
|
12213
|
+
return 0;
|
|
12214
|
+
}
|
|
12215
|
+
console.log(`组织:${hrOrgToken(org)} · 花名册统计`);
|
|
12216
|
+
for (const line of hrStatsLines(stats.stats))
|
|
12217
|
+
console.log(line);
|
|
12218
|
+
return 0;
|
|
12219
|
+
}
|
|
10817
12220
|
async function hrEventAdd() {
|
|
10818
12221
|
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
10819
12222
|
const type = readFlag("--type");
|
|
@@ -11057,7 +12460,7 @@ async function hrTaskCreate() {
|
|
|
11057
12460
|
const kind = readFlag("--kind");
|
|
11058
12461
|
const title = readFlag("--title");
|
|
11059
12462
|
if (!kind || !title) {
|
|
11060
|
-
console.error('用法:crab hr task create --kind <attendance|payroll|contract|recruit|meeting|asset|admin|...> --title "..." [--node 节点说明] [--detail 详情] [--due YYYY-MM-DD] [--status alert|confirm|queued|done] [--json]');
|
|
12463
|
+
console.error('用法:crab hr task create --kind <attendance|payroll|contract|recruit|meeting|asset|admin|person/onboard|...> --title "..." [--node 节点说明] [--detail 详情] [--due YYYY-MM-DD] [--status alert|confirm|queued|done] [--meta JSON] [--json]');
|
|
11061
12464
|
return EXIT_USAGE;
|
|
11062
12465
|
}
|
|
11063
12466
|
const status = readFlag("--status");
|
|
@@ -11077,10 +12480,20 @@ async function hrTaskCreate() {
|
|
|
11077
12480
|
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11078
12481
|
if (typeof org === "number")
|
|
11079
12482
|
return org;
|
|
12483
|
+
let meta;
|
|
12484
|
+
const metaFlag = readFlag("--meta");
|
|
12485
|
+
if (metaFlag !== undefined) {
|
|
12486
|
+
try {
|
|
12487
|
+
meta = JSON.parse(metaFlag);
|
|
12488
|
+
} catch {
|
|
12489
|
+
console.error("--meta 需要合法 JSON(申请单任务须含 meta.form,服务端按 kind schema 校验)。");
|
|
12490
|
+
return EXIT_USAGE;
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
11080
12493
|
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/tasks`, {
|
|
11081
12494
|
method: "POST",
|
|
11082
12495
|
headers: authHeaders(credentials),
|
|
11083
|
-
body: JSON.stringify({ kind, title, node: readFlag("--node") ?? "", detail: readFlag("--detail") ?? "", ...status ? { status } : {}, ...due != null ? { dueAt: due } : {} })
|
|
12496
|
+
body: JSON.stringify({ kind, title, node: readFlag("--node") ?? "", detail: readFlag("--detail") ?? "", ...status ? { status } : {}, ...due != null ? { dueAt: due } : {}, ...meta ? { meta } : {} })
|
|
11084
12497
|
});
|
|
11085
12498
|
if (res.status !== 200 && res.status !== 201) {
|
|
11086
12499
|
console.error(errorMessage(res.body, "创建任务失败。"));
|
|
@@ -11132,6 +12545,42 @@ async function hrTaskUpdate() {
|
|
|
11132
12545
|
console.log(`✓ 已更新 ${task.title}(${task.status})。`);
|
|
11133
12546
|
return 0;
|
|
11134
12547
|
}
|
|
12548
|
+
async function hrTaskLink() {
|
|
12549
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12550
|
+
if (!target) {
|
|
12551
|
+
console.error("用法:crab hr task link <taskId> [--reset] [--org <arg>] [--json](仅入职/转正/离职申请单有候选人表单)");
|
|
12552
|
+
return EXIT_USAGE;
|
|
12553
|
+
}
|
|
12554
|
+
const credentials = requireCredentials();
|
|
12555
|
+
if (typeof credentials === "number")
|
|
12556
|
+
return credentials;
|
|
12557
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12558
|
+
if (typeof org === "number")
|
|
12559
|
+
return org;
|
|
12560
|
+
const reset = hasFlag("--reset");
|
|
12561
|
+
const path = `/orgs/${encodeURIComponent(org.id)}/tasks/${encodeURIComponent(target)}${reset ? "/form-link/reset" : "/form-link"}`;
|
|
12562
|
+
const res = await api2(credentials.apiBase, `/v1/hr${path}`, { method: reset ? "POST" : "GET", headers: authHeaders(credentials) });
|
|
12563
|
+
if (res.status !== 200) {
|
|
12564
|
+
const code = res.body?.error?.code;
|
|
12565
|
+
if (res.status === 400 && code === "bad_request") {
|
|
12566
|
+
console.error("该任务没有候选人填单表单(仅 person/onboard · person/promotion · person/offboard 申请单支持)。");
|
|
12567
|
+
return exitCodeForApiError(res.body);
|
|
12568
|
+
}
|
|
12569
|
+
console.error(errorMessage(res.body, "获取填单链接失败。"));
|
|
12570
|
+
return exitCodeForApiError(res.body);
|
|
12571
|
+
}
|
|
12572
|
+
const token = res.body.data.token;
|
|
12573
|
+
const link = `${hrWebBase(credentials.apiBase)}/form/${token}`;
|
|
12574
|
+
if (hasFlag("--json"))
|
|
12575
|
+
console.log(JSON.stringify({ token, link }, null, 2));
|
|
12576
|
+
else {
|
|
12577
|
+
if (reset)
|
|
12578
|
+
console.log("✓ 已重置(旧链接立即失效)。");
|
|
12579
|
+
console.log(`填单链接:${link}`);
|
|
12580
|
+
console.log("候选人免登录打开即可填写本人区字段;提交后申请单进待拍板。");
|
|
12581
|
+
}
|
|
12582
|
+
return 0;
|
|
12583
|
+
}
|
|
11135
12584
|
async function hrDesk() {
|
|
11136
12585
|
const credentials = requireCredentials();
|
|
11137
12586
|
if (typeof credentials === "number")
|
|
@@ -11150,29 +12599,41 @@ async function hrDesk() {
|
|
|
11150
12599
|
return 0;
|
|
11151
12600
|
}
|
|
11152
12601
|
console.log(`组织:${hrOrgToken(org)} · 工作台`);
|
|
12602
|
+
for (const line of hrStatsLines(desk.desk.stats))
|
|
12603
|
+
console.log(line);
|
|
12604
|
+
console.log(`
|
|
12605
|
+
替你盯着:`);
|
|
12606
|
+
for (const reminder of desk.desk.reminders) {
|
|
12607
|
+
const chip = reminder.kind === "onboard_pending" ? `已等 ${reminder.days} 天` : reminder.days < 0 ? `过期 ${-reminder.days} 天` : `剩 ${reminder.days} 天`;
|
|
12608
|
+
console.log(` ⚠ ${reminder.text}(${chip})`);
|
|
12609
|
+
}
|
|
12610
|
+
if (!desk.desk.reminders.length)
|
|
12611
|
+
console.log(" (无)");
|
|
11153
12612
|
console.log(`
|
|
11154
12613
|
等你拍板:`);
|
|
11155
12614
|
for (const task of desk.desk.pending)
|
|
11156
|
-
console.log(` [${task.status}] ${task.title} ${task.node || ""} id: ${task.id}`);
|
|
12615
|
+
console.log(` [${task.status}] ${task.title}${task.refLabel ? ` · ${task.refLabel}` : ""} ${task.node || ""} id: ${task.id}`);
|
|
11157
12616
|
if (!desk.desk.pending.length)
|
|
11158
12617
|
console.log(" (空)");
|
|
11159
12618
|
console.log(`
|
|
11160
12619
|
助理在办:`);
|
|
11161
12620
|
for (const task of desk.desk.running)
|
|
11162
|
-
console.log(` ${task.title} ${task.node || ""} id: ${task.id}`);
|
|
12621
|
+
console.log(` ${task.title}${task.refLabel ? ` · ${task.refLabel}` : ""} ${task.node || ""} id: ${task.id}`);
|
|
11163
12622
|
if (!desk.desk.running.length)
|
|
11164
12623
|
console.log(" (空)");
|
|
11165
12624
|
console.log(`
|
|
11166
12625
|
近期完成:`);
|
|
11167
12626
|
for (const task of desk.desk.done)
|
|
11168
|
-
console.log(` ${task.title} ${task.node || ""}`);
|
|
12627
|
+
console.log(` ${task.title}${task.refLabel ? ` · ${task.refLabel}` : ""} ${task.node || ""}`);
|
|
11169
12628
|
if (!desk.desk.done.length)
|
|
11170
12629
|
console.log(" (空)");
|
|
11171
12630
|
console.log(`
|
|
11172
|
-
|
|
11173
|
-
for (const
|
|
11174
|
-
|
|
11175
|
-
|
|
12631
|
+
最近公告:`);
|
|
12632
|
+
for (const item of desk.desk.announcements.slice(0, 3)) {
|
|
12633
|
+
const day = new Date(item.createdAt).toISOString().slice(0, 10);
|
|
12634
|
+
console.log(` ${day} ${item.title}(${item.createdByName})`);
|
|
12635
|
+
}
|
|
12636
|
+
if (!desk.desk.announcements.length)
|
|
11176
12637
|
console.log(" (无)");
|
|
11177
12638
|
console.log(`
|
|
11178
12639
|
最近动态:`);
|
|
@@ -11184,6 +12645,259 @@ async function hrDesk() {
|
|
|
11184
12645
|
console.log(" (无)");
|
|
11185
12646
|
return 0;
|
|
11186
12647
|
}
|
|
12648
|
+
function hrAnnouncementStatus(s) {
|
|
12649
|
+
return s === "active" ? "[生效]" : "[已撤回]";
|
|
12650
|
+
}
|
|
12651
|
+
async function hrAnnounce() {
|
|
12652
|
+
const op = args[2];
|
|
12653
|
+
const credentials = requireCredentials();
|
|
12654
|
+
if (typeof credentials === "number")
|
|
12655
|
+
return credentials;
|
|
12656
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12657
|
+
if (typeof org === "number")
|
|
12658
|
+
return org;
|
|
12659
|
+
const base = `/v1/hr/orgs/${encodeURIComponent(org.id)}/announcements`;
|
|
12660
|
+
if (op === "list") {
|
|
12661
|
+
const params = new URLSearchParams;
|
|
12662
|
+
if (hasFlag("--all"))
|
|
12663
|
+
params.set("includeWithdrawn", "1");
|
|
12664
|
+
const limit = readFlag("--limit");
|
|
12665
|
+
if (limit)
|
|
12666
|
+
params.set("limit", limit);
|
|
12667
|
+
const offset = readFlag("--offset");
|
|
12668
|
+
if (offset)
|
|
12669
|
+
params.set("offset", offset);
|
|
12670
|
+
const res = await api2(credentials.apiBase, `${base}?${params}`, { headers: authHeaders(credentials) });
|
|
12671
|
+
if (res.status !== 200) {
|
|
12672
|
+
console.error(errorMessage(res.body, "获取公告失败。"));
|
|
12673
|
+
return exitCodeForApiError(res.body);
|
|
12674
|
+
}
|
|
12675
|
+
const page = res.body.data;
|
|
12676
|
+
if (hasFlag("--json")) {
|
|
12677
|
+
console.log(JSON.stringify({ ...page, org: { id: org.id, name: org.name } }, null, 2));
|
|
12678
|
+
return 0;
|
|
12679
|
+
}
|
|
12680
|
+
console.log(`组织:${hrOrgToken(org)} · 公告 ${page.total} 条`);
|
|
12681
|
+
for (const a of page.announcements) {
|
|
12682
|
+
const day = new Date(a.createdAt).toISOString().slice(0, 10);
|
|
12683
|
+
console.log(`${day} ${hrAnnouncementStatus(a.status)} ${a.title}(${a.createdByName}) id: ${a.id}`);
|
|
12684
|
+
console.log(` ${a.body.length > 120 ? `${a.body.slice(0, 120)}…` : a.body}`);
|
|
12685
|
+
}
|
|
12686
|
+
if (page.announcements.length < page.total)
|
|
12687
|
+
console.log(`(还有更多:--offset ${(Number(offset ?? 0) || 0) + page.announcements.length})`);
|
|
12688
|
+
return 0;
|
|
12689
|
+
}
|
|
12690
|
+
if (op === "post") {
|
|
12691
|
+
const title = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12692
|
+
const body = readFlag("--body");
|
|
12693
|
+
if (!title || !body) {
|
|
12694
|
+
console.error('用法:crab hr announce post <标题> --body "正文" [--org <arg>] [--json]');
|
|
12695
|
+
return EXIT_USAGE;
|
|
12696
|
+
}
|
|
12697
|
+
const res = await api2(credentials.apiBase, base, {
|
|
12698
|
+
method: "POST",
|
|
12699
|
+
headers: authHeaders(credentials),
|
|
12700
|
+
body: JSON.stringify({ title, body })
|
|
12701
|
+
});
|
|
12702
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
12703
|
+
console.error(errorMessage(res.body, "发布公告失败。"));
|
|
12704
|
+
return exitCodeForApiError(res.body);
|
|
12705
|
+
}
|
|
12706
|
+
const a = res.body.data.announcement;
|
|
12707
|
+
if (hasFlag("--json"))
|
|
12708
|
+
console.log(JSON.stringify(a, null, 2));
|
|
12709
|
+
else
|
|
12710
|
+
console.log(`✓ 已发布「${a.title}」· id: ${a.id}(成员收件箱可见)。`);
|
|
12711
|
+
return 0;
|
|
12712
|
+
}
|
|
12713
|
+
if (op === "withdraw") {
|
|
12714
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12715
|
+
if (!target) {
|
|
12716
|
+
console.error("用法:crab hr announce withdraw <公告id> [--org <arg>] [--json](撤回留档不删)");
|
|
12717
|
+
return EXIT_USAGE;
|
|
12718
|
+
}
|
|
12719
|
+
const res = await api2(credentials.apiBase, `${base}/${encodeURIComponent(target)}/withdraw`, { method: "POST", headers: authHeaders(credentials) });
|
|
12720
|
+
if (res.status !== 200) {
|
|
12721
|
+
console.error(errorMessage(res.body, "撤回失败。"));
|
|
12722
|
+
return exitCodeForApiError(res.body);
|
|
12723
|
+
}
|
|
12724
|
+
const a = res.body.data.announcement;
|
|
12725
|
+
if (hasFlag("--json"))
|
|
12726
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
12727
|
+
else
|
|
12728
|
+
console.log(`✓ 已撤回「${a?.title ?? target}」(成员不再可见,记录保留)。`);
|
|
12729
|
+
return 0;
|
|
12730
|
+
}
|
|
12731
|
+
console.error(`用法:crab hr announce list | post <标题> --body "..." | withdraw <id>
|
|
12732
|
+
`);
|
|
12733
|
+
return EXIT_USAGE;
|
|
12734
|
+
}
|
|
12735
|
+
async function hrInbox() {
|
|
12736
|
+
const credentials = requireCredentials();
|
|
12737
|
+
if (typeof credentials === "number")
|
|
12738
|
+
return credentials;
|
|
12739
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12740
|
+
if (typeof org === "number")
|
|
12741
|
+
return org;
|
|
12742
|
+
const base = `/v1/hr/orgs/${encodeURIComponent(org.id)}/inbox`;
|
|
12743
|
+
if (hasFlag("--seen")) {
|
|
12744
|
+
const res = await api2(credentials.apiBase, `${base}/seen`, { method: "POST", headers: authHeaders(credentials) });
|
|
12745
|
+
if (res.status !== 200) {
|
|
12746
|
+
console.error(errorMessage(res.body, "标记已读失败。"));
|
|
12747
|
+
return exitCodeForApiError(res.body);
|
|
12748
|
+
}
|
|
12749
|
+
if (hasFlag("--json"))
|
|
12750
|
+
console.log(JSON.stringify(res.body.data, null, 2));
|
|
12751
|
+
else
|
|
12752
|
+
console.log("✓ 公告已全部标记已读。");
|
|
12753
|
+
return 0;
|
|
12754
|
+
}
|
|
12755
|
+
const res = await api2(credentials.apiBase, base, { headers: authHeaders(credentials) });
|
|
12756
|
+
if (res.status !== 200) {
|
|
12757
|
+
console.error(errorMessage(res.body, "获取收件箱失败。"));
|
|
12758
|
+
return exitCodeForApiError(res.body);
|
|
12759
|
+
}
|
|
12760
|
+
const inbox = res.body.data.inbox;
|
|
12761
|
+
if (hasFlag("--json")) {
|
|
12762
|
+
console.log(JSON.stringify({ ...res.body.data, org: { id: org.id, name: org.name } }, null, 2));
|
|
12763
|
+
return 0;
|
|
12764
|
+
}
|
|
12765
|
+
console.log(`组织:${hrOrgToken(org)} · 收件箱(上次打开 ${inbox.lastSeenAt ? new Date(inbox.lastSeenAt).toLocaleDateString("zh-CN") : "从未"})`);
|
|
12766
|
+
console.log(`
|
|
12767
|
+
未读公告 ${inbox.unreadAnnouncementCount} 条:`);
|
|
12768
|
+
for (const a of inbox.announcements)
|
|
12769
|
+
console.log(` ${new Date(a.createdAt).toISOString().slice(0, 10)} ${a.title}(${a.createdByName}) id: ${a.id}`);
|
|
12770
|
+
if (!inbox.announcements.length)
|
|
12771
|
+
console.log(" (无)");
|
|
12772
|
+
console.log(`
|
|
12773
|
+
待拍板 ${inbox.pendingTaskCount} 条:`);
|
|
12774
|
+
for (const t of inbox.pendingTasks)
|
|
12775
|
+
console.log(` [${t.status}] ${t.title} ${t.node || ""} id: ${t.id}`);
|
|
12776
|
+
if (!inbox.pendingTasks.length)
|
|
12777
|
+
console.log(" (无)");
|
|
12778
|
+
console.log(`
|
|
12779
|
+
替你盯着:`);
|
|
12780
|
+
for (const r of inbox.reminders)
|
|
12781
|
+
console.log(` ⚠ ${r.text}(${r.days < 0 ? `过期 ${-r.days} 天` : `剩 ${r.days} 天`})`);
|
|
12782
|
+
if (!inbox.reminders.length)
|
|
12783
|
+
console.log(" (无)");
|
|
12784
|
+
console.log(`
|
|
12785
|
+
打开看完后标记已读:crab hr inbox --seen`);
|
|
12786
|
+
return 0;
|
|
12787
|
+
}
|
|
12788
|
+
async function hrDicts() {
|
|
12789
|
+
const credentials = requireCredentials();
|
|
12790
|
+
if (typeof credentials === "number")
|
|
12791
|
+
return credentials;
|
|
12792
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12793
|
+
if (typeof org === "number")
|
|
12794
|
+
return org;
|
|
12795
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/dicts`, { headers: authHeaders(credentials) });
|
|
12796
|
+
if (res.status !== 200) {
|
|
12797
|
+
console.error(errorMessage(res.body, "获取字典失败。"));
|
|
12798
|
+
return exitCodeForApiError(res.body);
|
|
12799
|
+
}
|
|
12800
|
+
const entries = res.body.data.entries;
|
|
12801
|
+
if (hasFlag("--json")) {
|
|
12802
|
+
console.log(JSON.stringify({ ...res.body.data, org: { id: org.id, name: org.name } }, null, 2));
|
|
12803
|
+
return 0;
|
|
12804
|
+
}
|
|
12805
|
+
for (const type of HR_DICT_TYPES) {
|
|
12806
|
+
const list = entries.filter((e) => e.type === type);
|
|
12807
|
+
console.log(`${type === "dept" ? "部门" : "岗位"}(${list.length}):`);
|
|
12808
|
+
for (const e of list)
|
|
12809
|
+
console.log(` ${e.name} id: ${e.id}`);
|
|
12810
|
+
if (!list.length)
|
|
12811
|
+
console.log(" (空)");
|
|
12812
|
+
}
|
|
12813
|
+
return 0;
|
|
12814
|
+
}
|
|
12815
|
+
async function hrDictAdd() {
|
|
12816
|
+
const type = args[3];
|
|
12817
|
+
const name = args[4];
|
|
12818
|
+
if (!type || !name || !HR_DICT_TYPES.includes(type)) {
|
|
12819
|
+
console.error("用法:crab hr dict add <dept|position> <名称> [--org <arg>] [--json]");
|
|
12820
|
+
return EXIT_USAGE;
|
|
12821
|
+
}
|
|
12822
|
+
const credentials = requireCredentials();
|
|
12823
|
+
if (typeof credentials === "number")
|
|
12824
|
+
return credentials;
|
|
12825
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12826
|
+
if (typeof org === "number")
|
|
12827
|
+
return org;
|
|
12828
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/dicts`, {
|
|
12829
|
+
method: "POST",
|
|
12830
|
+
headers: authHeaders(credentials),
|
|
12831
|
+
body: JSON.stringify({ type, name })
|
|
12832
|
+
});
|
|
12833
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
12834
|
+
console.error(errorMessage(res.body, "新增字典项失败。"));
|
|
12835
|
+
return exitCodeForApiError(res.body);
|
|
12836
|
+
}
|
|
12837
|
+
const entry = res.body.data.entry;
|
|
12838
|
+
if (hasFlag("--json"))
|
|
12839
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
12840
|
+
else
|
|
12841
|
+
console.log(`✓ 已新增${type === "dept" ? "部门" : "岗位"}「${entry.name}」· id: ${entry.id}(同名幂等返回已有项)。`);
|
|
12842
|
+
return 0;
|
|
12843
|
+
}
|
|
12844
|
+
async function hrDictRm() {
|
|
12845
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12846
|
+
if (!target) {
|
|
12847
|
+
console.error("用法:crab hr dict rm <entryId> [--org <arg>] [--json](被花名册在用的项服务端 409 拒删)");
|
|
12848
|
+
return EXIT_USAGE;
|
|
12849
|
+
}
|
|
12850
|
+
const credentials = requireCredentials();
|
|
12851
|
+
if (typeof credentials === "number")
|
|
12852
|
+
return credentials;
|
|
12853
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12854
|
+
if (typeof org === "number")
|
|
12855
|
+
return org;
|
|
12856
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/dicts/${encodeURIComponent(target)}`, {
|
|
12857
|
+
method: "DELETE",
|
|
12858
|
+
headers: authHeaders(credentials)
|
|
12859
|
+
});
|
|
12860
|
+
if (res.status !== 200) {
|
|
12861
|
+
const code = res.body?.error?.code;
|
|
12862
|
+
if (res.status === 409 || code === "conflict") {
|
|
12863
|
+
console.error("该字典项正被花名册人员使用,不能删除(先改人员的部门/岗位)。");
|
|
12864
|
+
return exitCodeForApiError(res.body);
|
|
12865
|
+
}
|
|
12866
|
+
console.error(errorMessage(res.body, "删除字典项失败。"));
|
|
12867
|
+
return exitCodeForApiError(res.body);
|
|
12868
|
+
}
|
|
12869
|
+
if (hasFlag("--json"))
|
|
12870
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
12871
|
+
else
|
|
12872
|
+
console.log("✓ 已删除字典项。");
|
|
12873
|
+
return 0;
|
|
12874
|
+
}
|
|
12875
|
+
async function hrApplyLink() {
|
|
12876
|
+
const credentials = requireCredentials();
|
|
12877
|
+
if (typeof credentials === "number")
|
|
12878
|
+
return credentials;
|
|
12879
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12880
|
+
if (typeof org === "number")
|
|
12881
|
+
return org;
|
|
12882
|
+
const reset = hasFlag("--reset");
|
|
12883
|
+
const path = `/orgs/${encodeURIComponent(org.id)}${reset ? "/form-link/reset" : "/form-link"}`;
|
|
12884
|
+
const res = await api2(credentials.apiBase, `/v1/hr${path}`, { method: reset ? "POST" : "GET", headers: authHeaders(credentials) });
|
|
12885
|
+
if (res.status !== 200) {
|
|
12886
|
+
console.error(errorMessage(res.body, "获取入职收集链接失败。"));
|
|
12887
|
+
return exitCodeForApiError(res.body);
|
|
12888
|
+
}
|
|
12889
|
+
const token = res.body.data.token;
|
|
12890
|
+
const link = `${hrWebBase(credentials.apiBase)}/apply/${token}`;
|
|
12891
|
+
if (hasFlag("--json"))
|
|
12892
|
+
console.log(JSON.stringify({ token, link }, null, 2));
|
|
12893
|
+
else {
|
|
12894
|
+
if (reset)
|
|
12895
|
+
console.log("✓ 已重置(散出去的旧二维码全部作废)。");
|
|
12896
|
+
console.log(`入职收集链接:${link}`);
|
|
12897
|
+
console.log("候选人扫码 / 打开提交即自动生成入职申请单(进待拍板),HR 无需先建单。");
|
|
12898
|
+
}
|
|
12899
|
+
return 0;
|
|
12900
|
+
}
|
|
11187
12901
|
switch (command) {
|
|
11188
12902
|
case "init":
|
|
11189
12903
|
process.exit(await init());
|
|
@@ -11253,6 +12967,40 @@ switch (command) {
|
|
|
11253
12967
|
process.exit(await contactsInvitees());
|
|
11254
12968
|
console.error(sub ? `未知子命令:contacts ${sub}
|
|
11255
12969
|
` : `缺少子命令:contacts list | add | rm | invite | invitees
|
|
12970
|
+
`);
|
|
12971
|
+
console.log(usage);
|
|
12972
|
+
process.exit(EXIT_USAGE);
|
|
12973
|
+
}
|
|
12974
|
+
case "addresses": {
|
|
12975
|
+
const sub = args[1];
|
|
12976
|
+
if (sub === "list")
|
|
12977
|
+
process.exit(await addressesList());
|
|
12978
|
+
if (sub === "add")
|
|
12979
|
+
process.exit(await addressesAdd());
|
|
12980
|
+
if (sub === "update")
|
|
12981
|
+
process.exit(await addressesUpdate(args[2]));
|
|
12982
|
+
if (sub === "set-default")
|
|
12983
|
+
process.exit(await addressesSetDefault(args[2]));
|
|
12984
|
+
if (sub === "rm")
|
|
12985
|
+
process.exit(await addressesRm(args[2]));
|
|
12986
|
+
console.error(sub ? `未知子命令:addresses ${sub}
|
|
12987
|
+
` : `缺少子命令:addresses list | add | update | set-default | rm
|
|
12988
|
+
`);
|
|
12989
|
+
console.log(usage);
|
|
12990
|
+
process.exit(EXIT_USAGE);
|
|
12991
|
+
}
|
|
12992
|
+
case "favorites": {
|
|
12993
|
+
const sub = args[1];
|
|
12994
|
+
if (sub === "list")
|
|
12995
|
+
process.exit(await favoritesList());
|
|
12996
|
+
if (sub === "add")
|
|
12997
|
+
process.exit(await favoritesAdd(args[2]));
|
|
12998
|
+
if (sub === "check")
|
|
12999
|
+
process.exit(await favoritesCheck(args[2]));
|
|
13000
|
+
if (sub === "rm")
|
|
13001
|
+
process.exit(await favoritesRm(args[2]));
|
|
13002
|
+
console.error(sub ? `未知子命令:favorites ${sub}
|
|
13003
|
+
` : `缺少子命令:favorites list | add | check | rm
|
|
11256
13004
|
`);
|
|
11257
13005
|
console.log(usage);
|
|
11258
13006
|
process.exit(EXIT_USAGE);
|
|
@@ -11334,6 +13082,26 @@ switch (command) {
|
|
|
11334
13082
|
`);
|
|
11335
13083
|
process.exit(EXIT_USAGE);
|
|
11336
13084
|
}
|
|
13085
|
+
if (sub === "products")
|
|
13086
|
+
process.exit(await shopProductsList());
|
|
13087
|
+
if (sub === "product") {
|
|
13088
|
+
const op = args[2];
|
|
13089
|
+
if (op === "create")
|
|
13090
|
+
process.exit(await shopProductCreate());
|
|
13091
|
+
if (op === "update")
|
|
13092
|
+
process.exit(await shopProductUpdate());
|
|
13093
|
+
if (op === "spec")
|
|
13094
|
+
process.exit(await shopProductSpec());
|
|
13095
|
+
if (op === "archive")
|
|
13096
|
+
process.exit(await shopProductArchive());
|
|
13097
|
+
process.exit(await shopProductDetail(op));
|
|
13098
|
+
}
|
|
13099
|
+
if (sub === "stock")
|
|
13100
|
+
process.exit(await shopStockAdjust(args[2]));
|
|
13101
|
+
if (sub === "listed")
|
|
13102
|
+
process.exit(await shopListedStock());
|
|
13103
|
+
if (sub === "movements")
|
|
13104
|
+
process.exit(await shopMovements());
|
|
11337
13105
|
if (sub === "categories")
|
|
11338
13106
|
process.exit(await shopCategories());
|
|
11339
13107
|
if (sub === "category") {
|
|
@@ -11346,19 +13114,6 @@ switch (command) {
|
|
|
11346
13114
|
if (args[2] === "delete")
|
|
11347
13115
|
process.exit(await shopCategoryDelete());
|
|
11348
13116
|
console.error(`用法:crab shop category add|rename|move|delete …(crab --help 看完整用法)
|
|
11349
|
-
`);
|
|
11350
|
-
process.exit(EXIT_USAGE);
|
|
11351
|
-
}
|
|
11352
|
-
if (sub === "specs")
|
|
11353
|
-
process.exit(await shopSpecs());
|
|
11354
|
-
if (sub === "spec") {
|
|
11355
|
-
if (args[2] === "add")
|
|
11356
|
-
process.exit(await shopSpecAdd());
|
|
11357
|
-
if (args[2] === "update")
|
|
11358
|
-
process.exit(await shopSpecUpdate());
|
|
11359
|
-
if (args[2] === "delete")
|
|
11360
|
-
process.exit(await shopSpecDelete());
|
|
11361
|
-
console.error(`用法:crab shop spec add|update|delete …(crab --help 看完整用法)
|
|
11362
13117
|
`);
|
|
11363
13118
|
process.exit(EXIT_USAGE);
|
|
11364
13119
|
}
|
|
@@ -11389,7 +13144,7 @@ switch (command) {
|
|
|
11389
13144
|
process.exit(await shopApply(positional.join(" "), taglineArg));
|
|
11390
13145
|
}
|
|
11391
13146
|
console.error(sub ? `未知子命令:shop ${sub}
|
|
11392
|
-
` : `缺少子命令:shop status |
|
|
13147
|
+
` : `缺少子命令:shop status | apply | products | product | stock | listed | movements | categories | category | members | member | invite | use | roles
|
|
11393
13148
|
`);
|
|
11394
13149
|
console.log(usage);
|
|
11395
13150
|
process.exit(EXIT_USAGE);
|
|
@@ -11451,9 +13206,13 @@ switch (command) {
|
|
|
11451
13206
|
if (sub === "orgs")
|
|
11452
13207
|
process.exit(await hrOrgsList());
|
|
11453
13208
|
if (sub === "org") {
|
|
11454
|
-
if (args[2] === "
|
|
11455
|
-
process.exit(await
|
|
11456
|
-
|
|
13209
|
+
if (args[2] === "apply")
|
|
13210
|
+
process.exit(await hrOrgApply(positionalArgs(args.slice(3), HR_VALUED_FLAGS).join(" ") || undefined));
|
|
13211
|
+
if (args[2] === "application")
|
|
13212
|
+
process.exit(await hrOrgApplication());
|
|
13213
|
+
if (args[2] === "rename")
|
|
13214
|
+
process.exit(await hrOrgRename());
|
|
13215
|
+
console.error(`用法:crab hr org apply <名称> [--note 说明] | org application | org rename <新名称>
|
|
11457
13216
|
`);
|
|
11458
13217
|
process.exit(EXIT_USAGE);
|
|
11459
13218
|
}
|
|
@@ -11475,6 +13234,8 @@ switch (command) {
|
|
|
11475
13234
|
process.exit(await hrInvite());
|
|
11476
13235
|
if (sub === "people")
|
|
11477
13236
|
process.exit(await hrPeopleList());
|
|
13237
|
+
if (sub === "stats")
|
|
13238
|
+
process.exit(await hrStats());
|
|
11478
13239
|
if (sub === "person") {
|
|
11479
13240
|
const op = args[2];
|
|
11480
13241
|
if (op === "create")
|
|
@@ -11483,6 +13244,8 @@ switch (command) {
|
|
|
11483
13244
|
process.exit(await hrPersonEdit());
|
|
11484
13245
|
if (op === "offboard")
|
|
11485
13246
|
process.exit(await hrPersonOffboard());
|
|
13247
|
+
if (op === "rm")
|
|
13248
|
+
process.exit(await hrPersonRm());
|
|
11486
13249
|
process.exit(await hrPersonDetail(op));
|
|
11487
13250
|
}
|
|
11488
13251
|
if (sub === "event") {
|
|
@@ -11496,6 +13259,17 @@ switch (command) {
|
|
|
11496
13259
|
process.exit(await hrEventsList());
|
|
11497
13260
|
if (sub === "import")
|
|
11498
13261
|
process.exit(await hrImport());
|
|
13262
|
+
if (sub === "dicts")
|
|
13263
|
+
process.exit(await hrDicts());
|
|
13264
|
+
if (sub === "dict") {
|
|
13265
|
+
if (args[2] === "add")
|
|
13266
|
+
process.exit(await hrDictAdd());
|
|
13267
|
+
if (args[2] === "rm")
|
|
13268
|
+
process.exit(await hrDictRm());
|
|
13269
|
+
console.error(`用法:crab hr dict add <dept|position> <名称> | dict rm <entryId>
|
|
13270
|
+
`);
|
|
13271
|
+
process.exit(EXIT_USAGE);
|
|
13272
|
+
}
|
|
11499
13273
|
if (sub === "tasks")
|
|
11500
13274
|
process.exit(await hrTasksList());
|
|
11501
13275
|
if (sub === "task") {
|
|
@@ -11504,14 +13278,22 @@ switch (command) {
|
|
|
11504
13278
|
process.exit(await hrTaskCreate());
|
|
11505
13279
|
if (op === "update")
|
|
11506
13280
|
process.exit(await hrTaskUpdate());
|
|
11507
|
-
|
|
13281
|
+
if (op === "link")
|
|
13282
|
+
process.exit(await hrTaskLink());
|
|
13283
|
+
console.error(`用法:crab hr task create|update|link
|
|
11508
13284
|
`);
|
|
11509
13285
|
process.exit(EXIT_USAGE);
|
|
11510
13286
|
}
|
|
13287
|
+
if (sub === "apply-link")
|
|
13288
|
+
process.exit(await hrApplyLink());
|
|
13289
|
+
if (sub === "announce")
|
|
13290
|
+
process.exit(await hrAnnounce());
|
|
13291
|
+
if (sub === "inbox")
|
|
13292
|
+
process.exit(await hrInbox());
|
|
11511
13293
|
if (sub === "desk")
|
|
11512
13294
|
process.exit(await hrDesk());
|
|
11513
13295
|
console.error(sub ? `未知子命令:hr ${sub}
|
|
11514
|
-
` : `缺少子命令:hr orgs | org
|
|
13296
|
+
` : `缺少子命令:hr orgs | org apply | members | member | invite | people | stats | person | event add | events | import | dicts | dict | tasks | task | apply-link | announce | inbox | desk
|
|
11515
13297
|
`);
|
|
11516
13298
|
console.log(usage);
|
|
11517
13299
|
process.exit(EXIT_USAGE);
|