@crabcloud/cli 0.3.1 → 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 +4014 -69
- package/dist/skills/crabcloud/SKILL.md +16 -5
- package/dist/skills/crabcloud-hr/SKILL.md +118 -0
- package/dist/skills/crabcloud-shop/SKILL.md +79 -0
- package/package.json +1 -1
package/dist/crab.js
CHANGED
|
@@ -2988,6 +2988,9 @@ function handlePipeResult(left, next, ctx) {
|
|
|
2988
2988
|
}
|
|
2989
2989
|
return next._zod.run({ value: left.value, issues: left.issues }, ctx);
|
|
2990
2990
|
}
|
|
2991
|
+
var $ZodPreprocess = /* @__PURE__ */ $constructor("$ZodPreprocess", (inst, def) => {
|
|
2992
|
+
$ZodPipe.init(inst, def);
|
|
2993
|
+
});
|
|
2991
2994
|
var $ZodReadonly = /* @__PURE__ */ $constructor("$ZodReadonly", (inst, def) => {
|
|
2992
2995
|
$ZodType.init(inst, def);
|
|
2993
2996
|
defineLazyInternal(inst, "propValues", (zod) => zod.def.innerType._zod.propValues);
|
|
@@ -6025,6 +6028,10 @@ function pipe(in_, out) {
|
|
|
6025
6028
|
out
|
|
6026
6029
|
});
|
|
6027
6030
|
}
|
|
6031
|
+
var ZodPreprocess = /* @__PURE__ */ $constructor("ZodPreprocess", (inst, def) => {
|
|
6032
|
+
ZodPipe.init(inst, def);
|
|
6033
|
+
$ZodPreprocess.init(inst, def);
|
|
6034
|
+
});
|
|
6028
6035
|
var ZodReadonly = /* @__PURE__ */ $constructor("ZodReadonly", (inst, def) => {
|
|
6029
6036
|
$ZodReadonly.init(inst, def);
|
|
6030
6037
|
ZodType.init(inst, def);
|
|
@@ -6048,6 +6055,13 @@ function refine(fn, _params = {}) {
|
|
|
6048
6055
|
function superRefine(fn, params) {
|
|
6049
6056
|
return _superRefine(fn, params);
|
|
6050
6057
|
}
|
|
6058
|
+
function preprocess(fn, schema) {
|
|
6059
|
+
return new ZodPreprocess({
|
|
6060
|
+
type: "pipe",
|
|
6061
|
+
in: transform(fn),
|
|
6062
|
+
out: schema
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
6051
6065
|
// ../../node_modules/.bun/zod@4.6.5/node_modules/zod/v4/classic/coerce.js
|
|
6052
6066
|
var exports_coerce = {};
|
|
6053
6067
|
__export(exports_coerce, {
|
|
@@ -6072,6 +6086,335 @@ function bigint2(params) {
|
|
|
6072
6086
|
function date2(params) {
|
|
6073
6087
|
return _coercedDate(ZodDate, params);
|
|
6074
6088
|
}
|
|
6089
|
+
// ../../packages/contracts/src/shop.ts
|
|
6090
|
+
var SHOP_PERMISSIONS = ["products", "orders", "aftercare", "customers", "marketing", "finance", "team", "settings"];
|
|
6091
|
+
var shopCategoryMoveInput = object({
|
|
6092
|
+
direction: _enum(["up", "down"])
|
|
6093
|
+
});
|
|
6094
|
+
var comboValues = array(string2().min(1).max(60)).min(1).max(24);
|
|
6095
|
+
var shopStoreCreateInput = object({
|
|
6096
|
+
name: string2().min(1).max(60),
|
|
6097
|
+
tagline: string2().max(200).default("")
|
|
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();
|
|
6106
|
+
var shopCategoryCreateInput = object({
|
|
6107
|
+
name: string2().min(1).max(60),
|
|
6108
|
+
parentId: string2().min(1).nullish()
|
|
6109
|
+
});
|
|
6110
|
+
var shopCategoryUpdateInput = object({
|
|
6111
|
+
name: string2().min(1).max(60),
|
|
6112
|
+
parentId: string2().min(1).nullable()
|
|
6113
|
+
}).partial();
|
|
6114
|
+
var shopVariantInput = object({
|
|
6115
|
+
combo: comboValues,
|
|
6116
|
+
priceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6117
|
+
compareAtPriceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6118
|
+
stock: number2().int().min(0).max(1e7).nullish(),
|
|
6119
|
+
enabled: boolean2().default(true),
|
|
6120
|
+
code: string2().max(64).nullish()
|
|
6121
|
+
});
|
|
6122
|
+
var shopProductCreateInput = object({
|
|
6123
|
+
kind: _enum(["digital", "physical"]),
|
|
6124
|
+
variantMode: _enum(["single", "multi"]),
|
|
6125
|
+
name: string2().min(1).max(140),
|
|
6126
|
+
description: string2().max(2000).default(""),
|
|
6127
|
+
content: string2().max(30000).default(""),
|
|
6128
|
+
categoryId: string2().min(1).nullish(),
|
|
6129
|
+
status: _enum(["draft", "active", "archived"]).default("draft"),
|
|
6130
|
+
priceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6131
|
+
compareAtPriceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6132
|
+
stock: number2().int().min(0).max(1e7).nullish(),
|
|
6133
|
+
refundable: boolean2().nullish(),
|
|
6134
|
+
deliveryType: _enum(["file", "license"]).nullish(),
|
|
6135
|
+
deliveryFileId: string2().min(1).nullish(),
|
|
6136
|
+
imageObjectId: string2().min(1).nullish(),
|
|
6137
|
+
gallery: array(string2().min(1)).max(8).nullish(),
|
|
6138
|
+
specAxes: array(object({ name: string2().min(1).max(30), values: comboValues })).max(3).nullish(),
|
|
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)
|
|
6152
|
+
});
|
|
6153
|
+
var shopStockAdjustInput = object({
|
|
6154
|
+
mode: _enum(["in", "out", "set"]),
|
|
6155
|
+
reason: _enum(["purchase", "manual"]).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)
|
|
6171
|
+
});
|
|
6172
|
+
var shopStripeConnectInput = object({
|
|
6173
|
+
secretKey: string2().min(1).max(255).regex(/^(sk|rk)_(test|live)_/, "must be a Stripe secret key (sk_…/rk_…)"),
|
|
6174
|
+
webhookSecret: string2().max(255).regex(/^whsec_/, "must be a Stripe webhook signing secret (whsec_…)").optional()
|
|
6175
|
+
});
|
|
6176
|
+
var shopDiscountCodeCreateInput = object({
|
|
6177
|
+
code: string2().trim().regex(/^[A-Z0-9]{0,20}$/, "code must be uppercase letters/digits").optional(),
|
|
6178
|
+
kind: _enum(["amount_off", "percent_off"]),
|
|
6179
|
+
amountCents: number2().int().min(1).max(1e8).nullish(),
|
|
6180
|
+
percent: number2().int().min(1).max(99).nullish(),
|
|
6181
|
+
minSpendCents: number2().int().min(0).max(1e8).nullish(),
|
|
6182
|
+
maxUses: number2().int().min(1).max(1e6).nullish(),
|
|
6183
|
+
expiresAt: number2().int().min(0).nullish()
|
|
6184
|
+
});
|
|
6185
|
+
var shopDiscountCodeToggleInput = object({
|
|
6186
|
+
status: _enum(["active", "disabled"])
|
|
6187
|
+
});
|
|
6188
|
+
var shopCustomerAssignInput = object({
|
|
6189
|
+
memberId: string2().min(1).nullable()
|
|
6190
|
+
});
|
|
6191
|
+
var shopCustomerNoteInput = object({
|
|
6192
|
+
note: string2().max(2000)
|
|
6193
|
+
});
|
|
6194
|
+
var shopInviteResetInput = object({
|
|
6195
|
+
permissions: array(_enum(SHOP_PERMISSIONS)).default(["orders", "customers"]),
|
|
6196
|
+
bizRoles: array(_enum(["sales", "support"])).default([])
|
|
6197
|
+
});
|
|
6198
|
+
var shopMemberUpdateInput = object({
|
|
6199
|
+
permissions: array(_enum(SHOP_PERMISSIONS)).min(0).max(SHOP_PERMISSIONS.length),
|
|
6200
|
+
bizRoles: array(_enum(["sales", "support"])).nullish()
|
|
6201
|
+
});
|
|
6202
|
+
var shopStoreRoleCreateInput = object({
|
|
6203
|
+
name: string2().trim().min(1).max(30),
|
|
6204
|
+
permissions: array(_enum(SHOP_PERMISSIONS)).max(SHOP_PERMISSIONS.length)
|
|
6205
|
+
});
|
|
6206
|
+
var shopStoreRoleUpdateInput = object({
|
|
6207
|
+
name: string2().trim().min(1).max(30).optional(),
|
|
6208
|
+
permissions: array(_enum(SHOP_PERMISSIONS)).max(SHOP_PERMISSIONS.length).optional()
|
|
6209
|
+
});
|
|
6210
|
+
var shopShippingAddressInput = object({
|
|
6211
|
+
name: string2().min(1).max(80),
|
|
6212
|
+
phone: string2().min(1).max(40),
|
|
6213
|
+
region: string2().max(120).optional(),
|
|
6214
|
+
detail: string2().min(1).max(500)
|
|
6215
|
+
});
|
|
6216
|
+
var shopCheckoutInput = object({
|
|
6217
|
+
productId: string2().min(1),
|
|
6218
|
+
variantId: string2().min(1).nullish(),
|
|
6219
|
+
quantity: number2().int().min(1).max(99).default(1),
|
|
6220
|
+
buyerEmail: string2().email().max(200),
|
|
6221
|
+
shippingAddress: shopShippingAddressInput.nullish(),
|
|
6222
|
+
discountCode: string2().trim().max(20).nullish(),
|
|
6223
|
+
provider: _enum(["auto", "mock", "stripe"]).default("auto")
|
|
6224
|
+
});
|
|
6225
|
+
var shopShipInput = object({
|
|
6226
|
+
trackingNumber: string2().min(1).max(120),
|
|
6227
|
+
quantity: number2().int().min(1).max(1e7).optional()
|
|
6228
|
+
});
|
|
6229
|
+
var shopApplicationCreateInput = object({
|
|
6230
|
+
name: string2().trim().min(1).max(60),
|
|
6231
|
+
tagline: string2().trim().max(200).default("")
|
|
6232
|
+
});
|
|
6233
|
+
// ../../packages/contracts/src/hr.ts
|
|
6234
|
+
var hrPermissionFlags = [
|
|
6235
|
+
"org.manage",
|
|
6236
|
+
"people.write",
|
|
6237
|
+
"tasks.write",
|
|
6238
|
+
"sensitive.read"
|
|
6239
|
+
];
|
|
6240
|
+
var hrEventTypes = [
|
|
6241
|
+
"onboard",
|
|
6242
|
+
"contract",
|
|
6243
|
+
"change",
|
|
6244
|
+
"attendance",
|
|
6245
|
+
"payroll",
|
|
6246
|
+
"award",
|
|
6247
|
+
"asset",
|
|
6248
|
+
"offboard",
|
|
6249
|
+
"note"
|
|
6250
|
+
];
|
|
6251
|
+
var hrTaskStatuses = ["alert", "confirm", "queued", "done"];
|
|
6252
|
+
var hrTaskDecisions = ["approved", "rejected"];
|
|
6253
|
+
var optionalDay = number2().int().positive().nullable().optional();
|
|
6254
|
+
var hrOrgUpdateInput = object({
|
|
6255
|
+
name: string2().trim().min(1).max(80)
|
|
6256
|
+
});
|
|
6257
|
+
var hrMemberAddInput = object({
|
|
6258
|
+
username: string2().trim().toLowerCase().min(3).max(32),
|
|
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("")
|
|
6268
|
+
});
|
|
6269
|
+
var hrMemberUpdateInput = object({
|
|
6270
|
+
role: _enum(["admin", "member"]).optional(),
|
|
6271
|
+
permissions: record(_enum(hrPermissionFlags), boolean2()).optional(),
|
|
6272
|
+
title: string2().trim().max(40).optional()
|
|
6273
|
+
});
|
|
6274
|
+
var hrPersonCreateInput = object({
|
|
6275
|
+
name: string2().trim().min(1).max(60),
|
|
6276
|
+
employeeNo: string2().trim().max(40).optional(),
|
|
6277
|
+
dept: string2().trim().max(40).optional(),
|
|
6278
|
+
role: string2().trim().max(40).optional(),
|
|
6279
|
+
status: _enum(["active", "probation"]).optional(),
|
|
6280
|
+
hiredAt: number2().int().positive(),
|
|
6281
|
+
probationEnd: optionalDay,
|
|
6282
|
+
contractType: string2().trim().max(20).optional(),
|
|
6283
|
+
contractStart: optionalDay,
|
|
6284
|
+
contractEnd: optionalDay,
|
|
6285
|
+
mobile: string2().trim().max(20).optional(),
|
|
6286
|
+
email: string2().trim().max(120).optional(),
|
|
6287
|
+
profile: record(string2(), unknown()).optional(),
|
|
6288
|
+
idNumber: string2().trim().max(18).optional(),
|
|
6289
|
+
salaryCents: number2().int().min(0).max(1e7).nullable().optional(),
|
|
6290
|
+
notes: string2().trim().max(2000).optional()
|
|
6291
|
+
});
|
|
6292
|
+
var hrImportRowInput = hrPersonCreateInput.omit({ status: true });
|
|
6293
|
+
var hrImportInput = object({
|
|
6294
|
+
rows: array(hrImportRowInput).min(1).max(200)
|
|
6295
|
+
});
|
|
6296
|
+
var hrPersonUpdateInput = object({
|
|
6297
|
+
name: string2().trim().min(1).max(60).optional(),
|
|
6298
|
+
employeeNo: string2().trim().max(40).optional(),
|
|
6299
|
+
dept: string2().trim().max(40).optional(),
|
|
6300
|
+
role: string2().trim().max(40).optional(),
|
|
6301
|
+
status: _enum(["active", "probation", "resigned"]).optional(),
|
|
6302
|
+
hiredAt: number2().int().positive().optional(),
|
|
6303
|
+
probationEnd: optionalDay,
|
|
6304
|
+
contractType: string2().trim().max(20).optional(),
|
|
6305
|
+
contractStart: optionalDay,
|
|
6306
|
+
contractEnd: optionalDay,
|
|
6307
|
+
offboardedAt: optionalDay,
|
|
6308
|
+
mobile: string2().trim().max(20).optional(),
|
|
6309
|
+
email: string2().trim().max(120).optional(),
|
|
6310
|
+
profile: record(string2(), unknown()).optional(),
|
|
6311
|
+
idNumber: string2().trim().max(18).nullable().optional(),
|
|
6312
|
+
salaryCents: number2().int().min(0).max(1e7).nullable().optional(),
|
|
6313
|
+
notes: string2().trim().max(2000).optional()
|
|
6314
|
+
});
|
|
6315
|
+
var hrPersonListQuery = object({
|
|
6316
|
+
q: string2().trim().max(100).optional(),
|
|
6317
|
+
status: _enum(["active", "probation", "resigned"]).optional(),
|
|
6318
|
+
dept: string2().trim().max(40).optional(),
|
|
6319
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6320
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6321
|
+
});
|
|
6322
|
+
var hrEventCreateInput = object({
|
|
6323
|
+
type: _enum(hrEventTypes),
|
|
6324
|
+
note: string2().trim().min(1).max(2000),
|
|
6325
|
+
at: number2().int().positive().optional()
|
|
6326
|
+
});
|
|
6327
|
+
var hrEventListQuery = object({
|
|
6328
|
+
personId: string2().trim().min(1).max(64).optional(),
|
|
6329
|
+
type: _enum(hrEventTypes).optional(),
|
|
6330
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6331
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6332
|
+
});
|
|
6333
|
+
var hrTaskCreateInput = object({
|
|
6334
|
+
kind: string2().trim().min(1).max(40),
|
|
6335
|
+
title: string2().trim().min(1).max(120),
|
|
6336
|
+
node: string2().trim().max(200).optional(),
|
|
6337
|
+
detail: string2().trim().max(2000).optional(),
|
|
6338
|
+
status: _enum(hrTaskStatuses).optional(),
|
|
6339
|
+
dueAt: number2().int().positive().nullable().optional(),
|
|
6340
|
+
assigneeAccountId: string2().trim().max(64).nullable().optional(),
|
|
6341
|
+
source: string2().trim().max(20).optional(),
|
|
6342
|
+
refType: string2().trim().max(20).optional(),
|
|
6343
|
+
refId: string2().trim().max(64).optional(),
|
|
6344
|
+
meta: record(string2(), unknown()).nullable().optional()
|
|
6345
|
+
});
|
|
6346
|
+
var hrTaskUpdateInput = object({
|
|
6347
|
+
status: _enum(hrTaskStatuses).optional(),
|
|
6348
|
+
node: string2().trim().max(200).optional(),
|
|
6349
|
+
detail: string2().trim().max(2000).optional(),
|
|
6350
|
+
dueAt: number2().int().positive().nullable().optional(),
|
|
6351
|
+
assigneeAccountId: string2().trim().max(64).nullable().optional(),
|
|
6352
|
+
decision: _enum(hrTaskDecisions).optional(),
|
|
6353
|
+
decisionNote: string2().trim().max(500).optional(),
|
|
6354
|
+
meta: record(string2(), unknown()).nullable().optional()
|
|
6355
|
+
});
|
|
6356
|
+
var hrTaskListQuery = object({
|
|
6357
|
+
status: _enum(hrTaskStatuses).optional(),
|
|
6358
|
+
kind: string2().trim().max(40).optional(),
|
|
6359
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6360
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6361
|
+
});
|
|
6362
|
+
var hrAnnouncementCreateInput = object({
|
|
6363
|
+
title: string2().trim().min(1).max(120),
|
|
6364
|
+
body: string2().trim().min(1).max(4000)
|
|
6365
|
+
});
|
|
6366
|
+
var hrAnnouncementListQuery = object({
|
|
6367
|
+
includeWithdrawn: _enum(["1", "0"]).optional(),
|
|
6368
|
+
limit: exports_coerce.number().int().min(1).max(100).optional(),
|
|
6369
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6370
|
+
});
|
|
6371
|
+
var hrDictTypes = ["dept", "position"];
|
|
6372
|
+
var hrDictCreateInput = object({
|
|
6373
|
+
type: _enum(hrDictTypes),
|
|
6374
|
+
name: string2().trim().min(1).max(40)
|
|
6375
|
+
});
|
|
6376
|
+
var hrAttachmentListQuery = object({
|
|
6377
|
+
personId: string2().trim().max(64).optional(),
|
|
6378
|
+
taskId: string2().trim().max(64).optional(),
|
|
6379
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6380
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6381
|
+
});
|
|
6382
|
+
var optionalDayField = number2().int().positive().nullable().optional();
|
|
6383
|
+
var hrOnboardFormSchema = object({
|
|
6384
|
+
name: string2().trim().max(60).optional(),
|
|
6385
|
+
gender: string2().trim().max(20).optional(),
|
|
6386
|
+
birthDate: optionalDayField,
|
|
6387
|
+
mobile: string2().trim().max(20).optional(),
|
|
6388
|
+
email: string2().trim().max(120).optional(),
|
|
6389
|
+
idNumber: string2().trim().max(18).optional(),
|
|
6390
|
+
education: string2().trim().max(40).optional(),
|
|
6391
|
+
address: string2().trim().max(120).optional(),
|
|
6392
|
+
emergencyContact: string2().trim().max(120).optional(),
|
|
6393
|
+
notes: string2().trim().max(2000).optional(),
|
|
6394
|
+
employeeNo: string2().trim().max(40).optional(),
|
|
6395
|
+
dept: string2().trim().max(40).optional(),
|
|
6396
|
+
role: string2().trim().max(40).optional(),
|
|
6397
|
+
contractType: string2().trim().max(20).optional(),
|
|
6398
|
+
contractStart: optionalDayField,
|
|
6399
|
+
contractEnd: optionalDayField,
|
|
6400
|
+
employmentType: _enum(["probation", "active"]).optional(),
|
|
6401
|
+
probationEnd: optionalDayField,
|
|
6402
|
+
hiredAt: number2().int().positive().optional(),
|
|
6403
|
+
salaryCents: number2().int().min(0).max(1e7).nullable().optional()
|
|
6404
|
+
});
|
|
6405
|
+
var hrRegularizeFormSchema = object({
|
|
6406
|
+
selfSummary: string2().trim().max(4000).optional(),
|
|
6407
|
+
newRole: string2().trim().max(40).optional(),
|
|
6408
|
+
salaryCents: number2().int().min(0).max(1e7).nullable().optional(),
|
|
6409
|
+
probationReview: string2().trim().max(2000).optional(),
|
|
6410
|
+
regularizeDate: optionalDayField
|
|
6411
|
+
});
|
|
6412
|
+
var hrOffboardFormSchema = object({
|
|
6413
|
+
offboardType: _enum(["personal", "mutual", "dismissal"]).optional(),
|
|
6414
|
+
reason: string2().trim().max(2000).optional(),
|
|
6415
|
+
lastDay: optionalDayField,
|
|
6416
|
+
handoverNotes: string2().trim().max(2000).optional()
|
|
6417
|
+
});
|
|
6075
6418
|
// ../../packages/contracts/src/index.ts
|
|
6076
6419
|
var accountDomain = "crabcloud.cc";
|
|
6077
6420
|
var adminDomain = `admin.${accountDomain}`;
|
|
@@ -6092,9 +6435,16 @@ var scopes = [
|
|
|
6092
6435
|
"storage.delete",
|
|
6093
6436
|
"contacts.read",
|
|
6094
6437
|
"contacts.write",
|
|
6438
|
+
"addresses.read",
|
|
6439
|
+
"addresses.write",
|
|
6440
|
+
"favorites.read",
|
|
6441
|
+
"favorites.write",
|
|
6095
6442
|
"collab.read",
|
|
6096
6443
|
"collab.write",
|
|
6097
|
-
"
|
|
6444
|
+
"hr.read",
|
|
6445
|
+
"hr.write",
|
|
6446
|
+
"agent.read",
|
|
6447
|
+
"agent.write"
|
|
6098
6448
|
];
|
|
6099
6449
|
var scopeGroups = [
|
|
6100
6450
|
{ id: "account", scopes: ["account.read", "credits.read"] },
|
|
@@ -6102,7 +6452,10 @@ var scopeGroups = [
|
|
|
6102
6452
|
{ id: "storage", scopes: ["storage.read", "storage.write", "storage.delete"] },
|
|
6103
6453
|
{ id: "collab", scopes: ["collab.read", "collab.write"] },
|
|
6104
6454
|
{ id: "contacts", scopes: ["contacts.read", "contacts.write"] },
|
|
6105
|
-
{ id: "
|
|
6455
|
+
{ id: "addresses", scopes: ["addresses.read", "addresses.write"] },
|
|
6456
|
+
{ id: "favorites", scopes: ["favorites.read", "favorites.write"] },
|
|
6457
|
+
{ id: "hr", scopes: ["hr.read", "hr.write"] },
|
|
6458
|
+
{ id: "agent", scopes: ["agent.read", "agent.write"] }
|
|
6106
6459
|
];
|
|
6107
6460
|
function expandScopes(input) {
|
|
6108
6461
|
const expanded = input.flatMap((item) => {
|
|
@@ -6184,7 +6537,8 @@ var sessionSummarySchema = object({
|
|
|
6184
6537
|
var deviceStartInput = object({
|
|
6185
6538
|
name: string2().trim().min(1).max(80),
|
|
6186
6539
|
scopes: array(_enum(scopes)).min(1).max(scopes.length),
|
|
6187
|
-
space: string2().trim().min(1).max(80).optional()
|
|
6540
|
+
space: string2().trim().min(1).max(80).optional(),
|
|
6541
|
+
org: string2().trim().min(1).max(80).optional()
|
|
6188
6542
|
});
|
|
6189
6543
|
var devicePollInput = object({ deviceCode: string2().min(16).max(200) });
|
|
6190
6544
|
var deviceDecisionInput = object({
|
|
@@ -6197,11 +6551,33 @@ var agentTokenSummarySchema = object({
|
|
|
6197
6551
|
name: string2(),
|
|
6198
6552
|
scopes: array(_enum(scopes)),
|
|
6199
6553
|
collabSpace: string2().nullable().optional(),
|
|
6554
|
+
hrOrg: string2().nullable().optional(),
|
|
6555
|
+
kind: _enum(["agent", "api", "mobile"]).optional(),
|
|
6556
|
+
keyHint: string2().optional(),
|
|
6200
6557
|
createdAt: number2(),
|
|
6201
6558
|
lastUsedAt: number2().nullable(),
|
|
6202
6559
|
expiresAt: number2().nullable(),
|
|
6203
6560
|
revoked: boolean2()
|
|
6204
6561
|
});
|
|
6562
|
+
var apiKeyCreateInput = object({
|
|
6563
|
+
name: string2().trim().min(1).max(64),
|
|
6564
|
+
scopes: array(string2().trim().min(1).max(40)).min(1).max(32)
|
|
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
|
+
});
|
|
6205
6581
|
var oauthProviders = ["google", "reai", "github"];
|
|
6206
6582
|
var oauthUnbindInput = object({ provider: _enum(oauthProviders) });
|
|
6207
6583
|
var oauthOnboardingConfirmInput = object({
|
|
@@ -6248,6 +6624,15 @@ var adminPlanTierUpdateInput = object({
|
|
|
6248
6624
|
quotaJson: record(string2(), unknown()).optional(),
|
|
6249
6625
|
status: _enum(adminPlanTierStatuses).optional()
|
|
6250
6626
|
});
|
|
6627
|
+
var adminShopStoreStatusInput = object({
|
|
6628
|
+
status: _enum(["active", "archived"])
|
|
6629
|
+
});
|
|
6630
|
+
var adminShopApplicationRejectInput = object({
|
|
6631
|
+
note: string2().trim().min(1).max(500)
|
|
6632
|
+
});
|
|
6633
|
+
var adminHrApplicationRejectInput = object({
|
|
6634
|
+
note: string2().trim().min(1).max(500)
|
|
6635
|
+
});
|
|
6251
6636
|
var subscriptionChangeInput = object({
|
|
6252
6637
|
tierId: string2().trim().min(1).max(50)
|
|
6253
6638
|
});
|
|
@@ -6362,6 +6747,27 @@ var contactQueryInput = object({
|
|
|
6362
6747
|
limit: number2().int().min(1).max(50).default(20),
|
|
6363
6748
|
offset: number2().int().min(0).default(0)
|
|
6364
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
|
+
});
|
|
6365
6771
|
var collabTaskStatuses = ["open", "done"];
|
|
6366
6772
|
var collabBoostKinds = ["tasks", "members"];
|
|
6367
6773
|
var COLLAB_RECORD_BODY_MAX = 20000;
|
|
@@ -6410,7 +6816,7 @@ var collabRecordListQuery = object({
|
|
|
6410
6816
|
});
|
|
6411
6817
|
var agentPermissionModes = ["confirm", "auto_edit", "yolo"];
|
|
6412
6818
|
var agentSessionKinds = ["chat", "task"];
|
|
6413
|
-
var agentMemoryTypes = ["preference", "contact", "project"];
|
|
6819
|
+
var agentMemoryTypes = ["preference", "contact", "project", "daily"];
|
|
6414
6820
|
var AGENT_TITLE_MAX = 120;
|
|
6415
6821
|
var AGENT_MEMORY_MAX = 2000;
|
|
6416
6822
|
var AGENT_PERSONA_MAX = 2000;
|
|
@@ -6426,10 +6832,17 @@ var agentToolsConfigSchema = object({
|
|
|
6426
6832
|
contacts: boolean2().optional()
|
|
6427
6833
|
}).optional()
|
|
6428
6834
|
});
|
|
6835
|
+
var agentDelegationModules = ["mail", "storage", "collab", "workspace", "web"];
|
|
6836
|
+
var agentDelegationRefSchema = object({
|
|
6837
|
+
module: _enum(agentDelegationModules),
|
|
6838
|
+
id: string2().min(1).max(200),
|
|
6839
|
+
title: string2().max(300).optional()
|
|
6840
|
+
});
|
|
6429
6841
|
var agentConfigUpdateInput = object({
|
|
6430
6842
|
activeModelId: string2().trim().min(1).max(64).optional(),
|
|
6431
6843
|
persona: string2().max(AGENT_PERSONA_MAX).optional(),
|
|
6432
6844
|
mode: _enum(agentPermissionModes).optional(),
|
|
6845
|
+
planEnabled: boolean2().optional(),
|
|
6433
6846
|
tools: agentToolsConfigSchema.optional()
|
|
6434
6847
|
});
|
|
6435
6848
|
var AGENT_PROVIDER_LABEL_MAX = 60;
|
|
@@ -6446,6 +6859,11 @@ var agentProviderModelInput = object({
|
|
|
6446
6859
|
inputImage: boolean2().optional(),
|
|
6447
6860
|
inputVideo: boolean2().optional()
|
|
6448
6861
|
});
|
|
6862
|
+
var agentSkillCreateInput = object({
|
|
6863
|
+
name: string2().trim().max(64).optional(),
|
|
6864
|
+
description: string2().trim().max(600).optional(),
|
|
6865
|
+
body: string2().min(1).max(1e5)
|
|
6866
|
+
});
|
|
6449
6867
|
var agentSessionCreateInput = object({
|
|
6450
6868
|
kind: _enum(agentSessionKinds).default("chat"),
|
|
6451
6869
|
title: string2().trim().max(AGENT_TITLE_MAX).optional(),
|
|
@@ -6454,10 +6872,35 @@ var agentSessionCreateInput = object({
|
|
|
6454
6872
|
}).refine((v) => v.kind === "chat" || !!v.title && v.title.length > 0, {
|
|
6455
6873
|
message: "task requires title"
|
|
6456
6874
|
});
|
|
6457
|
-
var agentSessionRenameInput = object({
|
|
6875
|
+
var agentSessionRenameInput = object({
|
|
6876
|
+
title: string2().trim().min(1).max(AGENT_TITLE_MAX).optional(),
|
|
6877
|
+
mode: _enum(agentPermissionModes).optional()
|
|
6878
|
+
}).refine((v) => v.title !== undefined || v.mode !== undefined, { message: "nothing to update" });
|
|
6879
|
+
var agentUsageOverviewSchema = object({
|
|
6880
|
+
monthActions: number2().int().min(0),
|
|
6881
|
+
turns30d: number2().int().min(0),
|
|
6882
|
+
daily: array(object({ day: string2().regex(/^\d{4}-\d{2}-\d{2}$/), count: number2().int().min(0) })).max(120),
|
|
6883
|
+
toolCalls: array(object({ action: string2().min(1), count: number2().int().min(0) })).max(40),
|
|
6884
|
+
approvals: object({
|
|
6885
|
+
approved: number2().int().min(0),
|
|
6886
|
+
rejected: number2().int().min(0),
|
|
6887
|
+
pending: number2().int().min(0),
|
|
6888
|
+
expired: number2().int().min(0)
|
|
6889
|
+
}),
|
|
6890
|
+
taskRuns: object({ total: number2().int().min(0), failed: number2().int().min(0) })
|
|
6891
|
+
});
|
|
6458
6892
|
var agentSessionListQuery = object({
|
|
6459
6893
|
kind: _enum(agentSessionKinds).optional(),
|
|
6460
|
-
limit: exports_coerce.number().int().min(1).max(200).optional()
|
|
6894
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6895
|
+
archived: preprocess((v) => v === "1" || v === "true", boolean2()).optional(),
|
|
6896
|
+
q: string2().trim().min(1).max(100).optional()
|
|
6897
|
+
});
|
|
6898
|
+
var agentTaskCreateInput = object({
|
|
6899
|
+
title: string2().trim().min(1).max(AGENT_TITLE_MAX),
|
|
6900
|
+
repeatCron: string2().trim().max(100).optional(),
|
|
6901
|
+
notifyMail: boolean2().optional(),
|
|
6902
|
+
mode: _enum(["task", "branch"]).optional(),
|
|
6903
|
+
seedMessageIds: array(string2().min(1).max(80)).max(20).optional()
|
|
6461
6904
|
});
|
|
6462
6905
|
var agentMemoryCreateInput = object({
|
|
6463
6906
|
type: _enum(agentMemoryTypes),
|
|
@@ -6815,6 +7258,9 @@ function exitCodeForApiError(body) {
|
|
|
6815
7258
|
case "collab_space_not_bound":
|
|
6816
7259
|
case "collab_space_mismatch":
|
|
6817
7260
|
return EXIT_SCOPE;
|
|
7261
|
+
case "hr_org_not_bound":
|
|
7262
|
+
case "hr_org_mismatch":
|
|
7263
|
+
return EXIT_SCOPE;
|
|
6818
7264
|
default:
|
|
6819
7265
|
return 1;
|
|
6820
7266
|
}
|
|
@@ -6894,6 +7340,22 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6894
7340
|
contacts invite [--reset] [--json] 我的邀请码、链接与统计(--reset 重置,旧链接失效)
|
|
6895
7341
|
contacts invitees [--offset N] [--json]
|
|
6896
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
|
+
取消收藏(幂等,不存在也成功)
|
|
6897
7359
|
storage ls [--q 关键字] [--source files|media|mail|collab|agent] [--category <id|none>]
|
|
6898
7360
|
[--type doc|image|video|archive|code] [--tag a,b] [--trash]
|
|
6899
7361
|
[--shared] [--recent] [--encrypted only|plain] [--message-id <id>]
|
|
@@ -6955,6 +7417,116 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6955
7417
|
collab activity [--json] 协作域活动事件(Agent 推送/读取、任务创建、摘要更新)
|
|
6956
7418
|
post <taskId> (--body "..." | --body-file <path>) [--attach <fileId,...>] [--link <url,...>] [--json]
|
|
6957
7419
|
推送任务记录(糖命令:等价 collab task records 的写入)
|
|
7420
|
+
hr orgs [--json] 我的组织列表(hr 命令需令牌绑定组织:login --scopes hr --org 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)
|
|
7426
|
+
hr members [--org <arg>] [--json] 成员列表(角色与权限 flags)
|
|
7427
|
+
hr member add <用户名> [--title 头衔] [--permissions org.manage=1,...] [--org <arg>]
|
|
7428
|
+
邀请平台账号入组织(默认只读,--permissions 定生效权限)
|
|
7429
|
+
hr member set <memberId> [--role admin|member] [--permissions org.manage=1,...] [--title]
|
|
7430
|
+
角色与权限逐项配置(owner 恒全权不可改)
|
|
7431
|
+
hr member rm <memberId> 移除成员
|
|
7432
|
+
hr invite [--permissions org.manage=1,...] [--reset] [--org <arg>]
|
|
7433
|
+
常驻邀请链接(/join 加入页;--permissions 改加入后默认权限,
|
|
7434
|
+
未指定项视为关;--reset 作废旧链,配置保留)
|
|
7435
|
+
hr people [--q 关键字] [--status active|probation|resigned] [--dept] [--limit N] [--offset N] [--json]
|
|
7436
|
+
花名册(身份证/薪资对令牌永不外泄,输出如实按无权限转述)
|
|
7437
|
+
hr stats [--org <arg>] [--json] 花名册统计带(在职/试用/离职 + 合同与试用期风险计数)
|
|
7438
|
+
hr person <id> [--json] 人员详情
|
|
7439
|
+
hr person create --name "姓名" --hired-at YYYY-MM-DD [--dept] [--role] [--probation-end] [--contract-type] [--contract-end] [--mobile] [--email] [--notes] [--json]
|
|
7440
|
+
建档(自动落入职事件)
|
|
7441
|
+
hr person edit <id> [--dept] [--role] [--status ...] [--contract-end YYYY-MM-DD] [--offboarded-at YYYY-MM-DD] [--json]
|
|
7442
|
+
编辑(转正/离职状态流转自动落痕)
|
|
7443
|
+
hr person offboard <id> [--date YYYY-MM-DD] [--json]
|
|
7444
|
+
办理离职(档案封存,记录依法保留)
|
|
7445
|
+
hr person rm <id> 删除人员(仅零记录档案可删;有事件时间线一律 409)
|
|
7446
|
+
hr event add <personId> --type <onboard|contract|change|attendance|payroll|award|asset|offboard|note> --note "..." [--at YYYY-MM-DD] [--json]
|
|
7447
|
+
事件落痕(时间线即证据链)
|
|
7448
|
+
hr events [--person <id>] [--type] [--limit N] [--offset N] [--json]
|
|
7449
|
+
事件时间线
|
|
7450
|
+
hr import <文件.csv|.tsv|.json> [--org <arg>] [--json]
|
|
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>] 删除字典项
|
|
7456
|
+
hr tasks [--status alert|confirm|queued|done] [--kind] [--limit N] [--offset N] [--json]
|
|
7457
|
+
任务列表
|
|
7458
|
+
hr task create --kind <attendance|payroll|contract|...> --title "..." [--node] [--detail] [--due YYYY-MM-DD] [--json]
|
|
7459
|
+
新建任务
|
|
7460
|
+
hr task update <id> [--status ...] [--node] [--detail] [--due YYYY-MM-DD] [--json]
|
|
7461
|
+
任务流转
|
|
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 首入口:统计 + 提醒 + 任务三段 + 公告 + 动态,一次取全)
|
|
7475
|
+
shop status [--json] 店铺列表 + 开通申请进度(多店全列,★ = 当前店)
|
|
7476
|
+
shop use <slug> 切换当前管理的店铺(持久保存;店级命令缺省作用于它,
|
|
7477
|
+
--store 可单条临时覆盖;不带参数显示当前店)
|
|
7478
|
+
shop apply <店铺名> [--tagline 一句话介绍] [--json]
|
|
7479
|
+
提交开店申请(成为卖家/增开统一入口;平台审核通过自动建店,
|
|
7480
|
+
增开通过时扣积分)
|
|
7481
|
+
shop roles --store <slug> [--json] 店铺角色预设(权限组合模板 + 匹配成员数)
|
|
7482
|
+
shop members --store <slug> [--json]
|
|
7483
|
+
成员列表(身份/权限组合/业务身份/归属客户/状态)
|
|
7484
|
+
shop member edit <用户名> --perms <a,b,c> [--biz sales,support] --store <slug> [--json]
|
|
7485
|
+
编辑成员权限组合与业务身份(--perms 七项逗号分隔,
|
|
7486
|
+
--biz 省略则不改;身份=归因资格,可多选)
|
|
7487
|
+
shop invite [--reset --perms <a,b> --biz <sales,support>] --store <slug> [--json]
|
|
7488
|
+
查看 / 重置常驻邀请链接(重置后旧链接作废,
|
|
7489
|
+
受邀者权限取新默认组合;接受走链接页)
|
|
7490
|
+
shop categories [--store <slug>] [--json]
|
|
7491
|
+
分类树(多级缩进 + 商品数 + id)
|
|
7492
|
+
shop category add <名称> [--parent <id或名称>] --store <slug> [--json]
|
|
7493
|
+
新建分类(缺省顶级;新建落同级末尾)
|
|
7494
|
+
shop category rename <id或名称> <新名称> --store <slug> [--json]
|
|
7495
|
+
shop category move <id或名称> <up|down> --store <slug> [--json]
|
|
7496
|
+
同级内上移/下移一位(边界静默不动)
|
|
7497
|
+
shop category delete <id或名称> --store <slug> [--json]
|
|
7498
|
+
删除分类(有子级拒绝;叶子删除后商品迁入未分类)
|
|
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 按商品过滤)
|
|
6958
7530
|
credits [--json] 积分余额(月送/充值/合计;需 credits.read scope)
|
|
6959
7531
|
credits history [--limit N] [--offset N] [--json]
|
|
6960
7532
|
积分流水(变动 ±:消耗为负;需 credits.read)
|
|
@@ -6998,10 +7570,21 @@ async function login() {
|
|
|
6998
7570
|
console.error("示例:crab login --scopes collab --space default");
|
|
6999
7571
|
return EXIT_USAGE;
|
|
7000
7572
|
}
|
|
7573
|
+
const orgArg = readFlag("--org");
|
|
7574
|
+
if (requested.some((s) => s === "hr.read" || s === "hr.write") && !orgArg) {
|
|
7575
|
+
console.error("含人事行政 scope(hr)时必须指定 --org <org:<名称>|组织id>>——令牌将绑定该组织,越界访问一律拒绝。");
|
|
7576
|
+
console.error("示例:crab login --scopes hr --org org:远帆文化");
|
|
7577
|
+
return EXIT_USAGE;
|
|
7578
|
+
}
|
|
7001
7579
|
const openBrowser = !hasFlag("--no-open");
|
|
7002
7580
|
const start = await api2(apiBase, "/v1/auth/device/start", {
|
|
7003
7581
|
method: "POST",
|
|
7004
|
-
body: JSON.stringify({
|
|
7582
|
+
body: JSON.stringify({
|
|
7583
|
+
name: `${hostname()} · crab cli`,
|
|
7584
|
+
scopes: requested,
|
|
7585
|
+
...spaceArg ? { space: spaceArg } : {},
|
|
7586
|
+
...orgArg ? { org: orgArg } : {}
|
|
7587
|
+
})
|
|
7005
7588
|
});
|
|
7006
7589
|
if (start.status !== 200) {
|
|
7007
7590
|
console.error(`发起授权失败(${start.status}):`, errorMessage(start.body, JSON.stringify(start.body)));
|
|
@@ -7045,6 +7628,8 @@ async function login() {
|
|
|
7045
7628
|
console.log(` scopes: ${poll.body.data.scopes.join(", ")}`);
|
|
7046
7629
|
if (spaceArg)
|
|
7047
7630
|
console.log(` 协作空间绑定: ${spaceArg}(collab 命令只能作用于该空间)`);
|
|
7631
|
+
if (orgArg)
|
|
7632
|
+
console.log(` HR 组织绑定: ${orgArg}(hr 命令只能作用于该组织)`);
|
|
7048
7633
|
return 0;
|
|
7049
7634
|
}
|
|
7050
7635
|
}
|
|
@@ -7187,11 +7772,13 @@ async function tokensList() {
|
|
|
7187
7772
|
return 0;
|
|
7188
7773
|
}
|
|
7189
7774
|
if (rows.length === 0) {
|
|
7190
|
-
console.log("
|
|
7775
|
+
console.log("还没有已授权的令牌。");
|
|
7191
7776
|
return 0;
|
|
7192
7777
|
}
|
|
7193
7778
|
for (const token of rows) {
|
|
7194
|
-
|
|
7779
|
+
const isApiKey = token.kind === "api";
|
|
7780
|
+
const hint = isApiKey && token.keyHint ? ` ⟨${token.keyHint}⟩` : "";
|
|
7781
|
+
console.log(`${token.id} ${isApiKey ? "[API Key] " : ""}${token.name}${hint} [${token.scopes.join(", ")}] ${token.revoked ? "已撤销" : "生效中"}${token.collabSpace ? ` ⟨空间 ${token.collabSpace}⟩` : ""}`);
|
|
7195
7782
|
console.log(` 创建 ${formatTime(token.createdAt)} · 最近使用 ${token.lastUsedAt ? formatTime(token.lastUsedAt) : "—"}${token.expiresAt ? ` · 过期 ${formatTime(token.expiresAt)}` : ""}`);
|
|
7196
7783
|
}
|
|
7197
7784
|
return 0;
|
|
@@ -7690,6 +8277,322 @@ async function contactsInvitees() {
|
|
|
7690
8277
|
console.log(`(还有更多:contacts invitees --offset ${page.nextOffset})`);
|
|
7691
8278
|
return 0;
|
|
7692
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
|
+
}
|
|
7693
8596
|
var STORAGE_SOURCES = ["files", "media", "mail", "collab", "agent"];
|
|
7694
8597
|
var STORAGE_TYPES = ["doc", "image", "video", "archive", "code"];
|
|
7695
8598
|
var MIME_BY_EXT = {
|
|
@@ -9166,75 +10069,2938 @@ async function logout() {
|
|
|
9166
10069
|
console.log("✓ 已撤销 Agent Token 并清除本地凭证。");
|
|
9167
10070
|
return 0;
|
|
9168
10071
|
}
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
10072
|
+
var HR_VALUED_FLAGS = [
|
|
10073
|
+
"--org",
|
|
10074
|
+
"--title",
|
|
10075
|
+
"--role",
|
|
10076
|
+
"--permissions",
|
|
10077
|
+
"--name",
|
|
10078
|
+
"--hired-at",
|
|
10079
|
+
"--employee-no",
|
|
10080
|
+
"--dept",
|
|
10081
|
+
"--role",
|
|
10082
|
+
"--status",
|
|
10083
|
+
"--probation-end",
|
|
10084
|
+
"--contract-type",
|
|
10085
|
+
"--contract-end",
|
|
10086
|
+
"--mobile",
|
|
10087
|
+
"--email",
|
|
10088
|
+
"--notes",
|
|
10089
|
+
"--type",
|
|
10090
|
+
"--note",
|
|
10091
|
+
"--at",
|
|
10092
|
+
"--date",
|
|
10093
|
+
"--kind",
|
|
10094
|
+
"--title",
|
|
10095
|
+
"--node",
|
|
10096
|
+
"--detail",
|
|
10097
|
+
"--due",
|
|
10098
|
+
"--limit",
|
|
10099
|
+
"--offset",
|
|
10100
|
+
"--q",
|
|
10101
|
+
"--body",
|
|
10102
|
+
"--meta"
|
|
10103
|
+
];
|
|
10104
|
+
var HR_PERSON_STATUSES = ["active", "probation", "resigned"];
|
|
10105
|
+
var HR_TASK_STATUSES = ["alert", "confirm", "queued", "done"];
|
|
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"];
|
|
10109
|
+
function hrOrgToken(org) {
|
|
10110
|
+
return `org:${org.name}`;
|
|
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;
|
|
9187
10119
|
}
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
`);
|
|
9198
|
-
|
|
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";
|
|
9199
10133
|
}
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
process.exit(await mailSend());
|
|
9212
|
-
if (sub === "archive" || sub === "trash" || sub === "delete")
|
|
9213
|
-
process.exit(await mailMove(sub, args[2]));
|
|
9214
|
-
if (sub === "folders")
|
|
9215
|
-
process.exit(await mailFoldersOverview());
|
|
9216
|
-
if (sub === "activity")
|
|
9217
|
-
process.exit(await mailActivity());
|
|
9218
|
-
console.error(sub ? `未知子命令:mail ${sub}
|
|
9219
|
-
` : `缺少子命令:mail list | read | unread | search | send | archive | trash | delete | folders | activity
|
|
9220
|
-
`);
|
|
9221
|
-
console.log(usage);
|
|
9222
|
-
process.exit(EXIT_USAGE);
|
|
10134
|
+
return out;
|
|
10135
|
+
}
|
|
10136
|
+
async function resolveHrOrg(credentials, arg) {
|
|
10137
|
+
const res = await api2(credentials.apiBase, "/v1/hr/orgs", { headers: authHeaders(credentials) });
|
|
10138
|
+
if (res.status !== 200) {
|
|
10139
|
+
console.error(errorMessage(res.body, "获取组织列表失败。"));
|
|
10140
|
+
const code = res.body?.error?.code;
|
|
10141
|
+
if (code === "scope_denied") {
|
|
10142
|
+
console.error("提示:hr 命令需要 hr scope——运行 crab login --scopes hr --org org:<名称> 重新授权。");
|
|
10143
|
+
}
|
|
10144
|
+
return exitCodeForApiError(res.body);
|
|
9223
10145
|
}
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
10146
|
+
const orgs = res.body.data.orgs;
|
|
10147
|
+
if (orgs.length === 0) {
|
|
10148
|
+
console.error("你还没有加入任何公司 / 组织(在 hr-web 申请创建,或 crab hr org apply <名称> 提交申请)。");
|
|
10149
|
+
return EXIT_NOT_FOUND;
|
|
10150
|
+
}
|
|
10151
|
+
const raw = (arg ?? "").trim();
|
|
10152
|
+
if (!raw) {
|
|
10153
|
+
if (orgs.length === 1)
|
|
10154
|
+
return orgs[0];
|
|
10155
|
+
console.error("你所在的组织不止一个,请用 --org 指定(org:<名称> / 组织 id):");
|
|
10156
|
+
for (const org of orgs)
|
|
10157
|
+
console.error(` ${hrOrgToken(org)} (成员 ${org.memberCount} · ${org.myRole})`);
|
|
10158
|
+
return EXIT_USAGE;
|
|
10159
|
+
}
|
|
10160
|
+
if (raw.startsWith("org:")) {
|
|
10161
|
+
const name = raw.slice("org:".length).trim();
|
|
10162
|
+
if (!name) {
|
|
10163
|
+
console.error("org: 后面要跟组织名称(crab hr orgs 查看)。");
|
|
10164
|
+
return EXIT_USAGE;
|
|
10165
|
+
}
|
|
10166
|
+
const hit = orgs.filter((o) => o.name === name && o.status === "active");
|
|
10167
|
+
if (hit.length === 1)
|
|
10168
|
+
return hit[0];
|
|
10169
|
+
console.error(hit.length === 0 ? `没有名为「${name}」的 active 组织。` : `「${name}」命中多个组织,请用组织 id 指定。`);
|
|
10170
|
+
return EXIT_NOT_FOUND;
|
|
10171
|
+
}
|
|
10172
|
+
const byId = orgs.find((o) => o.id === raw);
|
|
10173
|
+
if (byId)
|
|
10174
|
+
return byId;
|
|
10175
|
+
const byName = orgs.filter((o) => o.name === raw);
|
|
10176
|
+
if (byName.length === 1)
|
|
10177
|
+
return byName[0];
|
|
10178
|
+
console.error(`组织标识无效:${raw}
|
|
10179
|
+
应为 org:<名称> 或组织 id(crab hr orgs 查看)。`);
|
|
10180
|
+
return EXIT_USAGE;
|
|
10181
|
+
}
|
|
10182
|
+
function parseHrDate(text) {
|
|
10183
|
+
if (text === undefined || text.trim() === "")
|
|
10184
|
+
return;
|
|
10185
|
+
const [y, m, d] = text.trim().split("-").map(Number);
|
|
10186
|
+
if (!y || !m || !d)
|
|
10187
|
+
return null;
|
|
10188
|
+
const ms = new Date(y, m - 1, d).getTime();
|
|
10189
|
+
return Number.isFinite(ms) ? ms : null;
|
|
10190
|
+
}
|
|
10191
|
+
async function resolveStoreArg(credentials) {
|
|
10192
|
+
const arg = readFlag("--store") ?? credentials.shopStore;
|
|
10193
|
+
const res = await api2(credentials.apiBase, "/v1/shop/stores", { headers: authHeaders(credentials) });
|
|
10194
|
+
if (res.status !== 200) {
|
|
10195
|
+
console.error(errorMessage(res.body, "获取店铺列表失败。"));
|
|
10196
|
+
return exitCodeForApiError(res.body);
|
|
10197
|
+
}
|
|
10198
|
+
const stores = res.body.data.stores;
|
|
10199
|
+
const wanted = arg ? stores.find((x) => x.slug === arg || x.id === arg) : undefined;
|
|
10200
|
+
if (arg && !wanted) {
|
|
10201
|
+
console.error(`找不到店铺 ${arg}(crab shop status 查看可用的 slug;crab shop use <slug> 切换当前店)`);
|
|
10202
|
+
return EXIT_NOT_FOUND;
|
|
10203
|
+
}
|
|
10204
|
+
if (wanted)
|
|
10205
|
+
return wanted;
|
|
10206
|
+
if (stores.length === 1)
|
|
10207
|
+
return stores[0];
|
|
10208
|
+
console.error("该账号有多家店铺:先 crab shop use <slug> 设置当前店,或单条命令用 --store <slug> 临时指定");
|
|
10209
|
+
return EXIT_USAGE;
|
|
10210
|
+
}
|
|
10211
|
+
var SHOP_PERMS = ["products", "orders", "aftercare", "customers", "marketing", "finance", "team", "settings"];
|
|
10212
|
+
function shopWebBase(apiBase) {
|
|
10213
|
+
if (/127\.0\.0\.1:14321|localhost:14321/.test(apiBase))
|
|
10214
|
+
return "http://127.0.0.1:14341";
|
|
10215
|
+
try {
|
|
10216
|
+
return `https://shop.${new URL(apiBase).host.replace(/^api\./, "")}`;
|
|
10217
|
+
} catch {
|
|
10218
|
+
return apiBase;
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
function parseCsvPermList(raw) {
|
|
10222
|
+
if (!raw)
|
|
10223
|
+
return null;
|
|
10224
|
+
const items = raw.split(",").map((x) => x.trim()).filter(Boolean);
|
|
10225
|
+
const invalid = items.filter((x) => !SHOP_PERMS.includes(x));
|
|
10226
|
+
if (invalid.length > 0) {
|
|
10227
|
+
console.error(`未知权限项:${invalid.join(", ")}
|
|
10228
|
+
可用:${SHOP_PERMS.join(", ")}`);
|
|
10229
|
+
return null;
|
|
10230
|
+
}
|
|
10231
|
+
return items;
|
|
10232
|
+
}
|
|
10233
|
+
async function shopRoles() {
|
|
10234
|
+
const credentials = requireCredentials();
|
|
10235
|
+
if (typeof credentials === "number")
|
|
10236
|
+
return credentials;
|
|
10237
|
+
const store = await resolveStoreArg(credentials);
|
|
10238
|
+
if (typeof store === "number")
|
|
10239
|
+
return store;
|
|
10240
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/roles`, { headers: authHeaders(credentials) });
|
|
10241
|
+
if (res.status !== 200) {
|
|
10242
|
+
console.error(errorMessage(res.body, "获取角色列表失败。"));
|
|
10243
|
+
return exitCodeForApiError(res.body);
|
|
10244
|
+
}
|
|
10245
|
+
const roles = res.body.data.roles;
|
|
10246
|
+
if (hasFlag("--json")) {
|
|
10247
|
+
console.log(JSON.stringify(roles, null, 2));
|
|
10248
|
+
return 0;
|
|
10249
|
+
}
|
|
10250
|
+
console.log(`「${store.name}」角色预设(模板套用:改模板不影响已套用成员):`);
|
|
10251
|
+
for (const r of roles) {
|
|
10252
|
+
console.log(` ${r.name}${r.builtin ? "(默认)" : ""} [${r.permissions.join(", ") || "无权限"}] 匹配成员 ${r.memberCount}`);
|
|
10253
|
+
}
|
|
10254
|
+
return 0;
|
|
10255
|
+
}
|
|
10256
|
+
async function fetchStoreMembers(credentials, storeId) {
|
|
10257
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${storeId}/members`, { headers: authHeaders(credentials) });
|
|
10258
|
+
if (res.status !== 200) {
|
|
10259
|
+
console.error(errorMessage(res.body, "获取成员列表失败。"));
|
|
10260
|
+
return exitCodeForApiError(res.body);
|
|
10261
|
+
}
|
|
10262
|
+
return res.body.data.members;
|
|
10263
|
+
}
|
|
10264
|
+
async function shopMembers() {
|
|
10265
|
+
const credentials = requireCredentials();
|
|
10266
|
+
if (typeof credentials === "number")
|
|
10267
|
+
return credentials;
|
|
10268
|
+
const store = await resolveStoreArg(credentials);
|
|
10269
|
+
if (typeof store === "number")
|
|
10270
|
+
return store;
|
|
10271
|
+
const members = await fetchStoreMembers(credentials, store.id);
|
|
10272
|
+
if (typeof members === "number")
|
|
10273
|
+
return members;
|
|
10274
|
+
if (hasFlag("--json")) {
|
|
10275
|
+
console.log(JSON.stringify(members, null, 2));
|
|
10276
|
+
return 0;
|
|
10277
|
+
}
|
|
10278
|
+
console.log(`「${store.name}」成员:`);
|
|
10279
|
+
for (const m of members) {
|
|
10280
|
+
const biz = m.bizRoles.length > 0 ? m.bizRoles.join("/") : "无业务身份";
|
|
10281
|
+
console.log(` ${m.displayName || m.username} (@${m.username}) [${m.role === "owner" ? "店主" : m.permissions.join(", ") || "无权限"}] 身份: ${biz} 归属客户: ${m.assignedCustomers} ${m.status}`);
|
|
10282
|
+
}
|
|
10283
|
+
return 0;
|
|
10284
|
+
}
|
|
10285
|
+
async function shopMemberEdit() {
|
|
10286
|
+
const credentials = requireCredentials();
|
|
10287
|
+
if (typeof credentials === "number")
|
|
10288
|
+
return credentials;
|
|
10289
|
+
const positional = args.slice(3).filter((x) => !x.startsWith("--") && args[args.indexOf(x) - 1] !== "--perms" && args[args.indexOf(x) - 1] !== "--biz");
|
|
10290
|
+
const username = positional[0];
|
|
10291
|
+
const permsRaw = readFlag("--perms");
|
|
10292
|
+
const bizRaw = readFlag("--biz");
|
|
10293
|
+
if (!username || !permsRaw) {
|
|
10294
|
+
console.error(`用法:crab shop member edit <用户名> --perms <a,b,c> [--biz sales,support] --store <slug>
|
|
10295
|
+
权限项:products, orders, aftercare, customers, marketing, finance, team, settings`);
|
|
10296
|
+
return EXIT_USAGE;
|
|
10297
|
+
}
|
|
10298
|
+
const permissions = parseCsvPermList(permsRaw);
|
|
10299
|
+
if (permissions === null)
|
|
10300
|
+
return EXIT_USAGE;
|
|
10301
|
+
let bizRoles;
|
|
10302
|
+
if (bizRaw !== undefined) {
|
|
10303
|
+
bizRoles = bizRaw.split(",").map((x) => x.trim()).filter(Boolean);
|
|
10304
|
+
const invalid = bizRoles.filter((x) => x !== "sales" && x !== "support");
|
|
10305
|
+
if (invalid.length > 0) {
|
|
10306
|
+
console.error(`未知业务身份:${invalid.join(", ")}(可选 sales / support,可逗号分隔多选)`);
|
|
10307
|
+
return EXIT_USAGE;
|
|
10308
|
+
}
|
|
10309
|
+
}
|
|
10310
|
+
const store = await resolveStoreArg(credentials);
|
|
10311
|
+
if (typeof store === "number")
|
|
10312
|
+
return store;
|
|
10313
|
+
const members = await fetchStoreMembers(credentials, store.id);
|
|
10314
|
+
if (typeof members === "number")
|
|
10315
|
+
return members;
|
|
10316
|
+
const member = members.find((m) => m.username === username);
|
|
10317
|
+
if (!member) {
|
|
10318
|
+
console.error(`成员不存在:@${username}(crab shop members 查看)`);
|
|
10319
|
+
return EXIT_NOT_FOUND;
|
|
10320
|
+
}
|
|
10321
|
+
if (member.role === "owner") {
|
|
10322
|
+
console.error("店主由店铺归属决定,权限不可修改。");
|
|
10323
|
+
return 1;
|
|
10324
|
+
}
|
|
10325
|
+
const res = await api2(credentials.apiBase, `/v1/shop/members/${member.id}`, {
|
|
10326
|
+
method: "PATCH",
|
|
10327
|
+
headers: authHeaders(credentials),
|
|
10328
|
+
body: JSON.stringify({ permissions, ...bizRoles !== undefined ? { bizRoles } : {} })
|
|
10329
|
+
});
|
|
10330
|
+
if (res.status !== 200) {
|
|
10331
|
+
console.error(errorMessage(res.body, "更新成员权限失败。"));
|
|
10332
|
+
return exitCodeForApiError(res.body);
|
|
10333
|
+
}
|
|
10334
|
+
const updated = res.body.data.member;
|
|
10335
|
+
if (hasFlag("--json")) {
|
|
10336
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
10337
|
+
return 0;
|
|
10338
|
+
}
|
|
10339
|
+
console.log(`✓ @${username} 权限已更新:[${permissions.join(", ")}]${bizRoles !== undefined ? ` · 业务身份:${bizRoles.length > 0 ? bizRoles.join("/") : "无"}` : `(业务身份保持:${member.bizRoles.join("/") || "无"})`}`);
|
|
10340
|
+
return 0;
|
|
10341
|
+
}
|
|
10342
|
+
async function shopInvite() {
|
|
10343
|
+
const credentials = requireCredentials();
|
|
10344
|
+
if (typeof credentials === "number")
|
|
10345
|
+
return credentials;
|
|
10346
|
+
const store = await resolveStoreArg(credentials);
|
|
10347
|
+
if (typeof store === "number")
|
|
10348
|
+
return store;
|
|
10349
|
+
const reset = hasFlag("--reset");
|
|
10350
|
+
const permsRaw = readFlag("--perms");
|
|
10351
|
+
const bizRaw = readFlag("--biz");
|
|
10352
|
+
let body = {};
|
|
10353
|
+
if (reset) {
|
|
10354
|
+
const permissions = permsRaw ? parseCsvPermList(permsRaw) : null;
|
|
10355
|
+
if (permsRaw && permissions === null)
|
|
10356
|
+
return EXIT_USAGE;
|
|
10357
|
+
body = { permissions: permissions ?? ["orders", "customers"], bizRoles: bizRaw ? bizRaw.split(",").map((x) => x.trim()).filter(Boolean) : [] };
|
|
10358
|
+
}
|
|
10359
|
+
const path = reset ? `/v1/shop/stores/${store.id}/invite/reset` : `/v1/shop/stores/${store.id}/invite`;
|
|
10360
|
+
const res = await api2(credentials.apiBase, path, {
|
|
10361
|
+
method: reset ? "POST" : "GET",
|
|
10362
|
+
headers: authHeaders(credentials),
|
|
10363
|
+
...reset ? { body: JSON.stringify(body) } : {}
|
|
10364
|
+
});
|
|
10365
|
+
if (res.status !== 200) {
|
|
10366
|
+
console.error(errorMessage(res.body, "获取邀请链接失败。"));
|
|
10367
|
+
return exitCodeForApiError(res.body);
|
|
10368
|
+
}
|
|
10369
|
+
const invite = res.body.data.invite;
|
|
10370
|
+
const url = `${shopWebBase(credentials.apiBase)}/join/${store.slug}?t=${invite.token}`;
|
|
10371
|
+
if (hasFlag("--json")) {
|
|
10372
|
+
console.log(JSON.stringify({ ...invite, joinUrl: url }, null, 2));
|
|
10373
|
+
return 0;
|
|
10374
|
+
}
|
|
10375
|
+
console.log(`${reset ? "已重置,新邀请链接(旧链接作废):" : "常驻邀请链接:"}`);
|
|
10376
|
+
console.log(` ${url}`);
|
|
10377
|
+
console.log(` 受邀默认权限:[${invite.permissions.join(", ")}] · 业务身份:${invite.bizRoles.length > 0 ? invite.bizRoles.join("/") : "无"}`);
|
|
10378
|
+
console.log(" 对方打开链接登录后一键加入;--reset --perms <a,b> --biz <sales,support> 可重置并改默认组合。");
|
|
10379
|
+
return 0;
|
|
10380
|
+
}
|
|
10381
|
+
async function shopUse(slug) {
|
|
10382
|
+
const credentials = requireCredentials();
|
|
10383
|
+
if (typeof credentials === "number")
|
|
10384
|
+
return credentials;
|
|
10385
|
+
if (!slug) {
|
|
10386
|
+
if (credentials.shopStore)
|
|
10387
|
+
console.log(`当前店铺:${credentials.shopStore}(crab shop use <slug> 切换)`);
|
|
10388
|
+
else
|
|
10389
|
+
console.log("未设置当前店。crab shop use <slug> 设置(多店账号店级命令需要)。");
|
|
10390
|
+
return 0;
|
|
10391
|
+
}
|
|
10392
|
+
const res = await api2(credentials.apiBase, "/v1/shop/stores", { headers: authHeaders(credentials) });
|
|
10393
|
+
if (res.status !== 200) {
|
|
10394
|
+
console.error(errorMessage(res.body, "获取店铺列表失败。"));
|
|
10395
|
+
return exitCodeForApiError(res.body);
|
|
10396
|
+
}
|
|
10397
|
+
const stores = res.body.data.stores;
|
|
10398
|
+
const hit = stores.find((x) => x.slug === slug || x.id === slug);
|
|
10399
|
+
if (!hit) {
|
|
10400
|
+
console.error(`找不到店铺 ${slug}(crab shop status 查看)`);
|
|
10401
|
+
return EXIT_NOT_FOUND;
|
|
10402
|
+
}
|
|
10403
|
+
saveCredentials({ ...credentials, shopStore: hit.slug });
|
|
10404
|
+
console.log(`✓ 当前店铺切换为「${hit.name}」(${hit.slug})——店级命令缺省作用于它,--store 可临时覆盖`);
|
|
10405
|
+
return 0;
|
|
10406
|
+
}
|
|
10407
|
+
async function shopStatus() {
|
|
10408
|
+
const credentials = requireCredentials();
|
|
10409
|
+
if (typeof credentials === "number")
|
|
10410
|
+
return credentials;
|
|
10411
|
+
const json = hasFlag("--json");
|
|
10412
|
+
const [storesRes, appRes] = await Promise.all([
|
|
10413
|
+
api2(credentials.apiBase, "/v1/shop/stores", { headers: authHeaders(credentials) }),
|
|
10414
|
+
api2(credentials.apiBase, "/v1/shop/applications/mine", { headers: authHeaders(credentials) })
|
|
10415
|
+
]);
|
|
10416
|
+
if (storesRes.status !== 200) {
|
|
10417
|
+
console.error(errorMessage(storesRes.body, "获取店铺列表失败。"));
|
|
10418
|
+
return exitCodeForApiError(storesRes.body);
|
|
10419
|
+
}
|
|
10420
|
+
const stores = storesRes.body.data.stores;
|
|
10421
|
+
const application = appRes.body.data.application;
|
|
10422
|
+
if (json) {
|
|
10423
|
+
console.log(JSON.stringify({ stores, application }, null, 2));
|
|
10424
|
+
return 0;
|
|
10425
|
+
}
|
|
10426
|
+
const statusText = { pending: "审核中", approved: "已通过", rejected: "已拒绝" };
|
|
10427
|
+
if (application) {
|
|
10428
|
+
const line = `申请 ${application.name}[${application.kind === "seller" ? "卖家开通" : "增开店铺"}]:${statusText[application.status] ?? application.status}`;
|
|
10429
|
+
console.log(application.status === "rejected" ? `${line} — 理由:${application.reviewNote || "未填写"}` : line);
|
|
10430
|
+
}
|
|
10431
|
+
if (stores.length === 0) {
|
|
10432
|
+
console.log(application?.status === "pending" ? "(审核通过后店铺自动出现在这里)" : "还没有店铺。运行 crab shop apply <店铺名> 提交开店申请。");
|
|
10433
|
+
return 0;
|
|
10434
|
+
}
|
|
10435
|
+
console.log(`共 ${stores.length} 家店铺${credentials.shopStore ? "(★ = 当前店)" : ""}:`);
|
|
10436
|
+
for (const store of stores) {
|
|
10437
|
+
const star = credentials.shopStore === store.slug ? "★ " : " ";
|
|
10438
|
+
console.log(`${star}${store.name} [${store.myRole}] ${store.slug}${store.status === "archived" ? "(已下架)" : ""} id: ${store.id}`);
|
|
10439
|
+
}
|
|
10440
|
+
if (stores.length > 1)
|
|
10441
|
+
console.log("(crab shop use <slug> 切换当前店;单条命令也可 --store <slug>)");
|
|
10442
|
+
return 0;
|
|
10443
|
+
}
|
|
10444
|
+
async function shopApply(name, tagline) {
|
|
10445
|
+
const credentials = requireCredentials();
|
|
10446
|
+
if (typeof credentials === "number")
|
|
10447
|
+
return credentials;
|
|
10448
|
+
const res = await api2(credentials.apiBase, "/v1/shop/applications", {
|
|
10449
|
+
method: "POST",
|
|
10450
|
+
headers: authHeaders(credentials),
|
|
10451
|
+
body: JSON.stringify({ name, ...tagline ? { tagline } : {} })
|
|
10452
|
+
});
|
|
10453
|
+
if (res.status !== 201) {
|
|
10454
|
+
console.error(errorMessage(res.body, "提交开店申请失败。"));
|
|
10455
|
+
return exitCodeForApiError(res.body);
|
|
10456
|
+
}
|
|
10457
|
+
const application = res.body.data.application;
|
|
10458
|
+
if (hasFlag("--json")) {
|
|
10459
|
+
console.log(JSON.stringify(application, null, 2));
|
|
10460
|
+
return 0;
|
|
10461
|
+
}
|
|
10462
|
+
console.log(`✓ 申请已提交(${application.id})`);
|
|
10463
|
+
console.log(` 类型:${application.kind === "seller" ? "卖家开通(首店)" : "增开店铺"} · 状态:审核中`);
|
|
10464
|
+
console.log(" 平台审核通过后店铺自动开通(增开店铺在通过时扣积分);crab shop status 查看进度。");
|
|
10465
|
+
return 0;
|
|
10466
|
+
}
|
|
10467
|
+
function shopPositionals(start, valueFlags) {
|
|
10468
|
+
const positional = [];
|
|
10469
|
+
const rest = args.slice(start);
|
|
10470
|
+
for (let i = 0;i < rest.length; i++) {
|
|
10471
|
+
if (valueFlags.includes(rest[i])) {
|
|
10472
|
+
i++;
|
|
10473
|
+
continue;
|
|
10474
|
+
}
|
|
10475
|
+
if (rest[i]?.startsWith("--"))
|
|
10476
|
+
continue;
|
|
10477
|
+
positional.push(rest[i]);
|
|
10478
|
+
}
|
|
10479
|
+
return positional;
|
|
10480
|
+
}
|
|
10481
|
+
async function fetchStoreCategories(credentials, storeId) {
|
|
10482
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${storeId}/categories`, { headers: authHeaders(credentials) });
|
|
10483
|
+
if (res.status !== 200) {
|
|
10484
|
+
console.error(errorMessage(res.body, "获取分类列表失败。"));
|
|
10485
|
+
return exitCodeForApiError(res.body);
|
|
10486
|
+
}
|
|
10487
|
+
return res.body.data.categories;
|
|
10488
|
+
}
|
|
10489
|
+
function resolveCategoryRef(list, ref) {
|
|
10490
|
+
const byId = list.find((c) => c.id === ref);
|
|
10491
|
+
if (byId)
|
|
10492
|
+
return byId;
|
|
10493
|
+
const byName = list.filter((c) => c.name === ref);
|
|
10494
|
+
if (byName.length === 1)
|
|
10495
|
+
return byName[0];
|
|
10496
|
+
if (byName.length > 1)
|
|
10497
|
+
return `名称「${ref}」有 ${byName.length} 个同名分类,请用 id 指定(crab shop categories 查看)`;
|
|
10498
|
+
return `分类不存在:${ref}(crab shop categories 查看)`;
|
|
10499
|
+
}
|
|
10500
|
+
async function shopCategories() {
|
|
10501
|
+
const credentials = requireCredentials();
|
|
10502
|
+
if (typeof credentials === "number")
|
|
10503
|
+
return credentials;
|
|
10504
|
+
const store = await resolveStoreArg(credentials);
|
|
10505
|
+
if (typeof store === "number")
|
|
10506
|
+
return store;
|
|
10507
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10508
|
+
if (typeof categories === "number")
|
|
10509
|
+
return categories;
|
|
10510
|
+
if (hasFlag("--json")) {
|
|
10511
|
+
console.log(JSON.stringify(categories, null, 2));
|
|
10512
|
+
return 0;
|
|
10513
|
+
}
|
|
10514
|
+
if (categories.length === 0) {
|
|
10515
|
+
console.log(`「${store.name}」还没有分类。crab shop category add <名称> 创建。`);
|
|
10516
|
+
return 0;
|
|
10517
|
+
}
|
|
10518
|
+
console.log(`「${store.name}」分类树(名称后 = 商品数):`);
|
|
10519
|
+
const byParent = new Map;
|
|
10520
|
+
for (const c of categories) {
|
|
10521
|
+
const key = c.parentId ?? "";
|
|
10522
|
+
byParent.set(key, [...byParent.get(key) ?? [], c]);
|
|
10523
|
+
}
|
|
10524
|
+
const print = (parentId, depth) => {
|
|
10525
|
+
for (const c of byParent.get(parentId ?? "") ?? []) {
|
|
10526
|
+
console.log(`${" ".repeat(depth * 2)} ${c.name} (${c.productCount}) ${c.id}`);
|
|
10527
|
+
print(c.id, depth + 1);
|
|
10528
|
+
}
|
|
10529
|
+
};
|
|
10530
|
+
print(null, 0);
|
|
10531
|
+
return 0;
|
|
10532
|
+
}
|
|
10533
|
+
async function shopCategoryAdd() {
|
|
10534
|
+
const credentials = requireCredentials();
|
|
10535
|
+
if (typeof credentials === "number")
|
|
10536
|
+
return credentials;
|
|
10537
|
+
const positional = shopPositionals(3, ["--parent"]);
|
|
10538
|
+
const name = positional.join(" ").trim();
|
|
10539
|
+
const parentRef = readFlag("--parent");
|
|
10540
|
+
if (!name) {
|
|
10541
|
+
console.error("用法:crab shop category add <名称> [--parent <分类 id 或名称>] --store <slug>");
|
|
10542
|
+
return EXIT_USAGE;
|
|
10543
|
+
}
|
|
10544
|
+
const store = await resolveStoreArg(credentials);
|
|
10545
|
+
if (typeof store === "number")
|
|
10546
|
+
return store;
|
|
10547
|
+
let parentId;
|
|
10548
|
+
if (parentRef) {
|
|
10549
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10550
|
+
if (typeof categories === "number")
|
|
10551
|
+
return categories;
|
|
10552
|
+
const parent = resolveCategoryRef(categories, parentRef);
|
|
10553
|
+
if (typeof parent === "string") {
|
|
10554
|
+
console.error(parent);
|
|
10555
|
+
return EXIT_NOT_FOUND;
|
|
10556
|
+
}
|
|
10557
|
+
parentId = parent.id;
|
|
10558
|
+
}
|
|
10559
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/categories`, {
|
|
10560
|
+
method: "POST",
|
|
10561
|
+
headers: authHeaders(credentials),
|
|
10562
|
+
body: JSON.stringify({ name, ...parentId ? { parentId } : {} })
|
|
10563
|
+
});
|
|
10564
|
+
if (res.status !== 201) {
|
|
10565
|
+
console.error(errorMessage(res.body, "创建分类失败。"));
|
|
10566
|
+
return exitCodeForApiError(res.body);
|
|
10567
|
+
}
|
|
10568
|
+
const category = res.body.data.category;
|
|
10569
|
+
if (hasFlag("--json")) {
|
|
10570
|
+
console.log(JSON.stringify(category, null, 2));
|
|
10571
|
+
return 0;
|
|
10572
|
+
}
|
|
10573
|
+
console.log(`✓ 分类已创建:${category.name}(${category.id},落同级末尾)`);
|
|
10574
|
+
return 0;
|
|
10575
|
+
}
|
|
10576
|
+
async function shopCategoryRename() {
|
|
10577
|
+
const credentials = requireCredentials();
|
|
10578
|
+
if (typeof credentials === "number")
|
|
10579
|
+
return credentials;
|
|
10580
|
+
const positional = shopPositionals(3, []);
|
|
10581
|
+
const ref = positional[0];
|
|
10582
|
+
const name = positional.slice(1).join(" ").trim();
|
|
10583
|
+
if (!ref || !name) {
|
|
10584
|
+
console.error("用法:crab shop category rename <分类 id 或名称> <新名称> --store <slug>");
|
|
10585
|
+
return EXIT_USAGE;
|
|
10586
|
+
}
|
|
10587
|
+
const store = await resolveStoreArg(credentials);
|
|
10588
|
+
if (typeof store === "number")
|
|
10589
|
+
return store;
|
|
10590
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10591
|
+
if (typeof categories === "number")
|
|
10592
|
+
return categories;
|
|
10593
|
+
const target = resolveCategoryRef(categories, ref);
|
|
10594
|
+
if (typeof target === "string") {
|
|
10595
|
+
console.error(target);
|
|
10596
|
+
return EXIT_NOT_FOUND;
|
|
10597
|
+
}
|
|
10598
|
+
const res = await api2(credentials.apiBase, `/v1/shop/categories/${target.id}`, {
|
|
10599
|
+
method: "PATCH",
|
|
10600
|
+
headers: authHeaders(credentials),
|
|
10601
|
+
body: JSON.stringify({ name })
|
|
10602
|
+
});
|
|
10603
|
+
if (res.status !== 200) {
|
|
10604
|
+
console.error(errorMessage(res.body, "重命名分类失败。"));
|
|
10605
|
+
return exitCodeForApiError(res.body);
|
|
10606
|
+
}
|
|
10607
|
+
if (hasFlag("--json"))
|
|
10608
|
+
console.log(JSON.stringify(res.body.data.category, null, 2));
|
|
10609
|
+
else
|
|
10610
|
+
console.log(`✓ 已重命名:${target.name} → ${name}`);
|
|
10611
|
+
return 0;
|
|
10612
|
+
}
|
|
10613
|
+
async function shopCategoryMove() {
|
|
10614
|
+
const credentials = requireCredentials();
|
|
10615
|
+
if (typeof credentials === "number")
|
|
10616
|
+
return credentials;
|
|
10617
|
+
const positional = shopPositionals(3, []);
|
|
10618
|
+
const ref = positional[0];
|
|
10619
|
+
const direction = positional[1];
|
|
10620
|
+
if (!ref || direction !== "up" && direction !== "down") {
|
|
10621
|
+
console.error(`用法:crab shop category move <分类 id 或名称> <up|down> --store <slug>
|
|
10622
|
+
(同级内上移/下移一位;已在边界时静默不动)`);
|
|
10623
|
+
return EXIT_USAGE;
|
|
10624
|
+
}
|
|
10625
|
+
const store = await resolveStoreArg(credentials);
|
|
10626
|
+
if (typeof store === "number")
|
|
10627
|
+
return store;
|
|
10628
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10629
|
+
if (typeof categories === "number")
|
|
10630
|
+
return categories;
|
|
10631
|
+
const target = resolveCategoryRef(categories, ref);
|
|
10632
|
+
if (typeof target === "string") {
|
|
10633
|
+
console.error(target);
|
|
10634
|
+
return EXIT_NOT_FOUND;
|
|
10635
|
+
}
|
|
10636
|
+
const res = await api2(credentials.apiBase, `/v1/shop/categories/${target.id}/move`, {
|
|
10637
|
+
method: "POST",
|
|
10638
|
+
headers: authHeaders(credentials),
|
|
10639
|
+
body: JSON.stringify({ direction })
|
|
10640
|
+
});
|
|
10641
|
+
if (res.status !== 200) {
|
|
10642
|
+
console.error(errorMessage(res.body, "移动分类失败。"));
|
|
10643
|
+
return exitCodeForApiError(res.body);
|
|
10644
|
+
}
|
|
10645
|
+
if (hasFlag("--json"))
|
|
10646
|
+
console.log(JSON.stringify(res.body.data.category, null, 2));
|
|
10647
|
+
else
|
|
10648
|
+
console.log(`✓ 已${direction === "up" ? "上移" : "下移"}:${target.name}`);
|
|
10649
|
+
return 0;
|
|
10650
|
+
}
|
|
10651
|
+
async function shopCategoryDelete() {
|
|
10652
|
+
const credentials = requireCredentials();
|
|
10653
|
+
if (typeof credentials === "number")
|
|
10654
|
+
return credentials;
|
|
10655
|
+
const positional = shopPositionals(3, []);
|
|
10656
|
+
const ref = positional[0];
|
|
10657
|
+
if (!ref) {
|
|
10658
|
+
console.error(`用法:crab shop category delete <分类 id> --store <slug>
|
|
10659
|
+
(有子分类时会被拒绝;叶子分类删除后旗下商品迁入「未分类」)`);
|
|
10660
|
+
return EXIT_USAGE;
|
|
10661
|
+
}
|
|
10662
|
+
const store = await resolveStoreArg(credentials);
|
|
10663
|
+
if (typeof store === "number")
|
|
10664
|
+
return store;
|
|
10665
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10666
|
+
if (typeof categories === "number")
|
|
10667
|
+
return categories;
|
|
10668
|
+
const target = resolveCategoryRef(categories, ref);
|
|
10669
|
+
if (typeof target === "string") {
|
|
10670
|
+
console.error(target);
|
|
10671
|
+
return EXIT_NOT_FOUND;
|
|
10672
|
+
}
|
|
10673
|
+
const res = await api2(credentials.apiBase, `/v1/shop/categories/${target.id}`, {
|
|
10674
|
+
method: "DELETE",
|
|
10675
|
+
headers: authHeaders(credentials)
|
|
10676
|
+
});
|
|
10677
|
+
if (res.status !== 200) {
|
|
10678
|
+
console.error(errorMessage(res.body, "删除分类失败。"));
|
|
10679
|
+
return exitCodeForApiError(res.body);
|
|
10680
|
+
}
|
|
10681
|
+
if (hasFlag("--json"))
|
|
10682
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
10683
|
+
else
|
|
10684
|
+
console.log(`✓ 已删除分类:${target.name}(旗下商品迁入「未分类」)`);
|
|
10685
|
+
return 0;
|
|
10686
|
+
}
|
|
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]);
|
|
10720
|
+
}
|
|
10721
|
+
return out;
|
|
10722
|
+
}
|
|
10723
|
+
function fmtMoney(cents) {
|
|
10724
|
+
if (cents === null || cents === undefined)
|
|
10725
|
+
return "—";
|
|
10726
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
10727
|
+
}
|
|
10728
|
+
function parseCentsFlag(raw, flagName) {
|
|
10729
|
+
if (raw === undefined || raw.trim() === "") {
|
|
10730
|
+
console.error(`缺少 ${flagName}(金额一律整数分,如 1990 = $19.90)`);
|
|
10731
|
+
return null;
|
|
10732
|
+
}
|
|
10733
|
+
const n = Number(raw.trim());
|
|
10734
|
+
if (!Number.isSafeInteger(n) || n < 0 || n > 1e8) {
|
|
10735
|
+
console.error(`${flagName} 必须是非负整数分(禁止元/浮点),如 1990 = $19.90`);
|
|
10736
|
+
return null;
|
|
10737
|
+
}
|
|
10738
|
+
return n;
|
|
10739
|
+
}
|
|
10740
|
+
function parseIntFlag(raw, flagName, minN) {
|
|
10741
|
+
if (raw === undefined || raw.trim() === "") {
|
|
10742
|
+
console.error(`缺少 ${flagName}`);
|
|
10743
|
+
return null;
|
|
10744
|
+
}
|
|
10745
|
+
const n = Number(raw.trim());
|
|
10746
|
+
if (!Number.isSafeInteger(n) || n < minN) {
|
|
10747
|
+
console.error(`${flagName} 必须是 ≥${minN} 的整数`);
|
|
10748
|
+
return null;
|
|
10749
|
+
}
|
|
10750
|
+
return n;
|
|
10751
|
+
}
|
|
10752
|
+
function parseOnOffFlag(raw, flagName) {
|
|
10753
|
+
if (raw !== "on" && raw !== "off") {
|
|
10754
|
+
console.error(`${flagName} 只接受 on | off`);
|
|
10755
|
+
return null;
|
|
10756
|
+
}
|
|
10757
|
+
return raw === "on";
|
|
10758
|
+
}
|
|
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);
|
|
10777
|
+
}
|
|
10778
|
+
const store = await resolveStoreArg(credentials);
|
|
10779
|
+
if (typeof store === "number")
|
|
10780
|
+
return store;
|
|
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;
|
|
10785
|
+
}
|
|
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);
|
|
10791
|
+
}
|
|
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;
|
|
10810
|
+
}
|
|
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() {
|
|
10822
|
+
const credentials = requireCredentials();
|
|
10823
|
+
if (typeof credentials === "number")
|
|
10824
|
+
return credentials;
|
|
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;金额一律整数分`);
|
|
10957
|
+
return EXIT_USAGE;
|
|
10958
|
+
}
|
|
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(创建即锁定,之后不可改)");
|
|
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
|
+
}
|
|
11090
|
+
const store = await resolveStoreArg(credentials);
|
|
11091
|
+
if (typeof store === "number")
|
|
11092
|
+
return store;
|
|
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();
|
|
11205
|
+
}
|
|
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}`, {
|
|
11211
|
+
method: "PATCH",
|
|
11212
|
+
headers: authHeaders(credentials),
|
|
11213
|
+
body: JSON.stringify(body)
|
|
11214
|
+
});
|
|
11215
|
+
if (res.status !== 200) {
|
|
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, "归档商品失败。"));
|
|
11366
|
+
return exitCodeForApiError(res.body);
|
|
11367
|
+
}
|
|
11368
|
+
if (hasFlag("--json"))
|
|
11369
|
+
console.log(JSON.stringify({ ok: true, id: product.id }, null, 2));
|
|
11370
|
+
else
|
|
11371
|
+
console.log(`✓ 已归档:${product.name} ${product.id}(恢复:crab shop product update ${product.id} --status draft)`);
|
|
11372
|
+
return 0;
|
|
11373
|
+
}
|
|
11374
|
+
async function shopStockAdjust(mode) {
|
|
11375
|
+
const credentials = requireCredentials();
|
|
11376
|
+
if (typeof credentials === "number")
|
|
11377
|
+
return credentials;
|
|
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 的整数");
|
|
11496
|
+
return EXIT_USAGE;
|
|
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;
|
|
11559
|
+
const store = await resolveStoreArg(credentials);
|
|
11560
|
+
if (typeof store === "number")
|
|
11561
|
+
return store;
|
|
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);
|
|
11569
|
+
}
|
|
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) });
|
|
11577
|
+
if (res.status !== 200) {
|
|
11578
|
+
console.error(errorMessage(res.body, "获取出入库台账失败。"));
|
|
11579
|
+
return exitCodeForApiError(res.body);
|
|
11580
|
+
}
|
|
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 翻页)`);
|
|
11600
|
+
return 0;
|
|
11601
|
+
}
|
|
11602
|
+
async function hrOrgsList() {
|
|
11603
|
+
const credentials = requireCredentials();
|
|
11604
|
+
if (typeof credentials === "number")
|
|
11605
|
+
return credentials;
|
|
11606
|
+
const res = await api2(credentials.apiBase, "/v1/hr/orgs", { headers: authHeaders(credentials) });
|
|
11607
|
+
if (res.status !== 200) {
|
|
11608
|
+
console.error(errorMessage(res.body, "获取组织列表失败。"));
|
|
11609
|
+
return exitCodeForApiError(res.body);
|
|
11610
|
+
}
|
|
11611
|
+
const orgs = res.body.data.orgs;
|
|
11612
|
+
if (hasFlag("--json")) {
|
|
11613
|
+
console.log(JSON.stringify(orgs, null, 2));
|
|
11614
|
+
return 0;
|
|
11615
|
+
}
|
|
11616
|
+
if (orgs.length === 0) {
|
|
11617
|
+
console.log("还没有公司 / 组织。");
|
|
11618
|
+
return 0;
|
|
11619
|
+
}
|
|
11620
|
+
for (const org of orgs) {
|
|
11621
|
+
console.log(`${org.name} [${org.myRole}] ${hrOrgToken(org)} id: ${org.id}`);
|
|
11622
|
+
}
|
|
11623
|
+
return 0;
|
|
11624
|
+
}
|
|
11625
|
+
async function hrOrgApply(name) {
|
|
11626
|
+
if (!name) {
|
|
11627
|
+
console.error("用法:crab hr org apply <名称> [--note 说明] [--json](审核通过扣 1 积分自动建司)");
|
|
11628
|
+
return EXIT_USAGE;
|
|
11629
|
+
}
|
|
11630
|
+
const note = readFlag("--note") ?? "";
|
|
11631
|
+
const credentials = requireCredentials();
|
|
11632
|
+
if (typeof credentials === "number")
|
|
11633
|
+
return credentials;
|
|
11634
|
+
const res = await api2(credentials.apiBase, "/v1/hr/orgs/applications", {
|
|
11635
|
+
method: "POST",
|
|
11636
|
+
headers: authHeaders(credentials),
|
|
11637
|
+
body: JSON.stringify({ name, note })
|
|
11638
|
+
});
|
|
11639
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
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, "查询申请进度失败。"));
|
|
11664
|
+
return exitCodeForApiError(res.body);
|
|
11665
|
+
}
|
|
11666
|
+
const application = res.body.data.application;
|
|
11667
|
+
if (hasFlag("--json")) {
|
|
11668
|
+
console.log(JSON.stringify(application, null, 2));
|
|
11669
|
+
return 0;
|
|
11670
|
+
}
|
|
11671
|
+
if (!application) {
|
|
11672
|
+
console.log("还没有建司申请(crab hr org apply <名称> 提交)。");
|
|
11673
|
+
return 0;
|
|
11674
|
+
}
|
|
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: 令牌绑定标识随新名变化,注意重新授权)。`);
|
|
11715
|
+
return 0;
|
|
11716
|
+
}
|
|
11717
|
+
async function hrMembersList() {
|
|
11718
|
+
const credentials = requireCredentials();
|
|
11719
|
+
if (typeof credentials === "number")
|
|
11720
|
+
return credentials;
|
|
11721
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11722
|
+
if (typeof org === "number")
|
|
11723
|
+
return org;
|
|
11724
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members`, { headers: authHeaders(credentials) });
|
|
11725
|
+
if (res.status !== 200) {
|
|
11726
|
+
console.error(errorMessage(res.body, "获取成员列表失败。"));
|
|
11727
|
+
return exitCodeForApiError(res.body);
|
|
11728
|
+
}
|
|
11729
|
+
const members = res.body.data.members;
|
|
11730
|
+
if (hasFlag("--json")) {
|
|
11731
|
+
console.log(JSON.stringify(members, null, 2));
|
|
11732
|
+
return 0;
|
|
11733
|
+
}
|
|
11734
|
+
console.log(`组织:${hrOrgToken(org)}`);
|
|
11735
|
+
for (const m of members) {
|
|
11736
|
+
const flags = Object.entries(m.permissions).filter(([, v]) => v).map(([k]) => k).join(",") || "只读";
|
|
11737
|
+
console.log(`${m.username}${m.title ? `(${m.title})` : ""} [${m.role}] ${flags} id: ${m.id}${m.status !== "active" ? ` · ${m.status}` : ""}`);
|
|
11738
|
+
}
|
|
11739
|
+
return 0;
|
|
11740
|
+
}
|
|
11741
|
+
async function hrMemberAdd() {
|
|
11742
|
+
const credentials = requireCredentials();
|
|
11743
|
+
if (typeof credentials === "number")
|
|
11744
|
+
return credentials;
|
|
11745
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11746
|
+
if (typeof org === "number")
|
|
11747
|
+
return org;
|
|
11748
|
+
const username = positionalArgs(args.slice(3), HR_VALUED_FLAGS);
|
|
11749
|
+
const target = username[0];
|
|
11750
|
+
if (!target) {
|
|
11751
|
+
console.error("用法:crab hr member add <用户名> [--title 头衔] [--org <arg>]");
|
|
11752
|
+
return EXIT_USAGE;
|
|
11753
|
+
}
|
|
11754
|
+
const title = readFlag("--title");
|
|
11755
|
+
const permissions = parseHrPermissions(readFlag("--permissions"));
|
|
11756
|
+
if (permissions === null)
|
|
11757
|
+
return EXIT_USAGE;
|
|
11758
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members`, {
|
|
11759
|
+
method: "POST",
|
|
11760
|
+
headers: authHeaders(credentials),
|
|
11761
|
+
body: JSON.stringify({ username: target, ...title ? { title } : {}, ...permissions ? { permissions } : {} })
|
|
11762
|
+
});
|
|
11763
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
11764
|
+
console.error(errorMessage(res.body, "邀请成员失败。"));
|
|
11765
|
+
return exitCodeForApiError(res.body);
|
|
11766
|
+
}
|
|
11767
|
+
if (hasFlag("--json"))
|
|
11768
|
+
console.log(JSON.stringify(res.body.data, null, 2));
|
|
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 调整)。`);
|
|
11772
|
+
}
|
|
11773
|
+
return 0;
|
|
11774
|
+
}
|
|
11775
|
+
async function hrMemberSet() {
|
|
11776
|
+
const credentials = requireCredentials();
|
|
11777
|
+
if (typeof credentials === "number")
|
|
11778
|
+
return credentials;
|
|
11779
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11780
|
+
if (typeof org === "number")
|
|
11781
|
+
return org;
|
|
11782
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
11783
|
+
const role = readFlag("--role");
|
|
11784
|
+
const permissions = parseHrPermissions(readFlag("--permissions"));
|
|
11785
|
+
if (permissions === null)
|
|
11786
|
+
return EXIT_USAGE;
|
|
11787
|
+
const title = readFlag("--title");
|
|
11788
|
+
if (!target || !role && !permissions && title === undefined) {
|
|
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>]");
|
|
11790
|
+
return EXIT_USAGE;
|
|
11791
|
+
}
|
|
11792
|
+
if (role && role !== "admin" && role !== "member") {
|
|
11793
|
+
console.error("--role 只接受 admin | member(owner 不可改)。");
|
|
11794
|
+
return EXIT_USAGE;
|
|
11795
|
+
}
|
|
11796
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members/${encodeURIComponent(target)}`, {
|
|
11797
|
+
method: "PATCH",
|
|
11798
|
+
headers: authHeaders(credentials),
|
|
11799
|
+
body: JSON.stringify({ ...role ? { role } : {}, ...permissions ? { permissions } : {}, ...title !== undefined ? { title } : {} })
|
|
11800
|
+
});
|
|
11801
|
+
if (res.status !== 200) {
|
|
11802
|
+
console.error(errorMessage(res.body, "更新成员失败。"));
|
|
11803
|
+
return exitCodeForApiError(res.body);
|
|
11804
|
+
}
|
|
11805
|
+
const member = res.body.data.member;
|
|
11806
|
+
if (hasFlag("--json"))
|
|
11807
|
+
console.log(JSON.stringify(member, null, 2));
|
|
11808
|
+
else
|
|
11809
|
+
console.log(`✓ 已更新 ${member?.username ?? target}(${member?.role ?? "?"})。`);
|
|
11810
|
+
return 0;
|
|
11811
|
+
}
|
|
11812
|
+
async function hrMemberRm() {
|
|
11813
|
+
const credentials = requireCredentials();
|
|
11814
|
+
if (typeof credentials === "number")
|
|
11815
|
+
return credentials;
|
|
11816
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11817
|
+
if (typeof org === "number")
|
|
11818
|
+
return org;
|
|
11819
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
11820
|
+
if (!target) {
|
|
11821
|
+
console.error("用法:crab hr member rm <memberId> [--org <arg>](id 见 crab hr members)");
|
|
11822
|
+
return EXIT_USAGE;
|
|
11823
|
+
}
|
|
11824
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members/${encodeURIComponent(target)}`, {
|
|
11825
|
+
method: "DELETE",
|
|
11826
|
+
headers: authHeaders(credentials)
|
|
11827
|
+
});
|
|
11828
|
+
if (res.status !== 200) {
|
|
11829
|
+
console.error(errorMessage(res.body, "移除成员失败。"));
|
|
11830
|
+
return exitCodeForApiError(res.body);
|
|
11831
|
+
}
|
|
11832
|
+
if (hasFlag("--json"))
|
|
11833
|
+
console.log(JSON.stringify(res.body.data, null, 2));
|
|
11834
|
+
else
|
|
11835
|
+
console.log("✓ 已移除成员(可随时重新邀请)。");
|
|
11836
|
+
return 0;
|
|
11837
|
+
}
|
|
11838
|
+
async function hrInvite() {
|
|
11839
|
+
const credentials = requireCredentials();
|
|
11840
|
+
if (typeof credentials === "number")
|
|
11841
|
+
return credentials;
|
|
11842
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11843
|
+
if (typeof org === "number")
|
|
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
|
+
}
|
|
11867
|
+
const path = hasFlag("--reset") ? "/invite/reset" : "/invite";
|
|
11868
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}${path}`, { method: hasFlag("--reset") ? "POST" : "GET", headers: authHeaders(credentials) });
|
|
11869
|
+
if (res.status !== 200) {
|
|
11870
|
+
console.error(errorMessage(res.body, "获取邀请链接失败。"));
|
|
11871
|
+
return exitCodeForApiError(res.body);
|
|
11872
|
+
}
|
|
11873
|
+
const invite = res.body.data.invite;
|
|
11874
|
+
const link = `${hrWebBase(credentials.apiBase)}/join/${invite.token}`;
|
|
11875
|
+
if (hasFlag("--json"))
|
|
11876
|
+
console.log(JSON.stringify({ ...invite, link }, null, 2));
|
|
11877
|
+
else {
|
|
11878
|
+
if (hasFlag("--reset"))
|
|
11879
|
+
console.log("✓ 已重置(旧链接立即失效,默认权限配置保留)。");
|
|
11880
|
+
const on = Object.entries(invite.permissions).filter(([, v]) => v).map(([k]) => k);
|
|
11881
|
+
console.log(`邀请链接:${link}`);
|
|
11882
|
+
console.log(`加入后默认权限:${on.length ? on.join(", ") : "全只读"}(修改:--permissions org.manage=1,...)`);
|
|
11883
|
+
console.log("未注册的老板 / 同事打开此链接,注册后自动加入组织。");
|
|
11884
|
+
}
|
|
11885
|
+
return 0;
|
|
11886
|
+
}
|
|
11887
|
+
async function hrPeopleList() {
|
|
11888
|
+
const credentials = requireCredentials();
|
|
11889
|
+
if (typeof credentials === "number")
|
|
11890
|
+
return credentials;
|
|
11891
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11892
|
+
if (typeof org === "number")
|
|
11893
|
+
return org;
|
|
11894
|
+
const status = readFlag("--status");
|
|
11895
|
+
if (status && !HR_PERSON_STATUSES.includes(status)) {
|
|
11896
|
+
console.error(`未知状态:${status}
|
|
11897
|
+
可用:${HR_PERSON_STATUSES.join(", ")}`);
|
|
11898
|
+
return EXIT_USAGE;
|
|
11899
|
+
}
|
|
11900
|
+
const params = new URLSearchParams;
|
|
11901
|
+
const q = readFlag("--q");
|
|
11902
|
+
if (q)
|
|
11903
|
+
params.set("q", q);
|
|
11904
|
+
if (status)
|
|
11905
|
+
params.set("status", status);
|
|
11906
|
+
const dept = readFlag("--dept");
|
|
11907
|
+
if (dept)
|
|
11908
|
+
params.set("dept", dept);
|
|
11909
|
+
const limit = readFlag("--limit");
|
|
11910
|
+
if (limit)
|
|
11911
|
+
params.set("limit", limit);
|
|
11912
|
+
const offset = readFlag("--offset");
|
|
11913
|
+
if (offset)
|
|
11914
|
+
params.set("offset", offset);
|
|
11915
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people?${params}`, { headers: authHeaders(credentials) });
|
|
11916
|
+
if (res.status !== 200) {
|
|
11917
|
+
console.error(errorMessage(res.body, "获取花名册失败。"));
|
|
11918
|
+
return exitCodeForApiError(res.body);
|
|
11919
|
+
}
|
|
11920
|
+
const page = res.body.data;
|
|
11921
|
+
if (hasFlag("--json")) {
|
|
11922
|
+
console.log(JSON.stringify({ ...page, org: { id: org.id, name: org.name } }, null, 2));
|
|
11923
|
+
return 0;
|
|
11924
|
+
}
|
|
11925
|
+
console.log(`组织:${hrOrgToken(org)} · 共 ${page.total} 人`);
|
|
11926
|
+
if (page.people.length === 0) {
|
|
11927
|
+
console.log('没有匹配的人员(crab hr person create --name "…" --hired-at YYYY-MM-DD 建档)。');
|
|
11928
|
+
return 0;
|
|
11929
|
+
}
|
|
11930
|
+
for (const p of page.people) {
|
|
11931
|
+
const day = (ms) => ms ? new Date(ms).toISOString().slice(0, 10) : "—";
|
|
11932
|
+
console.log(`${p.name} ${p.dept ? `${p.dept} · ` : ""}${p.role || "员工"} [${p.status}] 入职 ${day(p.hiredAt)} id: ${p.id}`);
|
|
11933
|
+
if (p.alert)
|
|
11934
|
+
console.log(` ⚠ ${p.alert}`);
|
|
11935
|
+
}
|
|
11936
|
+
if (page.people.length < page.total)
|
|
11937
|
+
console.log(`(还有更多:--offset ${(Number(offset ?? 0) || 0) + page.people.length})`);
|
|
11938
|
+
return 0;
|
|
11939
|
+
}
|
|
11940
|
+
async function hrPersonDetail(id) {
|
|
11941
|
+
if (!id) {
|
|
11942
|
+
console.error("用法:crab hr person <id> [--json](id 见 crab hr people)");
|
|
11943
|
+
return EXIT_USAGE;
|
|
11944
|
+
}
|
|
11945
|
+
const credentials = requireCredentials();
|
|
11946
|
+
if (typeof credentials === "number")
|
|
11947
|
+
return credentials;
|
|
11948
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11949
|
+
if (typeof org === "number")
|
|
11950
|
+
return org;
|
|
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;
|
|
11964
|
+
}
|
|
11965
|
+
if (!person) {
|
|
11966
|
+
console.error("人员不存在或不属于当前组织。");
|
|
11967
|
+
return EXIT_NOT_FOUND;
|
|
11968
|
+
}
|
|
11969
|
+
if (hasFlag("--json")) {
|
|
11970
|
+
console.log(JSON.stringify(person, null, 2));
|
|
11971
|
+
return 0;
|
|
11972
|
+
}
|
|
11973
|
+
const p = person;
|
|
11974
|
+
const day = (ms) => ms ? new Date(ms).toISOString().slice(0, 10) : "—";
|
|
11975
|
+
console.log(`${p.name} ${p.dept ? `${p.dept} · ` : ""}${p.role || "员工"} [${p.status}]`);
|
|
11976
|
+
console.log(` 入职 ${day(p.hiredAt)} · 合同 ${p.contractType || "—"}(${day(p.contractEnd)})`);
|
|
11977
|
+
if (p.mobile || p.email)
|
|
11978
|
+
console.log(` 联系方式: ${p.mobile || "—"} / ${p.email || "—"}`);
|
|
11979
|
+
console.log(` 身份证: ${p.idNumber ?? "(无权限)"} · 薪资: ${p.salaryCents != null ? `¥${(p.salaryCents / 100).toFixed(2)}` : "(无权限)"}`);
|
|
11980
|
+
if (p.alert)
|
|
11981
|
+
console.log(` ⚠ ${p.alert}`);
|
|
11982
|
+
console.log(` 时间线: crab hr events --person ${p.id}`);
|
|
11983
|
+
return 0;
|
|
11984
|
+
}
|
|
11985
|
+
async function hrPersonCreate() {
|
|
11986
|
+
const name = readFlag("--name");
|
|
11987
|
+
const hiredFlag = readFlag("--hired-at");
|
|
11988
|
+
if (!name || !hiredFlag) {
|
|
11989
|
+
console.error('用法:crab hr person create --name "姓名" --hired-at YYYY-MM-DD [--employee-no 工号] [--dept 部门] [--role 岗位] [--status active|probation] [--probation-end YYYY-MM-DD] [--contract-type 类型] [--contract-end YYYY-MM-DD] [--mobile] [--email] [--notes] [--json]');
|
|
11990
|
+
return EXIT_USAGE;
|
|
11991
|
+
}
|
|
11992
|
+
const hired = parseHrDate(hiredFlag);
|
|
11993
|
+
if (hired == null) {
|
|
11994
|
+
console.error("--hired-at 格式应为 YYYY-MM-DD。");
|
|
11995
|
+
return EXIT_USAGE;
|
|
11996
|
+
}
|
|
11997
|
+
const credentials = requireCredentials();
|
|
11998
|
+
if (typeof credentials === "number")
|
|
11999
|
+
return credentials;
|
|
12000
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12001
|
+
if (typeof org === "number")
|
|
12002
|
+
return org;
|
|
12003
|
+
const probation = parseHrDate(readFlag("--probation-end"));
|
|
12004
|
+
if (probation === null) {
|
|
12005
|
+
console.error("--probation-end 格式应为 YYYY-MM-DD。");
|
|
12006
|
+
return EXIT_USAGE;
|
|
12007
|
+
}
|
|
12008
|
+
const contractEnd = parseHrDate(readFlag("--contract-end"));
|
|
12009
|
+
if (contractEnd === null) {
|
|
12010
|
+
console.error("--contract-end 格式应为 YYYY-MM-DD。");
|
|
12011
|
+
return EXIT_USAGE;
|
|
12012
|
+
}
|
|
12013
|
+
const status = readFlag("--status") ?? "active";
|
|
12014
|
+
if (!HR_PERSON_STATUSES.includes(status) || status === "resigned") {
|
|
12015
|
+
console.error("--status 只接受 active | probation(建档不直接建离职)。");
|
|
12016
|
+
return EXIT_USAGE;
|
|
12017
|
+
}
|
|
12018
|
+
const payload = {
|
|
12019
|
+
name,
|
|
12020
|
+
hiredAt: hired,
|
|
12021
|
+
status,
|
|
12022
|
+
employeeNo: readFlag("--employee-no") ?? "",
|
|
12023
|
+
dept: readFlag("--dept") ?? "",
|
|
12024
|
+
role: readFlag("--role") ?? "",
|
|
12025
|
+
mobile: readFlag("--mobile") ?? "",
|
|
12026
|
+
email: readFlag("--email") ?? "",
|
|
12027
|
+
notes: readFlag("--notes") ?? ""
|
|
12028
|
+
};
|
|
12029
|
+
if (probation != null)
|
|
12030
|
+
payload.probationEnd = probation;
|
|
12031
|
+
const contractType = readFlag("--contract-type");
|
|
12032
|
+
if (contractType)
|
|
12033
|
+
payload.contractType = contractType;
|
|
12034
|
+
if (contractEnd != null)
|
|
12035
|
+
payload.contractEnd = contractEnd;
|
|
12036
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people`, {
|
|
12037
|
+
method: "POST",
|
|
12038
|
+
headers: authHeaders(credentials),
|
|
12039
|
+
body: JSON.stringify(payload)
|
|
12040
|
+
});
|
|
12041
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
12042
|
+
console.error(errorMessage(res.body, "建档失败。"));
|
|
12043
|
+
return exitCodeForApiError(res.body);
|
|
12044
|
+
}
|
|
12045
|
+
const person = res.body.data.person;
|
|
12046
|
+
if (hasFlag("--json"))
|
|
12047
|
+
console.log(JSON.stringify(person, null, 2));
|
|
12048
|
+
else
|
|
12049
|
+
console.log(`✓ 已建档 ${person.name}(自动落入职事件)· id: ${person.id}`);
|
|
12050
|
+
return 0;
|
|
12051
|
+
}
|
|
12052
|
+
async function hrPersonEdit() {
|
|
12053
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12054
|
+
if (!target) {
|
|
12055
|
+
console.error("用法:crab hr person edit <id> [--dept] [--role] [--status active|probation|resigned] [--offboarded-at YYYY-MM-DD] [--contract-type] [--contract-end YYYY-MM-DD] [--mobile] [--email] [--notes] [--json]");
|
|
12056
|
+
return EXIT_USAGE;
|
|
12057
|
+
}
|
|
12058
|
+
const credentials = requireCredentials();
|
|
12059
|
+
if (typeof credentials === "number")
|
|
12060
|
+
return credentials;
|
|
12061
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12062
|
+
if (typeof org === "number")
|
|
12063
|
+
return org;
|
|
12064
|
+
const payload = {};
|
|
12065
|
+
const simple = [
|
|
12066
|
+
["--dept", "dept"],
|
|
12067
|
+
["--role", "role"],
|
|
12068
|
+
["--contract-type", "contractType"],
|
|
12069
|
+
["--mobile", "mobile"],
|
|
12070
|
+
["--email", "email"],
|
|
12071
|
+
["--notes", "notes"]
|
|
12072
|
+
];
|
|
12073
|
+
for (const [flag, key] of simple) {
|
|
12074
|
+
const value = readFlag(flag);
|
|
12075
|
+
if (value !== undefined)
|
|
12076
|
+
payload[key] = value;
|
|
12077
|
+
}
|
|
12078
|
+
const status = readFlag("--status");
|
|
12079
|
+
if (status) {
|
|
12080
|
+
if (!HR_PERSON_STATUSES.includes(status)) {
|
|
12081
|
+
console.error(`--status 只接受:${HR_PERSON_STATUSES.join(", ")}`);
|
|
12082
|
+
return EXIT_USAGE;
|
|
12083
|
+
}
|
|
12084
|
+
payload.status = status;
|
|
12085
|
+
}
|
|
12086
|
+
const contractEnd = parseHrDate(readFlag("--contract-end"));
|
|
12087
|
+
if (contractEnd === null) {
|
|
12088
|
+
console.error("--contract-end 格式应为 YYYY-MM-DD。");
|
|
12089
|
+
return EXIT_USAGE;
|
|
12090
|
+
}
|
|
12091
|
+
if (contractEnd != null)
|
|
12092
|
+
payload.contractEnd = contractEnd;
|
|
12093
|
+
const offboarded = parseHrDate(readFlag("--offboarded-at"));
|
|
12094
|
+
if (offboarded === null) {
|
|
12095
|
+
console.error("--offboarded-at 格式应为 YYYY-MM-DD。");
|
|
12096
|
+
return EXIT_USAGE;
|
|
12097
|
+
}
|
|
12098
|
+
if (offboarded != null)
|
|
12099
|
+
payload.offboardedAt = offboarded;
|
|
12100
|
+
if (Object.keys(payload).length === 0) {
|
|
12101
|
+
console.error("没有要更新的字段。");
|
|
12102
|
+
return EXIT_USAGE;
|
|
12103
|
+
}
|
|
12104
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/${encodeURIComponent(target)}`, {
|
|
12105
|
+
method: "PATCH",
|
|
12106
|
+
headers: authHeaders(credentials),
|
|
12107
|
+
body: JSON.stringify(payload)
|
|
12108
|
+
});
|
|
12109
|
+
if (res.status !== 200) {
|
|
12110
|
+
console.error(errorMessage(res.body, "更新失败。"));
|
|
12111
|
+
return exitCodeForApiError(res.body);
|
|
12112
|
+
}
|
|
12113
|
+
const person = res.body.data.person;
|
|
12114
|
+
if (hasFlag("--json"))
|
|
12115
|
+
console.log(JSON.stringify(person, null, 2));
|
|
12116
|
+
else
|
|
12117
|
+
console.log(`✓ 已更新 ${person?.name ?? target}(${person?.status ?? "?"};状态流转自动落痕)。`);
|
|
12118
|
+
return 0;
|
|
12119
|
+
}
|
|
12120
|
+
async function hrPersonOffboard() {
|
|
12121
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12122
|
+
if (!target) {
|
|
12123
|
+
console.error("用法:crab hr person offboard <id> [--date YYYY-MM-DD] [--json](离职封存不删,记录依法保留)");
|
|
12124
|
+
return EXIT_USAGE;
|
|
12125
|
+
}
|
|
12126
|
+
const credentials = requireCredentials();
|
|
12127
|
+
if (typeof credentials === "number")
|
|
12128
|
+
return credentials;
|
|
12129
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12130
|
+
if (typeof org === "number")
|
|
12131
|
+
return org;
|
|
12132
|
+
const date = parseHrDate(readFlag("--date"));
|
|
12133
|
+
if (date === null) {
|
|
12134
|
+
console.error("--date 格式应为 YYYY-MM-DD。");
|
|
12135
|
+
return EXIT_USAGE;
|
|
12136
|
+
}
|
|
12137
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/${encodeURIComponent(target)}`, {
|
|
12138
|
+
method: "PATCH",
|
|
12139
|
+
headers: authHeaders(credentials),
|
|
12140
|
+
body: JSON.stringify({ status: "resigned", ...date != null ? { offboardedAt: date } : {} })
|
|
12141
|
+
});
|
|
12142
|
+
if (res.status !== 200) {
|
|
12143
|
+
console.error(errorMessage(res.body, "离职办理失败。"));
|
|
12144
|
+
return exitCodeForApiError(res.body);
|
|
12145
|
+
}
|
|
12146
|
+
const person = res.body.data.person;
|
|
12147
|
+
if (hasFlag("--json"))
|
|
12148
|
+
console.log(JSON.stringify(person, null, 2));
|
|
12149
|
+
else
|
|
12150
|
+
console.log(`✓ ${person?.name ?? target} 已离职(档案封存,离职事件已落时间线)。`);
|
|
12151
|
+
return 0;
|
|
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
|
+
}
|
|
12220
|
+
async function hrEventAdd() {
|
|
12221
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12222
|
+
const type = readFlag("--type");
|
|
12223
|
+
const note = readFlag("--note");
|
|
12224
|
+
if (!target || !type || !note) {
|
|
12225
|
+
console.error(`用法:crab hr event add <personId> --type <${HR_EVENT_TYPES.join("|")}> --note "记录内容" [--at YYYY-MM-DD] [--json]`);
|
|
12226
|
+
return EXIT_USAGE;
|
|
12227
|
+
}
|
|
12228
|
+
if (!HR_EVENT_TYPES.includes(type)) {
|
|
12229
|
+
console.error(`未知事件类型:${type}
|
|
12230
|
+
可用:${HR_EVENT_TYPES.join(", ")}`);
|
|
12231
|
+
return EXIT_USAGE;
|
|
12232
|
+
}
|
|
12233
|
+
const at = parseHrDate(readFlag("--at"));
|
|
12234
|
+
if (at === null) {
|
|
12235
|
+
console.error("--at 格式应为 YYYY-MM-DD。");
|
|
12236
|
+
return EXIT_USAGE;
|
|
12237
|
+
}
|
|
12238
|
+
const credentials = requireCredentials();
|
|
12239
|
+
if (typeof credentials === "number")
|
|
12240
|
+
return credentials;
|
|
12241
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12242
|
+
if (typeof org === "number")
|
|
12243
|
+
return org;
|
|
12244
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/${encodeURIComponent(target)}/events`, {
|
|
12245
|
+
method: "POST",
|
|
12246
|
+
headers: authHeaders(credentials),
|
|
12247
|
+
body: JSON.stringify({ type, note, ...at != null ? { at } : {} })
|
|
12248
|
+
});
|
|
12249
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
12250
|
+
console.error(errorMessage(res.body, "落痕失败。"));
|
|
12251
|
+
return exitCodeForApiError(res.body);
|
|
12252
|
+
}
|
|
12253
|
+
const event = res.body.data.event;
|
|
12254
|
+
if (hasFlag("--json"))
|
|
12255
|
+
console.log(JSON.stringify(event, null, 2));
|
|
12256
|
+
else
|
|
12257
|
+
console.log(`✓ 已落痕(${type})· 事件 id: ${event.id}`);
|
|
12258
|
+
return 0;
|
|
12259
|
+
}
|
|
12260
|
+
async function hrEventsList() {
|
|
12261
|
+
const credentials = requireCredentials();
|
|
12262
|
+
if (typeof credentials === "number")
|
|
12263
|
+
return credentials;
|
|
12264
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12265
|
+
if (typeof org === "number")
|
|
12266
|
+
return org;
|
|
12267
|
+
const type = readFlag("--type");
|
|
12268
|
+
if (type && !HR_EVENT_TYPES.includes(type)) {
|
|
12269
|
+
console.error(`未知事件类型:${type}
|
|
12270
|
+
可用:${HR_EVENT_TYPES.join(", ")}`);
|
|
12271
|
+
return EXIT_USAGE;
|
|
12272
|
+
}
|
|
12273
|
+
const params = new URLSearchParams;
|
|
12274
|
+
const person = readFlag("--person");
|
|
12275
|
+
if (person)
|
|
12276
|
+
params.set("personId", person);
|
|
12277
|
+
if (type)
|
|
12278
|
+
params.set("type", type);
|
|
12279
|
+
const limit = readFlag("--limit");
|
|
12280
|
+
if (limit)
|
|
12281
|
+
params.set("limit", limit);
|
|
12282
|
+
const offset = readFlag("--offset");
|
|
12283
|
+
if (offset)
|
|
12284
|
+
params.set("offset", offset);
|
|
12285
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/events?${params}`, { headers: authHeaders(credentials) });
|
|
12286
|
+
if (res.status !== 200) {
|
|
12287
|
+
console.error(errorMessage(res.body, "获取时间线失败。"));
|
|
12288
|
+
return exitCodeForApiError(res.body);
|
|
12289
|
+
}
|
|
12290
|
+
const page = res.body.data;
|
|
12291
|
+
if (hasFlag("--json")) {
|
|
12292
|
+
console.log(JSON.stringify({ ...page, org: { id: org.id, name: org.name } }, null, 2));
|
|
12293
|
+
return 0;
|
|
12294
|
+
}
|
|
12295
|
+
console.log(`组织:${hrOrgToken(org)} · 共 ${page.total} 条`);
|
|
12296
|
+
for (const event of page.events) {
|
|
12297
|
+
const day = new Date(event.at).toISOString().slice(0, 10);
|
|
12298
|
+
console.log(`${day} [${event.type}] ${event.note} person: ${event.personId}`);
|
|
12299
|
+
}
|
|
12300
|
+
if (page.events.length < page.total)
|
|
12301
|
+
console.log(`(还有更多:--offset ${(Number(offset ?? 0) || 0) + page.events.length})`);
|
|
12302
|
+
return 0;
|
|
12303
|
+
}
|
|
12304
|
+
function parseHrImportFile(text) {
|
|
12305
|
+
try {
|
|
12306
|
+
const parsed = JSON.parse(text);
|
|
12307
|
+
if (Array.isArray(parsed))
|
|
12308
|
+
return { rows: parsed };
|
|
12309
|
+
} catch {}
|
|
12310
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
12311
|
+
if (lines.length < 2) {
|
|
12312
|
+
console.error("文件至少要有表头 + 一行数据(TSV/CSV),或 JSON 行数组。");
|
|
12313
|
+
return EXIT_USAGE;
|
|
12314
|
+
}
|
|
12315
|
+
const delimiter = lines[0].includes("\t") ? "\t" : ",";
|
|
12316
|
+
const cells = lines.map((line) => line.split(delimiter).map((cell) => cell.trim().replace(/^"|"$/g, "")));
|
|
12317
|
+
const aliases = [
|
|
12318
|
+
["name", ["姓名", "名字", "name"]],
|
|
12319
|
+
["employeeNo", ["工号", "员工编号", "emp"]],
|
|
12320
|
+
["dept", ["部门", "dept"]],
|
|
12321
|
+
["role", ["岗位", "职位", "role"]],
|
|
12322
|
+
["hiredAt", ["入职", "hire"]],
|
|
12323
|
+
["probationEnd", ["试用期", "probation"]],
|
|
12324
|
+
["contractType", ["合同类型", "contract type"]],
|
|
12325
|
+
["contractEnd", ["合同到期", "合同终止", "contract end"]],
|
|
12326
|
+
["mobile", ["手机", "电话", "mobile", "phone"]],
|
|
12327
|
+
["email", ["邮箱", "email"]],
|
|
12328
|
+
["notes", ["备注", "note"]]
|
|
12329
|
+
];
|
|
12330
|
+
const mapping = (cells[0] ?? []).map((header) => {
|
|
12331
|
+
const lower = header.toLowerCase();
|
|
12332
|
+
for (const [field, list] of aliases)
|
|
12333
|
+
if (list.some((alias) => lower.includes(alias)))
|
|
12334
|
+
return field;
|
|
12335
|
+
return null;
|
|
12336
|
+
});
|
|
12337
|
+
const nameIndex = mapping.indexOf("name");
|
|
12338
|
+
const hireIndex = mapping.indexOf("hiredAt");
|
|
12339
|
+
if (nameIndex < 0 || hireIndex < 0) {
|
|
12340
|
+
console.error("表头里没认出「姓名」和「入职」列——JSON 数组格式不受此限。");
|
|
12341
|
+
return EXIT_USAGE;
|
|
12342
|
+
}
|
|
12343
|
+
const rows = [];
|
|
12344
|
+
for (const row of cells.slice(1)) {
|
|
12345
|
+
const record = {};
|
|
12346
|
+
let skip = false;
|
|
12347
|
+
mapping.forEach((field, index) => {
|
|
12348
|
+
if (!field)
|
|
12349
|
+
return;
|
|
12350
|
+
const value = (row[index] ?? "").trim();
|
|
12351
|
+
if (!value)
|
|
12352
|
+
return;
|
|
12353
|
+
if (field === "hiredAt" || field === "probationEnd" || field === "contractEnd") {
|
|
12354
|
+
const [y, m, d] = value.replace(/[/.]/g, "-").split("-").map(Number);
|
|
12355
|
+
const ms = y && m && d ? new Date(y, m - 1, d).getTime() : NaN;
|
|
12356
|
+
if (!Number.isFinite(ms)) {
|
|
12357
|
+
skip = true;
|
|
12358
|
+
return;
|
|
12359
|
+
}
|
|
12360
|
+
record[field] = ms;
|
|
12361
|
+
} else {
|
|
12362
|
+
record[field] = value;
|
|
12363
|
+
}
|
|
12364
|
+
});
|
|
12365
|
+
if (!skip && record.name && record.hiredAt)
|
|
12366
|
+
rows.push(record);
|
|
12367
|
+
}
|
|
12368
|
+
return { rows };
|
|
12369
|
+
}
|
|
12370
|
+
async function hrImport() {
|
|
12371
|
+
const file = positionalArgs(args.slice(2), HR_VALUED_FLAGS)[0];
|
|
12372
|
+
if (!file) {
|
|
12373
|
+
console.error("用法:crab hr import <文件.csv|.tsv|.json> [--org <arg>] [--json](≤200 行;CSV/TSV 首行表头,姓名与入职列必填)");
|
|
12374
|
+
return EXIT_USAGE;
|
|
12375
|
+
}
|
|
12376
|
+
let text;
|
|
12377
|
+
try {
|
|
12378
|
+
text = readFileSync2(file, "utf8");
|
|
12379
|
+
} catch {
|
|
12380
|
+
console.error(`读取文件失败:${file}`);
|
|
12381
|
+
return EXIT_USAGE;
|
|
12382
|
+
}
|
|
12383
|
+
const parsed = parseHrImportFile(text);
|
|
12384
|
+
if (typeof parsed === "number")
|
|
12385
|
+
return parsed;
|
|
12386
|
+
if (parsed.rows.length === 0) {
|
|
12387
|
+
console.error("没有可导入的行(姓名/入职日期缺失或日期格式不对)。");
|
|
12388
|
+
return EXIT_USAGE;
|
|
12389
|
+
}
|
|
12390
|
+
const credentials = requireCredentials();
|
|
12391
|
+
if (typeof credentials === "number")
|
|
12392
|
+
return credentials;
|
|
12393
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12394
|
+
if (typeof org === "number")
|
|
12395
|
+
return org;
|
|
12396
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/import`, {
|
|
12397
|
+
method: "POST",
|
|
12398
|
+
headers: authHeaders(credentials),
|
|
12399
|
+
body: JSON.stringify({ rows: parsed.rows.slice(0, 200) })
|
|
12400
|
+
});
|
|
12401
|
+
if (res.status !== 200) {
|
|
12402
|
+
console.error(errorMessage(res.body, "导入失败。"));
|
|
12403
|
+
return exitCodeForApiError(res.body);
|
|
12404
|
+
}
|
|
12405
|
+
const report = res.body.data.report;
|
|
12406
|
+
if (hasFlag("--json"))
|
|
12407
|
+
console.log(JSON.stringify(report, null, 2));
|
|
12408
|
+
else {
|
|
12409
|
+
console.log(`✓ 导入完成:新建 ${report.created} 人${parsed.rows.length > 200 ? `(仅前 200 行,共 ${parsed.rows.length} 行)` : ""}。`);
|
|
12410
|
+
if (report.failed.length)
|
|
12411
|
+
console.log(` 失败 ${report.failed.length} 行:#${report.failed.map((f) => f.row + 1).join(", #")}`);
|
|
12412
|
+
}
|
|
12413
|
+
return 0;
|
|
12414
|
+
}
|
|
12415
|
+
async function hrTasksList() {
|
|
12416
|
+
const credentials = requireCredentials();
|
|
12417
|
+
if (typeof credentials === "number")
|
|
12418
|
+
return credentials;
|
|
12419
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12420
|
+
if (typeof org === "number")
|
|
12421
|
+
return org;
|
|
12422
|
+
const status = readFlag("--status");
|
|
12423
|
+
if (status && !HR_TASK_STATUSES.includes(status)) {
|
|
12424
|
+
console.error(`未知状态:${status}
|
|
12425
|
+
可用:${HR_TASK_STATUSES.join(", ")}`);
|
|
12426
|
+
return EXIT_USAGE;
|
|
12427
|
+
}
|
|
12428
|
+
const params = new URLSearchParams;
|
|
12429
|
+
if (status)
|
|
12430
|
+
params.set("status", status);
|
|
12431
|
+
const kind = readFlag("--kind");
|
|
12432
|
+
if (kind)
|
|
12433
|
+
params.set("kind", kind);
|
|
12434
|
+
const limit = readFlag("--limit");
|
|
12435
|
+
if (limit)
|
|
12436
|
+
params.set("limit", limit);
|
|
12437
|
+
const offset = readFlag("--offset");
|
|
12438
|
+
if (offset)
|
|
12439
|
+
params.set("offset", offset);
|
|
12440
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/tasks?${params}`, { headers: authHeaders(credentials) });
|
|
12441
|
+
if (res.status !== 200) {
|
|
12442
|
+
console.error(errorMessage(res.body, "获取任务失败。"));
|
|
12443
|
+
return exitCodeForApiError(res.body);
|
|
12444
|
+
}
|
|
12445
|
+
const page = res.body.data;
|
|
12446
|
+
if (hasFlag("--json")) {
|
|
12447
|
+
console.log(JSON.stringify({ ...page, org: { id: org.id, name: org.name } }, null, 2));
|
|
12448
|
+
return 0;
|
|
12449
|
+
}
|
|
12450
|
+
console.log(`组织:${hrOrgToken(org)} · 共 ${page.total} 条`);
|
|
12451
|
+
for (const task of page.tasks) {
|
|
12452
|
+
const due = task.dueAt ? new Date(task.dueAt).toISOString().slice(0, 10) : "—";
|
|
12453
|
+
console.log(`[${task.status}] ${task.title} ${task.node || ""} (${task.kind} · 截止 ${due}) id: ${task.id}`);
|
|
12454
|
+
}
|
|
12455
|
+
if (page.tasks.length < page.total)
|
|
12456
|
+
console.log(`(还有更多:--offset ${(Number(offset ?? 0) || 0) + page.tasks.length})`);
|
|
12457
|
+
return 0;
|
|
12458
|
+
}
|
|
12459
|
+
async function hrTaskCreate() {
|
|
12460
|
+
const kind = readFlag("--kind");
|
|
12461
|
+
const title = readFlag("--title");
|
|
12462
|
+
if (!kind || !title) {
|
|
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]');
|
|
12464
|
+
return EXIT_USAGE;
|
|
12465
|
+
}
|
|
12466
|
+
const status = readFlag("--status");
|
|
12467
|
+
if (status && !HR_TASK_STATUSES.includes(status)) {
|
|
12468
|
+
console.error(`未知状态:${status}
|
|
12469
|
+
可用:${HR_TASK_STATUSES.join(", ")}`);
|
|
12470
|
+
return EXIT_USAGE;
|
|
12471
|
+
}
|
|
12472
|
+
const due = parseHrDate(readFlag("--due"));
|
|
12473
|
+
if (due === null) {
|
|
12474
|
+
console.error("--due 格式应为 YYYY-MM-DD。");
|
|
12475
|
+
return EXIT_USAGE;
|
|
12476
|
+
}
|
|
12477
|
+
const credentials = requireCredentials();
|
|
12478
|
+
if (typeof credentials === "number")
|
|
12479
|
+
return credentials;
|
|
12480
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12481
|
+
if (typeof org === "number")
|
|
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
|
+
}
|
|
12493
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/tasks`, {
|
|
12494
|
+
method: "POST",
|
|
12495
|
+
headers: authHeaders(credentials),
|
|
12496
|
+
body: JSON.stringify({ kind, title, node: readFlag("--node") ?? "", detail: readFlag("--detail") ?? "", ...status ? { status } : {}, ...due != null ? { dueAt: due } : {}, ...meta ? { meta } : {} })
|
|
12497
|
+
});
|
|
12498
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
12499
|
+
console.error(errorMessage(res.body, "创建任务失败。"));
|
|
12500
|
+
return exitCodeForApiError(res.body);
|
|
12501
|
+
}
|
|
12502
|
+
const task = res.body.data.task;
|
|
12503
|
+
if (hasFlag("--json"))
|
|
12504
|
+
console.log(JSON.stringify(task, null, 2));
|
|
12505
|
+
else
|
|
12506
|
+
console.log(`✓ 已创建任务 ${task.title} · id: ${task.id}`);
|
|
12507
|
+
return 0;
|
|
12508
|
+
}
|
|
12509
|
+
async function hrTaskUpdate() {
|
|
12510
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
12511
|
+
const status = readFlag("--status");
|
|
12512
|
+
if (!target || !status && readFlag("--node") === undefined && readFlag("--detail") === undefined && readFlag("--due") === undefined) {
|
|
12513
|
+
console.error("用法:crab hr task update <id> [--status alert|confirm|queued|done] [--node 节点说明] [--detail 详情] [--due YYYY-MM-DD] [--json]");
|
|
12514
|
+
return EXIT_USAGE;
|
|
12515
|
+
}
|
|
12516
|
+
if (status && !HR_TASK_STATUSES.includes(status)) {
|
|
12517
|
+
console.error(`未知状态:${status}
|
|
12518
|
+
可用:${HR_TASK_STATUSES.join(", ")}`);
|
|
12519
|
+
return EXIT_USAGE;
|
|
12520
|
+
}
|
|
12521
|
+
const due = parseHrDate(readFlag("--due"));
|
|
12522
|
+
if (due === null) {
|
|
12523
|
+
console.error("--due 格式应为 YYYY-MM-DD。");
|
|
12524
|
+
return EXIT_USAGE;
|
|
12525
|
+
}
|
|
12526
|
+
const credentials = requireCredentials();
|
|
12527
|
+
if (typeof credentials === "number")
|
|
12528
|
+
return credentials;
|
|
12529
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12530
|
+
if (typeof org === "number")
|
|
12531
|
+
return org;
|
|
12532
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/tasks/${encodeURIComponent(target)}`, {
|
|
12533
|
+
method: "PATCH",
|
|
12534
|
+
headers: authHeaders(credentials),
|
|
12535
|
+
body: JSON.stringify({ ...status ? { status } : {}, ...readFlag("--node") !== undefined ? { node: readFlag("--node") } : {}, ...readFlag("--detail") !== undefined ? { detail: readFlag("--detail") } : {}, ...due != null ? { dueAt: due } : {} })
|
|
12536
|
+
});
|
|
12537
|
+
if (res.status !== 200) {
|
|
12538
|
+
console.error(errorMessage(res.body, "更新任务失败。"));
|
|
12539
|
+
return exitCodeForApiError(res.body);
|
|
12540
|
+
}
|
|
12541
|
+
const task = res.body.data.task;
|
|
12542
|
+
if (hasFlag("--json"))
|
|
12543
|
+
console.log(JSON.stringify(task, null, 2));
|
|
12544
|
+
else
|
|
12545
|
+
console.log(`✓ 已更新 ${task.title}(${task.status})。`);
|
|
12546
|
+
return 0;
|
|
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
|
+
}
|
|
12584
|
+
async function hrDesk() {
|
|
12585
|
+
const credentials = requireCredentials();
|
|
12586
|
+
if (typeof credentials === "number")
|
|
12587
|
+
return credentials;
|
|
12588
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
12589
|
+
if (typeof org === "number")
|
|
12590
|
+
return org;
|
|
12591
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/desk`, { headers: authHeaders(credentials) });
|
|
12592
|
+
if (res.status !== 200) {
|
|
12593
|
+
console.error(errorMessage(res.body, "获取工作台失败。"));
|
|
12594
|
+
return exitCodeForApiError(res.body);
|
|
12595
|
+
}
|
|
12596
|
+
const desk = res.body.data;
|
|
12597
|
+
if (hasFlag("--json")) {
|
|
12598
|
+
console.log(JSON.stringify({ ...desk, org: { id: org.id, name: org.name } }, null, 2));
|
|
12599
|
+
return 0;
|
|
12600
|
+
}
|
|
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(" (无)");
|
|
12612
|
+
console.log(`
|
|
12613
|
+
等你拍板:`);
|
|
12614
|
+
for (const task of desk.desk.pending)
|
|
12615
|
+
console.log(` [${task.status}] ${task.title}${task.refLabel ? ` · ${task.refLabel}` : ""} ${task.node || ""} id: ${task.id}`);
|
|
12616
|
+
if (!desk.desk.pending.length)
|
|
12617
|
+
console.log(" (空)");
|
|
12618
|
+
console.log(`
|
|
12619
|
+
助理在办:`);
|
|
12620
|
+
for (const task of desk.desk.running)
|
|
12621
|
+
console.log(` ${task.title}${task.refLabel ? ` · ${task.refLabel}` : ""} ${task.node || ""} id: ${task.id}`);
|
|
12622
|
+
if (!desk.desk.running.length)
|
|
12623
|
+
console.log(" (空)");
|
|
12624
|
+
console.log(`
|
|
12625
|
+
近期完成:`);
|
|
12626
|
+
for (const task of desk.desk.done)
|
|
12627
|
+
console.log(` ${task.title}${task.refLabel ? ` · ${task.refLabel}` : ""} ${task.node || ""}`);
|
|
12628
|
+
if (!desk.desk.done.length)
|
|
12629
|
+
console.log(" (空)");
|
|
12630
|
+
console.log(`
|
|
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)
|
|
12637
|
+
console.log(" (无)");
|
|
12638
|
+
console.log(`
|
|
12639
|
+
最近动态:`);
|
|
12640
|
+
for (const event of desk.desk.recentEvents.slice(0, 8)) {
|
|
12641
|
+
const day = new Date(event.at).toISOString().slice(0, 10);
|
|
12642
|
+
console.log(` ${day} ${event.personName ? `${event.personName} · ` : ""}${event.note}`);
|
|
12643
|
+
}
|
|
12644
|
+
if (!desk.desk.recentEvents.length)
|
|
12645
|
+
console.log(" (无)");
|
|
12646
|
+
return 0;
|
|
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
|
+
}
|
|
12901
|
+
switch (command) {
|
|
12902
|
+
case "init":
|
|
12903
|
+
process.exit(await init());
|
|
12904
|
+
case "login":
|
|
12905
|
+
process.exit(await login());
|
|
12906
|
+
case "whoami":
|
|
12907
|
+
process.exit(await whoami());
|
|
12908
|
+
case "tokens": {
|
|
12909
|
+
const sub = args[1];
|
|
12910
|
+
if (sub === "list")
|
|
12911
|
+
process.exit(await tokensList());
|
|
12912
|
+
if (sub === "revoke")
|
|
12913
|
+
process.exit(await tokensRevoke(args[2]));
|
|
12914
|
+
console.error(sub ? `未知子命令:tokens ${sub}
|
|
12915
|
+
` : `缺少子命令:tokens list | tokens revoke <id>
|
|
12916
|
+
`);
|
|
12917
|
+
console.log(usage);
|
|
12918
|
+
process.exit(EXIT_USAGE);
|
|
12919
|
+
}
|
|
12920
|
+
case "skills": {
|
|
12921
|
+
const sub = args[1];
|
|
12922
|
+
if (sub === "install")
|
|
12923
|
+
process.exit(installSkills(args.slice(2)));
|
|
12924
|
+
if (sub === "list")
|
|
12925
|
+
process.exit(listSkills());
|
|
12926
|
+
if (sub === "uninstall")
|
|
12927
|
+
process.exit(uninstallSkills(args.slice(2)));
|
|
12928
|
+
console.error(`用法:crab skills install | list | uninstall [--agent claude,zcode,codex,agents]
|
|
12929
|
+
`);
|
|
12930
|
+
process.exit(EXIT_USAGE);
|
|
12931
|
+
}
|
|
12932
|
+
case "mail": {
|
|
12933
|
+
const sub = args[1];
|
|
12934
|
+
if (sub === "list")
|
|
12935
|
+
process.exit(await mailList());
|
|
12936
|
+
if (sub === "read")
|
|
12937
|
+
process.exit(await mailRead(args[2]));
|
|
12938
|
+
if (sub === "unread")
|
|
12939
|
+
process.exit(await mailUnread(args[2]));
|
|
12940
|
+
if (sub === "search")
|
|
12941
|
+
process.exit(await mailSearch(args.slice(2).join(" ") || undefined));
|
|
12942
|
+
if (sub === "send")
|
|
12943
|
+
process.exit(await mailSend());
|
|
12944
|
+
if (sub === "archive" || sub === "trash" || sub === "delete")
|
|
12945
|
+
process.exit(await mailMove(sub, args[2]));
|
|
12946
|
+
if (sub === "folders")
|
|
12947
|
+
process.exit(await mailFoldersOverview());
|
|
12948
|
+
if (sub === "activity")
|
|
12949
|
+
process.exit(await mailActivity());
|
|
12950
|
+
console.error(sub ? `未知子命令:mail ${sub}
|
|
12951
|
+
` : `缺少子命令:mail list | read | unread | search | send | archive | trash | delete | folders | activity
|
|
12952
|
+
`);
|
|
12953
|
+
console.log(usage);
|
|
12954
|
+
process.exit(EXIT_USAGE);
|
|
12955
|
+
}
|
|
12956
|
+
case "contacts": {
|
|
12957
|
+
const sub = args[1];
|
|
12958
|
+
if (sub === "list")
|
|
12959
|
+
process.exit(await contactsList(args.slice(2).join(" ") || undefined));
|
|
12960
|
+
if (sub === "add")
|
|
12961
|
+
process.exit(await contactsAdd(args[2]));
|
|
12962
|
+
if (sub === "rm")
|
|
12963
|
+
process.exit(await contactsRm(args[2]));
|
|
9232
12964
|
if (sub === "invite")
|
|
9233
12965
|
process.exit(await contactsInvite(hasFlag("--reset")));
|
|
9234
12966
|
if (sub === "invitees")
|
|
9235
12967
|
process.exit(await contactsInvitees());
|
|
9236
12968
|
console.error(sub ? `未知子命令:contacts ${sub}
|
|
9237
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
|
|
9238
13004
|
`);
|
|
9239
13005
|
console.log(usage);
|
|
9240
13006
|
process.exit(EXIT_USAGE);
|
|
@@ -9297,6 +13063,88 @@ switch (command) {
|
|
|
9297
13063
|
process.exit(await agentApprovals());
|
|
9298
13064
|
console.error(sub ? `未知子命令:agent ${sub}
|
|
9299
13065
|
` : `缺少子命令:agent status | sessions | approvals
|
|
13066
|
+
`);
|
|
13067
|
+
console.log(usage);
|
|
13068
|
+
process.exit(EXIT_USAGE);
|
|
13069
|
+
}
|
|
13070
|
+
case "shop": {
|
|
13071
|
+
const sub = args[1];
|
|
13072
|
+
if (sub === "status")
|
|
13073
|
+
process.exit(await shopStatus());
|
|
13074
|
+
if (sub === "roles")
|
|
13075
|
+
process.exit(await shopRoles());
|
|
13076
|
+
if (sub === "members")
|
|
13077
|
+
process.exit(await shopMembers());
|
|
13078
|
+
if (sub === "member") {
|
|
13079
|
+
if (args[2] === "edit")
|
|
13080
|
+
process.exit(await shopMemberEdit());
|
|
13081
|
+
console.error(`用法:crab shop member edit <用户名> --perms <a,b,c> [--biz sales,support] --store <slug>
|
|
13082
|
+
`);
|
|
13083
|
+
process.exit(EXIT_USAGE);
|
|
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());
|
|
13105
|
+
if (sub === "categories")
|
|
13106
|
+
process.exit(await shopCategories());
|
|
13107
|
+
if (sub === "category") {
|
|
13108
|
+
if (args[2] === "add")
|
|
13109
|
+
process.exit(await shopCategoryAdd());
|
|
13110
|
+
if (args[2] === "rename")
|
|
13111
|
+
process.exit(await shopCategoryRename());
|
|
13112
|
+
if (args[2] === "move")
|
|
13113
|
+
process.exit(await shopCategoryMove());
|
|
13114
|
+
if (args[2] === "delete")
|
|
13115
|
+
process.exit(await shopCategoryDelete());
|
|
13116
|
+
console.error(`用法:crab shop category add|rename|move|delete …(crab --help 看完整用法)
|
|
13117
|
+
`);
|
|
13118
|
+
process.exit(EXIT_USAGE);
|
|
13119
|
+
}
|
|
13120
|
+
if (sub === "invite")
|
|
13121
|
+
process.exit(await shopInvite());
|
|
13122
|
+
if (sub === "use") {
|
|
13123
|
+
const target = args.slice(2).find((x) => !x.startsWith("--"));
|
|
13124
|
+
process.exit(await shopUse(target));
|
|
13125
|
+
}
|
|
13126
|
+
if (sub === "apply") {
|
|
13127
|
+
const positional = [];
|
|
13128
|
+
const rest = args.slice(2);
|
|
13129
|
+
for (let i = 0;i < rest.length; i++) {
|
|
13130
|
+
if (rest[i] === "--tagline") {
|
|
13131
|
+
i++;
|
|
13132
|
+
continue;
|
|
13133
|
+
}
|
|
13134
|
+
if (rest[i]?.startsWith("--"))
|
|
13135
|
+
continue;
|
|
13136
|
+
positional.push(rest[i]);
|
|
13137
|
+
}
|
|
13138
|
+
const taglineArg = readFlag("--tagline");
|
|
13139
|
+
if (positional.length === 0) {
|
|
13140
|
+
console.error(`用法:crab shop apply <店铺名> [--tagline 一句话介绍] [--json]
|
|
13141
|
+
`);
|
|
13142
|
+
process.exit(EXIT_USAGE);
|
|
13143
|
+
}
|
|
13144
|
+
process.exit(await shopApply(positional.join(" "), taglineArg));
|
|
13145
|
+
}
|
|
13146
|
+
console.error(sub ? `未知子命令:shop ${sub}
|
|
13147
|
+
` : `缺少子命令:shop status | apply | products | product | stock | listed | movements | categories | category | members | member | invite | use | roles
|
|
9300
13148
|
`);
|
|
9301
13149
|
console.log(usage);
|
|
9302
13150
|
process.exit(EXIT_USAGE);
|
|
@@ -9353,6 +13201,103 @@ switch (command) {
|
|
|
9353
13201
|
}
|
|
9354
13202
|
case "post":
|
|
9355
13203
|
process.exit(await collabPushRecord(args[1]));
|
|
13204
|
+
case "hr": {
|
|
13205
|
+
const sub = args[1];
|
|
13206
|
+
if (sub === "orgs")
|
|
13207
|
+
process.exit(await hrOrgsList());
|
|
13208
|
+
if (sub === "org") {
|
|
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 <新名称>
|
|
13216
|
+
`);
|
|
13217
|
+
process.exit(EXIT_USAGE);
|
|
13218
|
+
}
|
|
13219
|
+
if (sub === "members")
|
|
13220
|
+
process.exit(await hrMembersList());
|
|
13221
|
+
if (sub === "member") {
|
|
13222
|
+
const op = args[2];
|
|
13223
|
+
if (op === "add")
|
|
13224
|
+
process.exit(await hrMemberAdd());
|
|
13225
|
+
if (op === "set")
|
|
13226
|
+
process.exit(await hrMemberSet());
|
|
13227
|
+
if (op === "rm")
|
|
13228
|
+
process.exit(await hrMemberRm());
|
|
13229
|
+
console.error(`用法:crab hr member add|set|rm
|
|
13230
|
+
`);
|
|
13231
|
+
process.exit(EXIT_USAGE);
|
|
13232
|
+
}
|
|
13233
|
+
if (sub === "invite")
|
|
13234
|
+
process.exit(await hrInvite());
|
|
13235
|
+
if (sub === "people")
|
|
13236
|
+
process.exit(await hrPeopleList());
|
|
13237
|
+
if (sub === "stats")
|
|
13238
|
+
process.exit(await hrStats());
|
|
13239
|
+
if (sub === "person") {
|
|
13240
|
+
const op = args[2];
|
|
13241
|
+
if (op === "create")
|
|
13242
|
+
process.exit(await hrPersonCreate());
|
|
13243
|
+
if (op === "edit")
|
|
13244
|
+
process.exit(await hrPersonEdit());
|
|
13245
|
+
if (op === "offboard")
|
|
13246
|
+
process.exit(await hrPersonOffboard());
|
|
13247
|
+
if (op === "rm")
|
|
13248
|
+
process.exit(await hrPersonRm());
|
|
13249
|
+
process.exit(await hrPersonDetail(op));
|
|
13250
|
+
}
|
|
13251
|
+
if (sub === "event") {
|
|
13252
|
+
if (args[2] === "add")
|
|
13253
|
+
process.exit(await hrEventAdd());
|
|
13254
|
+
console.error(`用法:crab hr event add <personId> --type <类型> --note "..."
|
|
13255
|
+
`);
|
|
13256
|
+
process.exit(EXIT_USAGE);
|
|
13257
|
+
}
|
|
13258
|
+
if (sub === "events")
|
|
13259
|
+
process.exit(await hrEventsList());
|
|
13260
|
+
if (sub === "import")
|
|
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
|
+
}
|
|
13273
|
+
if (sub === "tasks")
|
|
13274
|
+
process.exit(await hrTasksList());
|
|
13275
|
+
if (sub === "task") {
|
|
13276
|
+
const op = args[2];
|
|
13277
|
+
if (op === "create")
|
|
13278
|
+
process.exit(await hrTaskCreate());
|
|
13279
|
+
if (op === "update")
|
|
13280
|
+
process.exit(await hrTaskUpdate());
|
|
13281
|
+
if (op === "link")
|
|
13282
|
+
process.exit(await hrTaskLink());
|
|
13283
|
+
console.error(`用法:crab hr task create|update|link
|
|
13284
|
+
`);
|
|
13285
|
+
process.exit(EXIT_USAGE);
|
|
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());
|
|
13293
|
+
if (sub === "desk")
|
|
13294
|
+
process.exit(await hrDesk());
|
|
13295
|
+
console.error(sub ? `未知子命令:hr ${sub}
|
|
13296
|
+
` : `缺少子命令:hr orgs | org apply | members | member | invite | people | stats | person | event add | events | import | dicts | dict | tasks | task | apply-link | announce | inbox | desk
|
|
13297
|
+
`);
|
|
13298
|
+
console.log(usage);
|
|
13299
|
+
process.exit(EXIT_USAGE);
|
|
13300
|
+
}
|
|
9356
13301
|
case "logout":
|
|
9357
13302
|
process.exit(await logout());
|
|
9358
13303
|
case "version":
|